From eba8bb1d944a4d8719b24f66fe8579679e3fa313 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Mon, 14 Sep 2026 07:40:14 -0300 Subject: [PATCH] fix(qa): make the quality harness work from a fresh clone The harness resolved workflows and referenced skills only under ~/.hermes/skills/, so the commands documented in the README ("Quick start") and in CONTRIBUTING ("Local checks before a PR") produced a meaningless result on a fresh clone: every workflow scored 0, the gates reported REPROVADO and the meta-tests failed. - wf_quality_harness.py: resolve each workflow from the repo (workflows/.md) first, then from the installed skills dir; resolve referenced skills from the vendored skills/ tree as well. Report the real per-workflow cap instead of a fixed 100 (scores were shown as >100%). - test_quality.py: G3 compared the current score against 2x its own final value, so it could never pass and made the documented command exit 1. Moved G3 into the --strict family alongside G2, and updated the stale docstring (364.1 / 3 workflows -> 1613.2 / 4). - test_tests.py: meta-test 3 pointed at a path that does not exist in the repo, so it was skipped in silence (false green). It now resolves the same path the harness uses and FAILS when the workflow is missing; also guards the None harness result that previously crashed test 2. Verified from the clone: harness exit 0, score 1613.2 (matches the committed snapshot exactly on CPython 3.10 and 3.11); test_quality exit 0 (APROVADO G1,G4,G5); test_tests exit 0 with both mutations detected. --- qa/test_quality.py | 27 ++++++++------ qa/test_tests.py | 75 ++++++++++++++++++++++++++------------ qa/wf_quality_harness.py | 77 +++++++++++++++++++++++++++------------- 3 files changed, 123 insertions(+), 56 deletions(-) diff --git a/qa/test_quality.py b/qa/test_quality.py index 29bc7bd..6f22681 100644 --- a/qa/test_quality.py +++ b/qa/test_quality.py @@ -4,14 +4,18 @@ test_quality.py — testes validados + aprovados (gate de qualidade não-regress Roda o harness, compara com o baseline snapshot e aplica gates: -Gate 1 (NÃO REGRESSÃO): score total atual >= baseline (364.1). Se caiu, exit 1. -Gate 2 (MELHORIA ≥2×): score total atual >= 2 * baseline (728.2). Meta do usuário. -Gate 3 (POR WORKFLOW): cada um dos 3 workflows >= 2 * seu baseline individual. +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,G3,G4,G5 ok (evidência) - python3 test_quality.py --strict -> também exige G2 (2x) — meta final +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 @@ -58,13 +62,16 @@ def main(): if strict: ok &= g2 - # Gate 3 — por workflow, 2x individual - print("[G3] POR WORKFLOW (2x individual):") + # 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 'FALHOU'}") - ok &= g3i + 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:") @@ -84,7 +91,7 @@ def main(): print("="*72) if ok: - print(f"RESULTADO: APROVADO (gates G1,G3,G4,G5 passaram{' + G2' if strict and g2 else ''}; score {cur_total:.1f})") + 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.") diff --git a/qa/test_tests.py b/qa/test_tests.py index cd0e9be..2a0ebcc 100644 --- a/qa/test_tests.py +++ b/qa/test_tests.py @@ -3,22 +3,33 @@ 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-364.json (score 364.1 total). - 2. Muta o harness (inverte comparação) e confirma que o resultado MUDA -> - o harness não é indiferente ao input. - 3. Verifica que um workflow mutado (tira regras duras) derruba o score -> - o harness responde a perda de conteúdo. + 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 +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: @@ -41,6 +52,13 @@ def main(): 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: @@ -66,23 +84,36 @@ def main(): shutil.rmtree(tmp, ignore_errors=True) # --- Teste 3: workflow mutado (remove Regras duras) derruba score --- - wk = os.path.join(os.path.dirname(WF_QA), "wf-frontend", "SKILL.md") - if os.path.exists(wk): + # 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() - 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) + 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: diff --git a/qa/wf_quality_harness.py b/qa/wf_quality_harness.py index 6b9f107..3e99a0f 100644 --- a/qa/wf_quality_harness.py +++ b/qa/wf_quality_harness.py @@ -16,7 +16,10 @@ 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", @@ -24,6 +27,26 @@ WORKFLOWS = { "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/.md`. Cópias instaladas ficam em + ~/.hermes/skills/software-development//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"] @@ -34,36 +57,40 @@ AI_SLOP_WORDS = ["robust", "seamless", "leverage", "delve", "game-changer", "unlock", "elevate", "empower", "foster", "robustamente"] def checks_exist(path): - """Existe um dos caminhos possíveis da skill (categoria ou raiz).""" - for base in (SKILLS_DIR,): - if os.path.exists(os.path.join(base, path, "SKILL.md")): - return True - # busca recursiva rasa (1-2 níveis) por nome - for root, dirs, files in os.walk(SKILLS_DIR): - if root.count(os.sep) - SKILLS_DIR.count(os.sep) > 2: - continue - if any(f == "SKILL.md" for f in files): - dname = os.path.basename(root) - if dname == path: - return True + """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 (SKILLS_DIR,): + 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(SKILLS_DIR): - if root.count(os.sep) - SKILLS_DIR.count(os.sep) > 2: - continue - if "SKILL.md" in files and os.path.basename(root) == name: - return os.path.join(root, "SKILL.md") + 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 = os.path.join(SKILLS_DIR, relpath) - if not os.path.exists(path): - return {"name": name, "error": f"SKILL.md não encontrado: {path}", "score": 0} + 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} @@ -221,7 +248,8 @@ def score_workflow(name, relpath): 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)}) + result.update({"criteria": crit, "score": round(total, 1), + "max": STATIC_CAP + PER_FASE_CAP * len(fases)}) return result def main(): @@ -231,7 +259,7 @@ def main(): res = score_workflow(name, relpath) out["workflows"].append(res) out["total"] += res.get("score", 0) - out["max"] += 100 + out["max"] += res.get("max", 0) if res.get("skills_sem_match"): all_ok = False if "--json" in sys.argv: @@ -240,7 +268,8 @@ def main(): 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',[])}") - print(f"\nTOTAL: {out['total']:.1f} / {out['max']:.1f} ({out['total']/out['max']*100:.1f}%)") + 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__":