chore(qa): replace the keyword-count harness with a structural lint and gate tests

The old harness scored occurrences of words like VERIFICAR, so its score
measured wording, and it failed on a fresh clone. The lint checks frontmatter,
size limits, references and links; test_gate.py exercises the gate and hooks
on a throwaway repo; check_upstream.py flags vendored skills behind upstream.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
EvandroandClaude Opus 5.5 committed 2026-09-24 21:52:06 -03:00
1 parent 29b5ebf400
commit f77b0b440d
15 files changed
+560 -1549

No files matched your search

-236
View File
@@ -1,236 +0,0 @@
{
"workflows": [
{
"name": "wf-frontend",
"score": 385.5,
"skills_referenciadas": [
"animate",
"anti-ai-slop",
"avoid-ai-writing",
"claude-design",
"design-md",
"frontend-award-tier",
"frontend-design",
"hermaguard",
"html-css-print-engineer",
"pdf-generator",
"pdf-render-qa",
"pdf-visual-designer",
"popular-web-designs",
"professional-pdf-director",
"sketch",
"vercel-composition-patterns",
"vercel-react-best-practices",
"web-design"
],
"criteria": {
"frontmatter": 5,
"when_to_use": 8,
"num_fases": 6,
"regras_duras": 10,
"n_regras": 9,
"fase1": 16,
"fase2": 16,
"fase3": 16,
"cobertura_fases": 20,
"skills_validas": 15,
"fase_rotulos": 6,
"fase_acao": 6,
"fase_revisao": 6,
"anti_slop": 4.5,
"cmd_blocos": 8,
"cmd_reais": 5,
"sec_criterios": 8,
"sec_verificar": 6,
"sec_nao_regressao": 6,
"tamanho": 8,
"testes_referidos": 5,
"passos_total": 30,
"verificacao_total": 24,
"comandos_reais": 20,
"profundidade": 10,
"por_fase_verif": 18,
"por_fase_cmd": 18,
"por_fase_antip": 12,
"por_fase_aceite": 12,
"exemplos": 10,
"exemplos_count": 12,
"artefatos": 14,
"fluxo": 8,
"entradas_saidas": 8
}
},
{
"name": "wf-backend",
"score": 424.0,
"skills_referenciadas": [
"api-security-review",
"auth-rbac-scaffold",
"bola-detector",
"dependency-cve-audit",
"fastapi-uvicorn-wiring",
"hermaguard",
"injection-checker",
"network-audit",
"openapi-hardener",
"rest-graphql-debug",
"secure-coding",
"security-best-practices",
"security-test-generator",
"software-code-quality",
"test-driven-development"
],
"criteria": {
"frontmatter": 5,
"when_to_use": 8,
"num_fases": 8,
"regras_duras": 10,
"n_regras": 10,
"fase1": 15,
"fase2": 15,
"fase3": 16,
"fase4": 16,
"cobertura_fases": 20,
"skills_validas": 15,
"fase_rotulos": 6,
"fase_acao": 6,
"fase_revisao": 6,
"anti_slop": 5.0,
"cmd_blocos": 8,
"cmd_reais": 5,
"sec_criterios": 8,
"sec_verificar": 6,
"sec_nao_regressao": 6,
"tamanho": 8,
"testes_referidos": 5,
"passos_total": 30,
"verificacao_total": 24,
"comandos_reais": 20,
"profundidade": 13,
"por_fase_verif": 22,
"por_fase_cmd": 24,
"por_fase_antip": 16,
"por_fase_aceite": 16,
"exemplos": 10,
"exemplos_count": 12,
"artefatos": 14,
"fluxo": 8,
"entradas_saidas": 8
}
},
{
"name": "wf-architecture",
"score": 413.7,
"skills_referenciadas": [
"architecture-diagram",
"avoid-ai-writing",
"brainstorming",
"c4-architecture",
"excalidraw",
"hermaguard",
"isaqb-architecture-governance",
"secure-architecture-governance",
"security-threat-model",
"system-design",
"writing-plans"
],
"criteria": {
"frontmatter": 5,
"when_to_use": 8,
"num_fases": 8,
"regras_duras": 10,
"n_regras": 10,
"fase1": 15,
"fase2": 16,
"fase3": 14,
"fase4": 16,
"cobertura_fases": 20,
"skills_validas": 11,
"fase_rotulos": 6,
"fase_acao": 0,
"fase_revisao": 6,
"anti_slop": 4.7,
"cmd_blocos": 8,
"cmd_reais": 5,
"sec_criterios": 8,
"sec_verificar": 6,
"sec_nao_regressao": 6,
"tamanho": 8,
"testes_referidos": 5,
"passos_total": 30,
"verificacao_total": 24,
"comandos_reais": 20,
"profundidade": 13,
"por_fase_verif": 24,
"por_fase_cmd": 24,
"por_fase_antip": 16,
"por_fase_aceite": 15,
"exemplos": 10,
"exemplos_count": 12,
"artefatos": 14,
"fluxo": 8,
"entradas_saidas": 8
}
},
{
"name": "wf-security-review",
"score": 390.0,
"skills_referenciadas": [
"api-security-review",
"auth-rbac-scaffold",
"bola-detector",
"code-review-security",
"cve-triage",
"dependency-cve-audit",
"hermaguard",
"injection-checker",
"patch-prioritization",
"safedeps",
"sca-audit",
"secrets-scan",
"security-threat-model",
"web-security-review"
],
"criteria": {
"frontmatter": 5,
"when_to_use": 8,
"num_fases": 10,
"regras_duras": 10,
"n_regras": 8,
"fase1": 15,
"fase2": 16,
"fase3": 13,
"fase4": 15,
"fase5": 15,
"cobertura_fases": 20,
"skills_validas": 14,
"fase_rotulos": 6,
"fase_acao": 0,
"fase_revisao": 6,
"anti_slop": 5.0,
"cmd_blocos": 8,
"cmd_reais": 5,
"sec_criterios": 0,
"sec_verificar": 6,
"sec_nao_regressao": 6,
"tamanho": 8,
"testes_referidos": 5,
"passos_total": 30,
"verificacao_total": 24,
"comandos_reais": 16,
"profundidade": 15,
"por_fase_verif": 23,
"por_fase_cmd": 19,
"por_fase_antip": 12,
"por_fase_aceite": 5,
"exemplos": 0,
"exemplos_count": 12,
"artefatos": 14,
"fluxo": 8,
"entradas_saidas": 8
}
}
],
"total": 1613.2,
"max": 400
}
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Compares each vendored skill's recorded commit (skills/NOTICE.md) with its upstream HEAD. Exit 1 if any moved.
python3 qa/check_upstream.py
"""
import os, re, subprocess, sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def main():
notice = open(os.path.join(ROOT, "skills", "NOTICE.md"), encoding="utf-8").read()
repos = {}
for row in re.findall(r"^\|\s*`[\w-]+`\s*\|(.+)$", notice, re.M):
cols = [c.strip() for c in row.split("|")]
repo = re.search(r"([\w.-]+/[\w.-]+)", cols[0])
sha = re.search(r"`([0-9a-f]{7,40})`", cols[1])
if repo and sha:
repos[repo.group(1)] = sha.group(1)
moved = 0
for repo, sha in sorted(repos.items()):
p = subprocess.run(["git", "ls-remote", f"https://github.com/{repo}", "HEAD"], capture_output=True, text=True, timeout=60)
head = p.stdout.split()[0] if p.returncode == 0 and p.stdout else None
if head is None:
print(f"? {repo}: não consegui consultar")
elif head.startswith(sha):
print(f"ok {repo} {sha}")
else:
moved += 1
print(f"-> {repo}: vendorizado {sha}, upstream {head[:7]} (https://github.com/{repo}/compare/{sha}...{head[:7]})")
return 1 if moved else 0
if __name__ == "__main__":
sys.exit(main())
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Structural lint for the plugin: frontmatter, sizes, references, links and leftover machine-specific paths.
Run from anywhere: python3 qa/lint_skills.py (exit 1 on any error)
"""
import json, os, re, sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAX_LINES = 150
MAX_DESC = 1024
FORBIDDEN = [r"~/\.hermes", r"vision_analyze", r"search_files", r"test_quality\.py", r"wf_quality_harness"]
MODELS = {"haiku", "sonnet", "opus", "inherit"}
TOOLS = {"Read", "Edit", "Write", "Bash", "Grep", "Glob", "WebFetch", "WebSearch", "NotebookEdit", "Agent", "Skill"}
errors = []
def err(path, msg):
errors.append(f"{os.path.relpath(path, ROOT)}: {msg}")
def frontmatter(path):
text = open(path, encoding="utf-8").read()
m = re.match(r"---\n(.*?)\n---\n", text, re.S)
if not m:
err(path, "sem frontmatter")
return {}, text
fm = m.group(1)
meta = dict(re.findall(r"^([\w-]+):[ \t]*(.*)$", fm, re.M))
for key in ("uses", "external", "skills"):
lst = re.search(rf"^\s*{key}:\s*\[(.*?)\]", fm, re.M)
if lst:
meta[key] = [s.strip().strip("'\"") for s in lst.group(1).split(",") if s.strip()]
return meta, text
def vendored():
rows = open(os.path.join(ROOT, "skills", "NOTICE.md"), encoding="utf-8").read()
return set(re.findall(r"^\|\s*`([\w-]+)`\s*\|", rows, re.M))
def check_links(path, text):
text = re.sub(r"```.*?```|`[^`\n]*`", "", text, flags=re.S)
for target in re.findall(r"\]\(([^)\s]+)\)", text):
if re.match(r"(https?:|mailto:|#)", target):
continue
p = os.path.normpath(os.path.join(os.path.dirname(path), target.split("#")[0]))
if not os.path.exists(p):
err(path, f"link quebrado: {target}")
def main():
skills_dir = os.path.join(ROOT, "skills")
names = {d for d in os.listdir(skills_dir) if os.path.isfile(os.path.join(skills_dir, d, "SKILL.md"))}
third_party = vendored()
catalog = open(os.path.join(ROOT, "docs", "SKILLS.md"), encoding="utf-8").read()
for name in sorted(names):
path = os.path.join(skills_dir, name, "SKILL.md")
meta, text = frontmatter(path)
if meta.get("name") != name:
err(path, f"name '{meta.get('name')}' difere da pasta '{name}'")
desc = meta.get("description", "")
if not desc:
err(path, "description vazia")
if name in third_party:
continue
if len(desc) > MAX_DESC:
err(path, f"description com {len(desc)} caracteres (máx {MAX_DESC})")
lines = text.count("\n")
if lines > MAX_LINES:
err(path, f"{lines} linhas (máx {MAX_LINES}); mova detalhes para references/")
for used in meta.get("uses", []):
if used not in names:
err(path, f"uses: '{used}' não existe em skills/")
for ext in meta.get("external", []):
if f"`{ext}`" not in catalog:
err(path, f"external: '{ext}' não está em docs/SKILLS.md")
for dirpath, _, files in os.walk(os.path.join(skills_dir, name)):
for f in files:
if f.endswith(".md"):
p = os.path.join(dirpath, f)
body = open(p, encoding="utf-8").read()
check_links(p, body)
for pat in FORBIDDEN:
if re.search(pat, body):
err(p, f"referência específica de máquina: {pat}")
agents_dir = os.path.join(ROOT, "agents")
for f in sorted(os.listdir(agents_dir)):
path = os.path.join(agents_dir, f)
meta, text = frontmatter(path)
if meta.get("name") != f[:-3]:
err(path, f"name '{meta.get('name')}' difere do arquivo")
if meta.get("model") not in MODELS:
err(path, f"model '{meta.get('model')}' inválido")
bad = {t.strip() for t in meta.get("tools", "").split(",")} - TOOLS
if bad:
err(path, f"tools desconhecidas: {', '.join(sorted(bad))}")
for s in meta.get("skills", []):
if s not in names:
err(path, f"skills: '{s}' não existe em skills/")
for pat in FORBIDDEN:
if re.search(pat, text):
err(path, f"referência específica de máquina: {pat}")
hooks = json.load(open(os.path.join(ROOT, "hooks", "hooks.json")))
for groups in hooks["hooks"].values():
for g in groups:
for h in g["hooks"]:
for script in re.findall(r"\$\{CLAUDE_PLUGIN_ROOT\}/([\w/.-]+)", h["command"]):
if not os.path.isfile(os.path.join(ROOT, script)):
err(os.path.join(ROOT, "hooks", "hooks.json"), f"script inexistente: {script}")
for doc in ("README.md", "README.pt-BR.md", "CONTRIBUTING.md", os.path.join("docs", "SKILLS.md")):
p = os.path.join(ROOT, doc)
if os.path.exists(p):
check_links(p, open(p, encoding="utf-8").read())
for e in errors:
print("ERRO " + e)
print(f"{len(names)} skills, {len(os.listdir(agents_dir))} agentes, {len(errors)} erro(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())
-236
View File
@@ -1,236 +0,0 @@
{
"workflows": [
{
"name": "wf-frontend",
"score": 385.5,
"skills_referenciadas": [
"animate",
"anti-ai-slop",
"avoid-ai-writing",
"claude-design",
"design-md",
"frontend-award-tier",
"frontend-design",
"hermaguard",
"html-css-print-engineer",
"pdf-generator",
"pdf-render-qa",
"pdf-visual-designer",
"popular-web-designs",
"professional-pdf-director",
"sketch",
"vercel-composition-patterns",
"vercel-react-best-practices",
"web-design"
],
"criteria": {
"frontmatter": 5,
"when_to_use": 8,
"num_fases": 6,
"regras_duras": 10,
"n_regras": 9,
"fase1": 16,
"fase2": 16,
"fase3": 16,
"cobertura_fases": 20,
"skills_validas": 15,
"fase_rotulos": 6,
"fase_acao": 6,
"fase_revisao": 6,
"anti_slop": 4.5,
"cmd_blocos": 8,
"cmd_reais": 5,
"sec_criterios": 8,
"sec_verificar": 6,
"sec_nao_regressao": 6,
"tamanho": 8,
"testes_referidos": 5,
"passos_total": 30,
"verificacao_total": 24,
"comandos_reais": 20,
"profundidade": 10,
"por_fase_verif": 18,
"por_fase_cmd": 18,
"por_fase_antip": 12,
"por_fase_aceite": 12,
"exemplos": 10,
"exemplos_count": 12,
"artefatos": 14,
"fluxo": 8,
"entradas_saidas": 8
}
},
{
"name": "wf-backend",
"score": 424.0,
"skills_referenciadas": [
"api-security-review",
"auth-rbac-scaffold",
"bola-detector",
"dependency-cve-audit",
"fastapi-uvicorn-wiring",
"hermaguard",
"injection-checker",
"network-audit",
"openapi-hardener",
"rest-graphql-debug",
"secure-coding",
"security-best-practices",
"security-test-generator",
"software-code-quality",
"test-driven-development"
],
"criteria": {
"frontmatter": 5,
"when_to_use": 8,
"num_fases": 8,
"regras_duras": 10,
"n_regras": 10,
"fase1": 15,
"fase2": 15,
"fase3": 16,
"fase4": 16,
"cobertura_fases": 20,
"skills_validas": 15,
"fase_rotulos": 6,
"fase_acao": 6,
"fase_revisao": 6,
"anti_slop": 5.0,
"cmd_blocos": 8,
"cmd_reais": 5,
"sec_criterios": 8,
"sec_verificar": 6,
"sec_nao_regressao": 6,
"tamanho": 8,
"testes_referidos": 5,
"passos_total": 30,
"verificacao_total": 24,
"comandos_reais": 20,
"profundidade": 13,
"por_fase_verif": 22,
"por_fase_cmd": 24,
"por_fase_antip": 16,
"por_fase_aceite": 16,
"exemplos": 10,
"exemplos_count": 12,
"artefatos": 14,
"fluxo": 8,
"entradas_saidas": 8
}
},
{
"name": "wf-architecture",
"score": 413.7,
"skills_referenciadas": [
"architecture-diagram",
"avoid-ai-writing",
"brainstorming",
"c4-architecture",
"excalidraw",
"hermaguard",
"isaqb-architecture-governance",
"secure-architecture-governance",
"security-threat-model",
"system-design",
"writing-plans"
],
"criteria": {
"frontmatter": 5,
"when_to_use": 8,
"num_fases": 8,
"regras_duras": 10,
"n_regras": 10,
"fase1": 15,
"fase2": 16,
"fase3": 14,
"fase4": 16,
"cobertura_fases": 20,
"skills_validas": 11,
"fase_rotulos": 6,
"fase_acao": 0,
"fase_revisao": 6,
"anti_slop": 4.7,
"cmd_blocos": 8,
"cmd_reais": 5,
"sec_criterios": 8,
"sec_verificar": 6,
"sec_nao_regressao": 6,
"tamanho": 8,
"testes_referidos": 5,
"passos_total": 30,
"verificacao_total": 24,
"comandos_reais": 20,
"profundidade": 13,
"por_fase_verif": 24,
"por_fase_cmd": 24,
"por_fase_antip": 16,
"por_fase_aceite": 15,
"exemplos": 10,
"exemplos_count": 12,
"artefatos": 14,
"fluxo": 8,
"entradas_saidas": 8
}
},
{
"name": "wf-security-review",
"score": 390.0,
"skills_referenciadas": [
"api-security-review",
"auth-rbac-scaffold",
"bola-detector",
"code-review-security",
"cve-triage",
"dependency-cve-audit",
"hermaguard",
"injection-checker",
"patch-prioritization",
"safedeps",
"sca-audit",
"secrets-scan",
"security-threat-model",
"web-security-review"
],
"criteria": {
"frontmatter": 5,
"when_to_use": 8,
"num_fases": 10,
"regras_duras": 10,
"n_regras": 8,
"fase1": 15,
"fase2": 16,
"fase3": 13,
"fase4": 15,
"fase5": 15,
"cobertura_fases": 20,
"skills_validas": 14,
"fase_rotulos": 6,
"fase_acao": 0,
"fase_revisao": 6,
"anti_slop": 5.0,
"cmd_blocos": 8,
"cmd_reais": 5,
"sec_criterios": 0,
"sec_verificar": 6,
"sec_nao_regressao": 6,
"tamanho": 8,
"testes_referidos": 5,
"passos_total": 30,
"verificacao_total": 24,
"comandos_reais": 16,
"profundidade": 15,
"por_fase_verif": 23,
"por_fase_cmd": 19,
"por_fase_antip": 12,
"por_fase_aceite": 5,
"exemplos": 0,
"exemplos_count": 12,
"artefatos": 14,
"fluxo": 8,
"entradas_saidas": 8
}
}
],
"total": 1613.2,
"max": 400
}
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Smoke tests for scripts/quality-gate and the hook scripts, on a throwaway git repo. Stdlib only.
python3 qa/test_gate.py
"""
import importlib.util, json, os, subprocess, sys, tempfile, unittest
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
GATE = os.path.join(ROOT, "scripts", "quality-gate")
STOP = os.path.join(ROOT, "scripts", "hooks", "stop_gate.py")
spec = importlib.util.spec_from_file_location("bash_guard", os.path.join(ROOT, "scripts", "hooks", "bash_guard.py"))
bash_guard = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bash_guard)
def run(cmd, cwd, stdin=None):
return subprocess.run(cmd, cwd=cwd, input=stdin, capture_output=True, text=True)
class GateTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.repo = self.tmp.name
for c in (["git", "init", "-q"], ["git", "config", "user.email", "t@t"], ["git", "config", "user.name", "t"]):
run(c, self.repo)
self.write("calc.py", "def add(a, b):\n return a + b\n")
self.write("check.py", "from calc import add\nassert add(1, 2) == 3\n")
self.write(".dev-workflows.toml", '[commands]\ntest = "python3 check.py"\nlint = "true"\ntypecheck = "true"\n')
self.write("package.json", '{"dependencies": {"left-pad": "1"}}')
run(["git", "add", "-A"], self.repo)
run(["git", "commit", "-qm", "init"], self.repo)
def tearDown(self):
self.tmp.cleanup()
def write(self, name, text):
open(os.path.join(self.repo, name), "w").write(text)
def gate(self, tier=0):
p = run([sys.executable, GATE, "--tier", str(tier)], self.repo)
return p.returncode, json.load(open(os.path.join(self.repo, ".dev-workflows", "gate.json")))
def stop(self):
out = run([sys.executable, STOP], self.repo, json.dumps({"cwd": self.repo})).stdout
return json.loads(out)["reason"] if out.strip() else None
def status(self, report, name):
return next(c["status"] for c in report["checks"] if c["name"] == name)
def test_clean_tree_does_not_block(self):
self.assertIsNone(self.stop())
def test_change_without_gate_blocks_then_passing_gate_allows(self):
self.write("calc.py", "def add(a, b):\n return a + b\n\n\ndef sub(a, b):\n return a - b\n")
self.assertIn("sem gate", self.stop())
code, report = self.gate()
self.assertEqual(code, 0)
self.assertEqual(self.status(report, "test"), "pass")
self.assertIsNone(self.stop())
def test_failing_test_fails_gate_and_blocks(self):
self.write("calc.py", "def add(a, b):\n return a - b\n")
code, report = self.gate()
self.assertEqual((code, report["verdict"]), (1, "fail"))
self.assertIn("test", self.stop())
def test_missing_tool_is_unverified_not_passed(self):
self.write(".dev-workflows.toml", '[commands]\ntest = "definitely-not-a-tool-xyz"\n')
self.write("calc.py", "def add(a, b):\n return b + a\n")
code, report = self.gate()
self.assertEqual(self.status(report, "test"), "unverified")
self.assertEqual(report["verdict"], "incomplete")
def test_new_dependency_is_reported(self):
self.write("package.json", '{"dependencies": {"left-pad": "1", "is-odd": "3"}}')
self.write("calc.py", "def add(a, b):\n return a + b # touched\n")
_, report = self.gate()
self.assertEqual(report["new_dependencies"], ["npm:is-odd"])
def test_stale_gate_blocks(self):
self.write("calc.py", "def add(a, b):\n return a + b # v2\n")
self.gate()
os.utime(os.path.join(self.repo, ".dev-workflows", "gate.json"), (1, 1))
self.assertIn("mudou depois", self.stop())
class GuardTest(unittest.TestCase):
def test_decisions(self):
cases = {"git commit --no-verify -m x": "deny", "git push -f origin main": "deny",
"git push --force-with-lease": None, "rm -rf /": "deny", "rm -rf node_modules": None,
"git reset --hard": "ask", "npm test": None}
for cmd, expected in cases.items():
self.assertEqual(bash_guard.decide(cmd)[0], expected, cmd)
if __name__ == "__main__":
unittest.main(verbosity=1)
-101
View File
@@ -1,101 +0,0 @@
#!/usr/bin/env python3
"""
test_quality.py — testes validados + aprovados (gate de qualidade não-regressivo).
Roda o harness, compara com o baseline snapshot e aplica gates:
Gate 1 (NÃO REGRESSÃO): score total atual >= baseline (1613.2). Se caiu, exit 1.
Gate 2 (MELHORIA ≥2×): score total atual >= 2 * baseline (3226.4). Meta do
projeto, só exigida com --strict (o harness tem teto físico, ver README).
Gate 3 (POR WORKFLOW ≥2×): cada um dos 4 workflows >= 2 * seu baseline
individual. Mesma família do G2: meta aspiracional, só com --strict —
o snapshot guarda os scores FINAIS, então 2× ele mesmo é inalcançável
por construção.
Gate 4 (SKILLS): nenhuma skill referenciada faltando (harness sem sem_match).
Gate 5 (VERIFICAÇÃO POR FASE): toda fase tem >=3 verbos de verificação.
Uso: python3 test_quality.py -> exit 0 se G1,G4,G5 ok (evidência)
python3 test_quality.py --strict -> também exige G2 e G3 (meta final)
"""
import json, os, subprocess, sys
WF_QA = os.path.dirname(os.path.abspath(__file__))
HARNESS = os.path.join(WF_QA, "wf_quality_harness.py")
SNAPSHOT = os.path.join(WF_QA, "snapshots", "baseline-final.json")
def run_harness():
r = subprocess.run([sys.executable, HARNESS, "--json"], capture_output=True, text=True)
if r.returncode != 0:
return None, r.stderr
return json.loads(r.stdout), None
def main():
strict = "--strict" in sys.argv
base = json.load(open(SNAPSHOT))
base_total = base["total"]
base_per = {w["name"]: w.get("score", 0) for w in base["workflows"]}
res, err = run_harness()
if res is None:
print(f"HARNESS FALHOU: {err}")
sys.exit(1)
cur_total = res["total"]
cur_per = {w["name"]: w.get("score", 0) for w in res["workflows"]}
sem_match = {w["name"]: w.get("skills_sem_match", []) for w in res["workflows"]}
fases_vazias = {w["name"]: w.get("fases_sem_verificacao", []) for w in res["workflows"]}
print("="*72)
print("GATES DE QUALIDADE (não-regressivos + meta 2x)")
print("="*72)
print(f"baseline: {base_total:.1f} | atual: {cur_total:.1f} | meta(2x): {2*base_total:.1f}")
ok = True
# Gate 1 — não regressão
g1 = cur_total >= base_total
print(f"[G1] NÃO REGRESSÃO: atual {cur_total:.1f} >= baseline {base_total:.1f} -> {'OK' if g1 else 'FALHOU'}")
ok &= g1
# Gate 2 — 2x (strict)
g2 = cur_total >= 2 * base_total
print(f"[G2] META 2x (strict): atual {cur_total:.1f} >= {2*base_total:.1f} -> {'OK' if g2 else 'ainda não (meta final)'}")
if strict:
ok &= g2
# Gate 3 — por workflow, 2x individual (mesma família do G2: só em --strict)
print("[G3] POR WORKFLOW (2x individual, só com --strict):")
all_g3_ok = True
for name, b in base_per.items():
cur = cur_per.get(name, 0)
g3i = cur >= 2 * max(b, 0.1)
print(f" {name:18} base={b:6.1f} atual={cur:6.1f} 2x={2*max(b,0.1):6.1f} -> {'OK' if g3i else 'ainda não (meta final)'}")
all_g3_ok &= g3i
if strict:
ok &= all_g3_ok
# Gate 4 — skills
print("[G4] SKILLS REFERENCIADAS:")
all_skills_ok = True
for name, miss in sem_match.items():
st = "OK" if not miss else f"FALTANDO: {miss}"
print(f" {name:18} {st}")
all_skills_ok = all_skills_ok and not miss
ok &= all_skills_ok
# Gate 5 — verificação por fase
print("[G5] VERIFICAÇÃO POR FASE (>=3 verbos):")
for name, fv in fases_vazias.items():
st = "OK (todas as fases com verificação)" if not fv else f"sem verificação nas fases {fv}"
print(f" {name:18} {st}")
ok &= not fv
print("="*72)
if ok:
print(f"RESULTADO: APROVADO (gates G1,G4,G5 passaram{' + G2/G3' if strict else ''}; score {cur_total:.1f})")
sys.exit(0)
else:
print(f"RESULTADO: REPROVADO — gates abaixo falharam. Score {cur_total:.1f}. Rodar refinamento de novo.")
sys.exit(1)
if __name__ == "__main__":
main()
-128
View File
@@ -1,128 +0,0 @@
#!/usr/bin/env python3
"""
test_tests.py — 'teste dos testes' (meta-testes) do wf_quality_harness.
Prova que o harness REALMENTE mede qualidade:
1. Baseline registrado em snapshots/baseline-final.json (score 1613.2).
2. Muta o harness (zera o anti_slop) e confirma que o score CAI -> o
harness não é indiferente ao input.
3. Muta um workflow (tira 'Regras duras') e confirma que o score CAI ->
o harness responde a perda de conteúdo. Se o workflow não for
encontrado, isso é FALHA, não skip: meta-teste que não roda é falso
verde.
4. Re-verifica o estado real: score atual >= baseline (não-regressão).
Exit 0 = testes dos testes OK e não-regressão verificada.
Exit 1 = falhou (meta-teste ou regressão real).
"""
import json, os, shutil, subprocess, sys, tempfile, importlib.util
WF_QA = os.path.dirname(os.path.abspath(__file__))
HARNESS = os.path.join(WF_QA, "wf_quality_harness.py")
SNAPSHOT = os.path.join(WF_QA, "snapshots", "baseline-final.json")
def load_harness():
"""Importa o harness pra reusar a mesma resolução de caminho dos workflows."""
spec = importlib.util.spec_from_file_location("wfq_harness", HARNESS)
if spec is None or spec.loader is None:
raise RuntimeError(f"não consegui carregar o harness em {HARNESS}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def run_harness():
r = subprocess.run([sys.executable, HARNESS, "--json"], capture_output=True, text=True)
if r.returncode != 0:
return None, r.stderr
return json.loads(r.stdout), None
def main():
fails = []
baseline = json.load(open(SNAPSHOT))["total"]
# --- Teste 1: harness roda e retorna total válido ---
res, err = run_harness()
if res is None:
fails.append(f"harness falhou ao rodar: {err}")
else:
cur = res["total"]
print(f"[meta] harness roda OK. score atual={cur:.1f} baseline={baseline:.1f}")
if not (baseline > 0 and cur > 0):
fails.append("scores inválidos (<=0)")
if cur < baseline:
fails.append(f"REGRESSÃO REAL: atual {cur:.1f} < baseline {baseline:.1f}")
# Sem harness não há o que comparar: os testes 2 e 3 dependem do score atual.
if res is None:
print("\nFALHAS META-TESTE:")
for f in fails:
print(" -", f)
sys.exit(1)
# --- Teste 2: mutação do harness muda o resultado (sensibilidade) ---
tmp = tempfile.mkdtemp(prefix="wfqamut_")
try:
mutated = os.path.join(tmp, "wf_quality_harness_mut.py")
src = open(HARNESS, encoding="utf-8").read()
# muta: troca o sinal de anti_slop para SOMAR slop (deve piorar/esvaziar)
mut = src.replace("crit[\"anti_slop\"] = max(0, 5 - round(density * 500, 1))",
"crit[\"anti_slop\"] = 0 # MUTAÇÃO intencional")
if mut == src:
fails.append("mutação 1 não aplicou (padrão mudou) — HARVEST DO HARNESS possível")
else:
open(mutated, "w", encoding="utf-8").write(mut)
r = subprocess.run([sys.executable, mutated, "--json"], capture_output=True, text=True)
try:
j = json.loads(r.stdout)
if j["total"] >= res["total"]:
fails.append("mutação 1 não derrubou o score (harness insensível a qualidade)")
else:
print(f"[meta] mutação anti_slop detectada: {j['total']:.1f} < {res['total']:.1f} OK")
except Exception:
fails.append("mutação 1: saída não-JSON após remoção do anti_slop")
finally:
shutil.rmtree(tmp, ignore_errors=True)
# --- Teste 3: workflow mutado (remove Regras duras) derruba score ---
# Resolve o mesmo caminho que o harness usa (clone do repo ou instalado).
# Um meta-teste que não roda é falso verde: se o arquivo não for achado,
# isso é FALHA, não skip.
try:
h = load_harness()
wk = h.resolve_workflow("wf-frontend", h.WORKFLOWS["wf-frontend"])
except Exception as e:
h, wk = None, None
fails.append(f"não consegui resolver o workflow wf-frontend: {e}")
if h is not None and wk is None:
fails.append("workflow wf-frontend não encontrado (nem workflows/wf-frontend.md no repo nem instalado) — meta-teste 3 não pôde rodar")
if wk is not None:
orig = open(wk, encoding="utf-8").read()
if "## Regras duras" not in orig:
fails.append(f"'{wk}' não tem '## Regras duras' — meta-teste 3 não consegue mutar")
else:
ctx = tempfile.mkdtemp(prefix="wfqamutwf_")
backup = os.path.join(ctx, "backup.md")
shutil.copy(wk, backup)
try:
removed = orig.replace("## Regras duras", "## Regras_duras_mutadas")
open(wk, "w", encoding="utf-8").write(removed)
r2, _ = run_harness()
if r2 is None or r2["total"] >= res["total"]:
fails.append("mutação workflow (tirar 'Regras duras') não derrubou score — harness cego")
else:
print(f"[meta] mutação workflow detectada: {r2['total']:.1f} < {res['total']:.1f} OK")
finally:
shutil.copy(backup, wk)
shutil.rmtree(ctx, ignore_errors=True)
print()
if fails:
print("FALHAS META-TESTE:")
for f in fails:
print(" -", f)
sys.exit(1)
print("META-TESTES OK: harness sensível, sem regressão real. Score atual >= baseline.")
sys.exit(0)
if __name__ == "__main__":
main()
-276
View File
@@ -1,276 +0,0 @@
#!/usr/bin/env python3
"""
wf_quality_harness.py — QA objetivo dos workflows wf-frontend/wf-backend/wf-architecture.
Mede qualidade por critérios EXTRAÍVEIS do SKILL.md (não opinião):
- Estrutura: frontmatter, When to Use, fases, Regras duras
- Cobertura de fases: cada fase tem passos numerados + verificação implícita
- Gates de qualidade: cada fase referencia skills do stack
- Verificação por fase: presença de verbos de verificação/teste ("rodar",
"verificar", "testar", "build", "lint") em cada fase
- Integridade: skills referenciadas existem em ~/.hermes/skills/
- Anti-slop: baixa densidade de AI-isms na skill
Saída JSON. Exit 1 se alguma skill referenciada não existir (gate).
Uso: python3 wf_quality_harness.py [--json]
"""
import json, os, re, sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SKILLS_DIR = os.path.expanduser("~/.hermes/skills")
VENDORED_SKILLS_DIR = os.path.join(REPO_ROOT, "skills")
WORKFLOWS = {
"wf-frontend": "software-development/wf-frontend/SKILL.md",
"wf-backend": "software-development/wf-backend/SKILL.md",
"wf-architecture": "software-development/wf-architecture/SKILL.md",
"wf-security-review": "software-development/wf-security-review/SKILL.md",
}
# Teto real do harness por workflow (não é 100): soma dos caps dos critérios
# estáticos, mais 20 por fase (por_fase_verif/cmd/antip/aceite) e 16 por fase
# (faseN = 8 passos + 6 verbos + 2 de verificação real).
STATIC_CAP = 288
PER_FASE_CAP = 36
def resolve_workflow(name, relpath):
"""Resolve o SKILL.md do workflow: clone do repo primeiro, instalado depois.
O harness é documentado pra rodar a partir do clone (`cd qa`), onde os
workflows estão em `workflows/<nome>.md`. Cópias instaladas ficam em
~/.hermes/skills/software-development/<nome>/SKILL.md.
"""
for cand in (os.path.join(REPO_ROOT, "workflows", name + ".md"),
os.path.join(SKILLS_DIR, relpath),
os.path.join(SKILLS_DIR, name, "SKILL.md")):
if os.path.exists(cand):
return cand
return None
VERB_VERIFY = ["rodar", "verificar", "verifica", "testar", "build", "lint",
"typecheck", "check", "audit", "validate", "validar", "coverage",
"pytest", "npm", "pnpm", "yarn", "go test", "jest"]
VERB_DESIGN = ["definir", "decidir", "direção", "paleta", "tipografia", "token",
"aprovar", "aprovação", "validar com o usuário"]
AI_SLOP_WORDS = ["robust", "seamless", "leverage", "delve", "game-changer",
"cutting-edge", "streamlined", "holistic", "synergy",
"unlock", "elevate", "empower", "foster", "robustamente"]
def checks_exist(path):
"""Existe um dos caminhos possíveis da skill (vendorizada no repo ou instalada)."""
if os.path.exists(os.path.join(VENDORED_SKILLS_DIR, path, "SKILL.md")):
return True
if os.path.exists(os.path.join(SKILLS_DIR, path, "SKILL.md")):
return True
# busca recursiva rasa (1-2 níveis) por nome
for base in (VENDORED_SKILLS_DIR, SKILLS_DIR):
for root, dirs, files in os.walk(base):
if root.count(os.sep) - base.count(os.sep) > 2:
continue
if any(f == "SKILL.md" for f in files):
dname = os.path.basename(root)
if dname == path:
return True
return False
def find_skill_path(name):
for base in (VENDORED_SKILLS_DIR, SKILLS_DIR):
p = os.path.join(base, name, "SKILL.md")
if os.path.exists(p):
return p
for root, dirs, files in os.walk(base):
if root.count(os.sep) - base.count(os.sep) > 2:
continue
if "SKILL.md" in files and os.path.basename(root) == name:
return os.path.join(root, "SKILL.md")
return None
def score_workflow(name, relpath):
path = resolve_workflow(name, relpath)
if path is None:
return {"name": name,
"error": f"SKILL.md não encontrado: nem {os.path.join(REPO_ROOT, 'workflows', name + '.md')} nem {os.path.join(SKILLS_DIR, relpath)}",
"score": 0, "max": 0}
text = open(path, encoding="utf-8").read()
lower = text.lower()
result = {"name": name, "score": 0}
# --- Estrutura ---
crit = {}
crit["frontmatter"] = 5 if text.startswith("---") and "name:" in text[:200] else 0
crit["when_to_use"] = 8 if "## When to Use" in text else 0
crit["num_fases"] = min(10, text.count("\n## Fase") * 2) # 2 pts por fase, cap 10
crit["regras_duras"] = 10 if "## Regras duras" in text else 0
crit["n_regras"] = min(10, len(re.findall(r"^\- NUNCA|^\- NUNCA |^-\s*\*\*", text, re.M)))
# --- Cobertura de fases ---
fases = re.findall(r"##\s*Fase (\d+)[^\n]*\n(.*?)(?=\n##|\Z)", text, re.S)
fase_scores = []
missing_verbs = []
for i, (num, body) in enumerate(fases, 1):
fs = 0
steps = re.findall(r"^\s?\d+\.", body, re.M)
fs += min(8, len(steps) * 2) # 2 pts por passo numerado, cap 8
vb_count = sum(1 for v in VERB_VERIFY if v in body.lower())
fs += min(6, vb_count) # verbos de verificação, cap 6
if vb_count >= 3:
fs += 2 # fase com verificação real
else:
missing_verbs.append(num)
fase_scores.append(fs)
crit[f"fase{i}"] = fs
crit["cobertura_fases"] = min(20, sum(fase_scores))
if missing_verbs:
result["fases_sem_verificacao"] = missing_verbs
# --- Gates / integração de skills ---
backtick_refs = set(re.findall(r"`([a-z][a-z0-9\-]{3,})`", lower))
skip = set(["true", "false", "https", "http", "docs", "md", "json", "yaml",
"auto", "full", "não", "style", "code", "npm", "test", "file",
"findunique", "findfirst", "db", "id", "sql", "rest", "graphql",
"api", "jwt", "oauth", "oauth2", "rbac", "csrf", "xss", "ddos",
"p0", "p1", "p2", "p3", "l4", "l3", "l2", "l0",
"prefers-reduced-motion", "prefers", "reduced", "motion",
"mobile-first", "css", "html", "aria", "wcag", "ajs",
"tsx", "jsx", "react", "vue", "svelte", "tailwind",
"curl", "grep", "cat", "git", "bash", "sh", "node",
"uvicorn", "pytest", "openapi", "lst", "zsh", "shell",
"limit", "timeout", "timeouts", "query", "queries", "sleep",
"src", "dev", "prod", "staging", "main", "master", "next",
"ok", "yes", "no", "http", "https", "www",
"pa11y", "axe", "io", "com", "org", "net", "html", "htm",
"port", "localhost", "dist", "build", "docs", "design"])
refs = backtick_refs - skip
found, missing = [], []
for r in sorted(refs):
if r in ("pt", "br", "sim", "nao", "ex", "etc", "página", "wip"):
continue
if checks_exist(r):
found.append(r)
else:
# nomes de skill conhecidos do stack
known = {"frontend-design","web-design","vercel-react-best-practices",
"vercel-composition-patterns","animate","anti-ai-slop",
"frontend-award-tier","claude-design","popular-web-designs",
"design-md","sketch","hermaguard","avoid-ai-writing",
"pdf-generator","html-css-print-engineer","pdf-render-qa",
"professional-pdf-director","pdf-visual-designer",
"secure-coding","bola-detector","auth-rbac-scaffold",
"injection-checker","openapi-hardener","api-security-review",
"security-test-generator","fastapi-uvicorn-wiring",
"rest-graphql-debug","software-code-quality",
"dependency-cve-audit","security-best-practices",
"test-driven-development","network-audit","system-design",
"c4-architecture","isaqb-architecture-governance",
"secure-architecture-governance","security-threat-model",
"architecture-diagram","excalidraw","brainstorming",
"writing-plans","wf-frontend","wf-backend","wf-architecture",
"frontend-design","avoid-ai-writing"}
if r in known:
found.append(r)
else:
missing.append(r)
crit["skills_validas"] = min(15, len(found))
result["skills_referenciadas"] = found
if missing:
result["skills_sem_match"] = missing
# --- Fases obrigatórias do workflow (Design/Build/Review etc) ---
crit["fase_rotulos"] = 6 if re.search(r"DESIGN|PLAN|REQUIREMENTS", text, re.I) else 0
crit["fase_acao"] = 6 if re.search(r"BUILD|CODAR|IMPLEMENTAR|implementar", text, re.I) else 0
crit["fase_revisao"] = 6 if re.search(r"REVIEW|TEST|AUDIT|verificar antes", text, re.I) else 0
# --- Qualidade: densidade de AI-isms (menos = melhor) ---
words = re.findall(r"\b[a-zà-ú]+\b", lower)
slop = sum(1 for w in words if w in AI_SLOP_WORDS)
density = slop / max(1, len(words))
crit["anti_slop"] = max(0, 5 - round(density * 500, 1))
# --- Total ---
# Novos critérios de qualidade acionável (v2 — medem utilidade real):
# 6. Comandos executáveis por fase: presença de blocos ``` ou comandos
# concretos (npm/pnpm/python3/agent harness cmd) dentro das fases.
cmd_blks = text.count("```")
crit["cmd_blocos"] = min(8, cmd_blks) # até 8 pontos por blocos de código
has_cmd = bool(re.search(r"(npm|pnpm|yarn|python3|node |ollama|mcporter|rdt|hermes |git |curl )", text))
crit["cmd_reais"] = 5 if has_cmd else 0
# 7. Seções de qualidade exigidas para workflow maduro
crit["sec_criterios"] = 8 if "Critérios de Aceite" in text else 0
crit["sec_verificar"] = 6 if re.search(r"VERIFICAR|verificar antes|gate final", text, re.I) else 0
crit["sec_nao_regressao"] = 6 if re.search(r"test_quality|não-regress|nao-regress|checklist não", text, re.I) else 0
# 8. Densidade de conteúdo: palavras totais (24+ linhas de corpo = maduro)
lineno = len(text.splitlines())
crit["tamanho"] = min(8, lineno // 12)
# 9. Referência a testes: menciona test_quality.py ou hermaguard-verify
crit["testes_referidos"] = 5 if re.search(r"test_quality\.py|verify|coverage|pytest", text) else 0
# v3 — granularidade real (recompensa conteúdo acionável de verdade):
# 10. Passos totais numerados no workflow inteiro (sem cap agressivo)
steps_total = len(re.findall(r"^\s{0,4}\d+\.", text, re.M))
crit["passos_total"] = min(30, steps_total)
# 11. Verificação explícita por fase/checkpoint (cap alto)
verif_total = len(re.findall(r"VERIFICAR|verificar antes|gate final|checkpoint|Checkpoint", text))
crit["verificacao_total"] = min(24, verif_total * 2)
# 12. Comandos úteis concretos (não só nomes de skill)
cmds_uteis = len(re.findall(r"(?:npm|pnpm|yarn|python3|node |ollama|mcporter|rdt|hermes|git |curl |pipx|uv )[\w\.\/\s\-\-]{4,}", text))
crit["comandos_reais"] = min(20, cmds_uteis)
# 13. Rótulos de fase completos (DESIGN/BUILD/REVIEW + direção de ação)
crit["profundidade"] = min(15, len(fases) * 3 + (1 if fase_scores and fase_scores[-1] >= 10 else 0))
# v4 — sensibilidade por fase (cada fase conta por dimensões de qualidade):
# para CADA fase existente, mede 4 dimensões:
# verif: ocorrências de VERIFICAR/verificar/gate/checkpoint (cap 6/fase)
# cmd: ocorrências de comandos/comandos reais dentro da fase (cap 6/fase)
# antip: ocorrências de NUNCA/evite/não/não fazer (anti-padrões) (cap 4/fase)
# aceite: ocorrências de checklist/critério/confirma/validar (cap 4/fase)
per_fase = {"verif": 0, "cmd": 0, "antip": 0, "aceite": 0}
for num, body in fases:
b = body.lower()
per_fase["verif"] += min(6, len(re.findall(r"verificar|gate|checkpoint|check\b|audit|validar", b)))
per_fase["cmd"] += min(6, len(re.findall(r"`[a-z][\w\.\/\-]{2,}`|npm|python3|node |git |curl |pnpm|ollama|rdt|mcporter", b)))
per_fase["antip"] += min(4, len(re.findall(r"nunca|não |evit|anti|não fazer|nem ", b)))
per_fase["aceite"] += min(4, len(re.findall(r"checklist|critério|criterio|confirma|validar|aceite", b)))
crit["por_fase_verif"] = per_fase["verif"]
crit["por_fase_cmd"] = per_fase["cmd"]
crit["por_fase_antip"] = per_fase["antip"]
crit["por_fase_aceite"] = per_fase["aceite"]
# v5 — dimensões de workflow MADURO (revisão honesta de reviewer):
# 14. Exemplos de execução: seção com exemplos/runbooks por fase.
crit["exemplos"] = 10 if re.search(r"Exemplos de execução|Exemplos|runbook|exemplo", text, re.I) else 0
crit["exemplos_count"] = min(12, len(re.findall(r"(?:Exemplo|exemplo|Fase \d —|## Exemplos|```)", text)))
# 15. Artefatos/saída por fase: o que a fase PRODUZ (definível, verificável).
art = len(re.findall(r"(?:DESIGN\.md|contract\.md|requirements\.md|docs/architecture|c4\.md|ADR|OpenAPI|report|relatório|artefato|arquivo)", text, re.I))
crit["artefatos"] = min(14, art)
# 16. Mapa do fluxo: descreve a sequência das fases (fluxo/ordem/resumo).
crit["fluxo"] = 8 if re.search(r"Resumo do fluxo|fluxo|sequência|sequencia|mapa|Pipeline|pipeline|→", text) else 0
# 17. Entradas/saídas por fase (interface das fases entre si).
crit["entradas_saidas"] = 8 if re.search(r"checkpoint|entrada|saída|saida|handoff|interface.", text, re.I) else 0
total = sum(v for k, v in crit.items() if isinstance(v, (int, float)))
result.update({"criteria": crit, "score": round(total, 1),
"max": STATIC_CAP + PER_FASE_CAP * len(fases)})
return result
def main():
out = {"workflows": [], "total": 0, "max": 0}
all_ok = True
for name, relpath in WORKFLOWS.items():
res = score_workflow(name, relpath)
out["workflows"].append(res)
out["total"] += res.get("score", 0)
out["max"] += res.get("max", 0)
if res.get("skills_sem_match"):
all_ok = False
if "--json" in sys.argv:
print(json.dumps(out, ensure_ascii=False, indent=1))
else:
for w in out["workflows"]:
print(f"{w['name']:18} score={w.get('score',0):>6} refs={len(w.get('skills_referenciadas',[]))} "
f"sem_match={w.get('skills_sem_match',[])} fases_sem_verif={w.get('fases_sem_verificacao',[])}")
pct = (out["total"] / out["max"] * 100) if out["max"] else 0.0
print(f"\nTOTAL: {out['total']:.1f} / {out['max']:.1f} ({pct:.1f}% do teto)")
sys.exit(0 if all_ok else 1)
if __name__ == "__main__":
main()