@andresmassello/uscha 1.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/bin/README.md +6 -0
  4. package/bin/uscha.js +28 -0
  5. package/package.json +38 -0
  6. package/uscha-kit/.claude/skills/uscha-adr-refine/SKILL.md +161 -0
  7. package/uscha-kit/.claude/skills/uscha-characterize/SKILL.md +124 -0
  8. package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +490 -0
  9. package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +5602 -0
  10. package/uscha-kit/.claude/skills/uscha-discovery/SKILL.md +161 -0
  11. package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +145 -0
  12. package/uscha-kit/.claude/skills/uscha-mirador/mirador-render.py +112 -0
  13. package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.ps1 +22 -0
  14. package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.sh +26 -0
  15. package/uscha-kit/.claude/skills/uscha-mirador/mirador.template.html +586 -0
  16. package/uscha-kit/.claude/skills/uscha-mirador/telemetry-extract.py +130 -0
  17. package/uscha-kit/.claude/skills/uscha-reverse-discovery/SKILL.md +116 -0
  18. package/uscha-kit/.claude/skills/uscha-rubric/SKILL.md +79 -0
  19. package/uscha-kit/.claude/skills/uscha-sysdoc/SKILL.md +88 -0
  20. package/uscha-kit/.claude-plugin/plugin.json +24 -0
  21. package/uscha-kit/.codex-plugin/plugin.json +37 -0
  22. package/uscha-kit/CHANGELOG-1.10.0.md +84 -0
  23. package/uscha-kit/CHANGELOG-1.11.0.md +67 -0
  24. package/uscha-kit/CHANGELOG-1.12.0.md +46 -0
  25. package/uscha-kit/CHANGELOG-1.13.0.md +33 -0
  26. package/uscha-kit/CHANGELOG-1.14.0.md +42 -0
  27. package/uscha-kit/CHANGELOG-1.15.0.md +58 -0
  28. package/uscha-kit/CHANGELOG-1.16.0.md +55 -0
  29. package/uscha-kit/CHANGELOG-1.17.0.md +44 -0
  30. package/uscha-kit/CHANGELOG-1.18.0.md +42 -0
  31. package/uscha-kit/CHANGELOG-1.19.0.md +41 -0
  32. package/uscha-kit/CHANGELOG-1.2.2.md +16 -0
  33. package/uscha-kit/CHANGELOG-1.2.3.md +20 -0
  34. package/uscha-kit/CHANGELOG-1.2.4.md +10 -0
  35. package/uscha-kit/CHANGELOG-1.2.5.md +23 -0
  36. package/uscha-kit/CHANGELOG-1.2.6.md +11 -0
  37. package/uscha-kit/CHANGELOG-1.2.7.md +15 -0
  38. package/uscha-kit/CHANGELOG-1.2.8.md +24 -0
  39. package/uscha-kit/CHANGELOG-1.2.9.md +4 -0
  40. package/uscha-kit/CHANGELOG-1.20.0.md +29 -0
  41. package/uscha-kit/CHANGELOG-1.21.0.md +33 -0
  42. package/uscha-kit/CHANGELOG-1.22.0.md +60 -0
  43. package/uscha-kit/CHANGELOG-1.23.0.md +75 -0
  44. package/uscha-kit/CHANGELOG-1.24.0.md +50 -0
  45. package/uscha-kit/CHANGELOG-1.25.0.md +55 -0
  46. package/uscha-kit/CHANGELOG-1.26.0.md +70 -0
  47. package/uscha-kit/CHANGELOG-1.27.0.md +45 -0
  48. package/uscha-kit/CHANGELOG-1.28.0.md +35 -0
  49. package/uscha-kit/CHANGELOG-1.29.0.md +20 -0
  50. package/uscha-kit/CHANGELOG-1.3.0.md +74 -0
  51. package/uscha-kit/CHANGELOG-1.30.0.md +46 -0
  52. package/uscha-kit/CHANGELOG-1.31.0.md +59 -0
  53. package/uscha-kit/CHANGELOG-1.32.0.md +50 -0
  54. package/uscha-kit/CHANGELOG-1.33.0.md +46 -0
  55. package/uscha-kit/CHANGELOG-1.34.0.md +55 -0
  56. package/uscha-kit/CHANGELOG-1.35.0.md +30 -0
  57. package/uscha-kit/CHANGELOG-1.36.0.md +33 -0
  58. package/uscha-kit/CHANGELOG-1.37.0.md +41 -0
  59. package/uscha-kit/CHANGELOG-1.38.0.md +11 -0
  60. package/uscha-kit/CHANGELOG-1.39.0.md +14 -0
  61. package/uscha-kit/CHANGELOG-1.4.0.md +68 -0
  62. package/uscha-kit/CHANGELOG-1.40.0.md +16 -0
  63. package/uscha-kit/CHANGELOG-1.40.1.md +11 -0
  64. package/uscha-kit/CHANGELOG-1.5.0.md +64 -0
  65. package/uscha-kit/CHANGELOG-1.6.0.md +57 -0
  66. package/uscha-kit/CHANGELOG-1.7.0.md +74 -0
  67. package/uscha-kit/CHANGELOG-1.8.0.md +46 -0
  68. package/uscha-kit/CHANGELOG-1.9.0.md +112 -0
  69. package/uscha-kit/LICENSE +21 -0
  70. package/uscha-kit/README.md +497 -0
  71. package/uscha-kit/VERSION +1 -0
  72. package/uscha-kit/WORKBENCH.md +178 -0
  73. package/uscha-kit/hooks/block-approved-writes.ps1 +46 -0
  74. package/uscha-kit/hooks/hooks.json +15 -0
  75. package/uscha-kit/install-uscha.py +344 -0
  76. package/uscha-kit/skills/uscha-adr-refine/SKILL.md +161 -0
  77. package/uscha-kit/skills/uscha-characterize/SKILL.md +124 -0
  78. package/uscha-kit/skills/uscha-devloop/SKILL.md +490 -0
  79. package/uscha-kit/skills/uscha-devloop/qa_ledger.py +5602 -0
  80. package/uscha-kit/skills/uscha-discovery/SKILL.md +161 -0
  81. package/uscha-kit/skills/uscha-mirador/SKILL.md +145 -0
  82. package/uscha-kit/skills/uscha-mirador/mirador-render.py +112 -0
  83. package/uscha-kit/skills/uscha-mirador/mirador-watch.ps1 +22 -0
  84. package/uscha-kit/skills/uscha-mirador/mirador-watch.sh +26 -0
  85. package/uscha-kit/skills/uscha-mirador/mirador.template.html +586 -0
  86. package/uscha-kit/skills/uscha-mirador/telemetry-extract.py +130 -0
  87. package/uscha-kit/skills/uscha-reverse-discovery/SKILL.md +116 -0
  88. package/uscha-kit/skills/uscha-rubric/SKILL.md +79 -0
  89. package/uscha-kit/skills/uscha-sysdoc/SKILL.md +88 -0
  90. package/uscha-kit/templates/.gitattributes +6 -0
  91. package/uscha-kit/templates/CLAUDE.md +56 -0
  92. package/uscha-kit/templates/CONSTITUTION.md +149 -0
  93. package/uscha-kit/templates/RUBRIC.md +38 -0
  94. package/uscha-kit/templates/docs/adr/README.md +19 -0
  95. package/uscha-kit/templates/rubric-grader-prompt.md +63 -0
  96. package/uscha-kit/tests/smoke-engine.sh +1739 -0
  97. package/uscha-kit/uscha.config.json +181 -0
  98. package/uscha-kit/workbench-doctor.sh +45 -0
@@ -0,0 +1,1739 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-engine.sh — suite de smoke del motor qa_ledger.py contra un ledger SINTÉTICO.
3
+ # Valida el cableado de los fact gates (v1.3.0): log-gate, flag-blocker,
4
+ # resolve-escalation, UNMEASURED, convergencia per-tool, gate-check, golden-diff,
5
+ # spec-check estructural, simplicity floor, oscillation — el adapter python
6
+ # (v1.4.0): coverage Cobertura, junit envuelto, LOC, ruff/mypy, UNMEASURED python —
7
+ # el adapter node (v1.5.0): lcov, jest-junit, LOC ts, eslint/tsc, UNMEASURED node —
8
+ # el adapter go (v1.6.0): cover profile, gotestsum, LOC _test.go, golangci —
9
+ # los adapters rust/dotnet (v1.7.0): Cobertura reusada, clippy JSONL, SARIF —
10
+ # el adapter cpp (v1.8.0): gcovr Cobertura, ctest junit plano, clang-tidy —
11
+ # y los adapters gradle/swift (v1.9.0): JaCoCo/lcov/junit/checkstyle reusados
12
+ # en paths nuevos (detekt, swiftlint) + fixes de los reviews 1.7.0/1.8.0 —
13
+ # y acceptance trazable (v1.10.0): AC-n cierra por testcase medido, spec-check
14
+ # --acceptance como FACT estructural —
15
+ # y tests fuera del presupuesto de simplicity (v1.11.0): escribir tests no
16
+ # penaliza el gate (Topic 51) + edges 1.10.0 (falsos positivos, flaky) —
17
+ # y secret-scan en gate-check (v1.12.0): secretos agregados bloquean como hecho —
18
+ # y ledger atomico (v1.13.0): checksum de integridad + carga blindada —
19
+ # y plateau/stop-signal (v1.14.0): stall y candidato-a-PR como advisories —
20
+ # y golden scrub (v1.15.0): volatiles declarados enmascaran con masking visible —
21
+ # y regression-capture (v1.16.0): cierre sin test = narrado; escape-analysis
22
+ # obligatoria al resolver blockers — y procedencia de umbrales (v1.17.0):
23
+ # requerimiento (config) vs default del kit, etiquetado en cada gate —
24
+ # y phase (v1.18.0): FSM derivada del ledger, el estado se computa —
25
+ # y spikes (v1.19.0): rama spike/* jamas pasa el gate de PR —
26
+ # y doctor (v1.22.0): diagnostico de la instalacion, ledger corrupto = error —
27
+ # y rubric layer (v1.23.0): criterio cualitativo versionado, agnostico (T43 a mano).
28
+ #
29
+ # Uso: bash tests/smoke-engine.sh (desde la raíz del kit)
30
+ # Exit: 0 = todos los checks verdes · 1 = algún check falló
31
+ #
32
+ # Nota INV-GOLDEN-01: el path CLEAN de golden-diff NO se auto-testea — crear un
33
+ # fixture aprobado es un acto HUMANO incluso en tests. Se cubren NOT-RUN y DIVERGE.
34
+
35
+ set -u
36
+ # stdin SIEMPRE cerrado: spec-check (y cualquier subcomando futuro) lee stdin
37
+ # cuando viene redirigido — con un pipe abierto sin EOF la suite se cuelga
38
+ # infinito (paso en la vida real: exit 124 por timeout con commit encadenado).
39
+ exec < /dev/null
40
+ KIT="$(cd "$(dirname "$0")/.." && pwd)"
41
+ ROOT="$(cd "$KIT/.." && pwd)"
42
+ QL="$KIT/.claude/skills/uscha-devloop/qa_ledger.py"
43
+ # probe FUNCIONAL: en Windows 'python3' puede ser un stub de Store que está en
44
+ # PATH pero no ejecuta — hay que probar --version, no solo command -v.
45
+ PY="${PYTHON:-}"
46
+ if [ -z "$PY" ]; then
47
+ for cand in python3 python py; do
48
+ if "$cand" --version >/dev/null 2>&1; then PY="$cand"; break; fi
49
+ done
50
+ fi
51
+ [ -n "$PY" ] || { echo "FAIL: no hay Python funcional en PATH"; exit 1; }
52
+ SB="$(mktemp -d 2>/dev/null || echo "${TMP:-/tmp}/smoke-$$")"; mkdir -p "$SB/repo-a" "$SB/repo-b" "$SB/repo-c" "$SB/repo-d" "$SB/repo-e" "$SB/repo-f" "$SB/repo-g" "$SB/repo-h" "$SB/repo-i" "$SB/repo-j"
53
+ cd "$SB"
54
+
55
+ PASS=0; FAIL=0
56
+ chk() { # $1 = descripción, $2 = exit esperado, $3.. = comando
57
+ local desc="$1" want="$2"; shift 2
58
+ "$@" >/dev/null 2>&1; local got=$?
59
+ if [ "$got" -eq "$want" ]; then PASS=$((PASS+1)); echo " ok $desc"
60
+ else FAIL=$((FAIL+1)); echo " FAIL $desc (exit $got, esperado $want)"; fi
61
+ }
62
+ run() { PYTHONIOENCODING=utf-8 "$PY" "$QL" "$@"; }
63
+
64
+ cat > uscha.config.json <<'EOF'
65
+ { "version": "1.3.0",
66
+ "defaults": { "coverage_threshold": 60, "tools_per_cycle": 3,
67
+ "severity_gate": ["BLOCKER","CRITICAL","HIGH"],
68
+ "qa_tools_order": ["code-review","judgment-day","improve"],
69
+ "acceptance_file": "ACCEPTANCE.md" },
70
+ "repos": [ {"name":"repo-a","path":"repo-a","type":"maven"},
71
+ {"name":"repo-b","path":"repo-b","type":"flutter"},
72
+ {"name":"repo-c","path":"repo-c","type":"python"},
73
+ {"name":"repo-d","path":"repo-d","type":"node"},
74
+ {"name":"repo-e","path":"repo-e","type":"go"},
75
+ {"name":"repo-f","path":"repo-f","type":"rust"},
76
+ {"name":"repo-g","path":"repo-g","type":"dotnet"},
77
+ {"name":"repo-h","path":"repo-h","type":"cpp"},
78
+ {"name":"repo-i","path":"repo-i","type":"gradle"},
79
+ {"name":"repo-j","path":"repo-j","type":"swift"} ],
80
+ "integration": {"enabled": false} }
81
+ EOF
82
+ printf -- "# ACCEPTANCE\n\n- [x] criterio uno\n- [ ] criterio dos\n" > ACCEPTANCE.md
83
+ run init --config uscha.config.json >/dev/null || { echo "FAIL init"; exit 1; }
84
+
85
+ echo "== T1 readiness virgen: static UNMEASURED, no 1.0 por silencio =="
86
+ run readiness 2>/dev/null | grep -q "UNMEASURED" && { PASS=$((PASS+1)); echo " ok warning UNMEASURED presente"; } || { FAIL=$((FAIL+1)); echo " FAIL sin warning UNMEASURED"; }
87
+
88
+ echo "== T2 converged exige TODAS las tools de qa_tools_order =="
89
+ run log-step --repo repo-a --tool code-review --iteration 1 --tests-passed true >/dev/null
90
+ run log-step --repo repo-a --tool judgment-day --iteration 1 --tests-passed true >/dev/null
91
+ chk "falta 'improve' -> NOT converged" 1 run converged --repo repo-a
92
+
93
+ echo "== T3 log-gate fail bloquea convergencia; pass la limpia =="
94
+ run log-step --repo repo-a --tool improve --iteration 1 --tests-passed true >/dev/null
95
+ chk "ciclo completo limpio -> CONVERGED" 0 run converged --repo repo-a
96
+ run log-gate --repo repo-a --iteration 1 --kind golden-diff --verdict fail --note smoke >/dev/null
97
+ chk "fact gate rojo -> NOT converged" 1 run converged --repo repo-a
98
+ run log-gate --repo repo-a --iteration 2 --kind golden-diff --verdict pass >/dev/null
99
+ chk "fact gate pass -> CONVERGED de nuevo" 0 run converged --repo repo-a
100
+
101
+ echo "== T4 flag-blocker (constitution) bloquea hasta --resolve =="
102
+ run flag-blocker --repo repo-a --kind constitution --note "INV-X breached" >/dev/null
103
+ chk "blocker abierto -> NOT converged" 1 run converged --repo repo-a
104
+ chk "resolver SIN escape-analysis -> rechazado (Find Bugs Once)" 1 \
105
+ run flag-blocker --repo repo-a --kind constitution --resolve
106
+ run flag-blocker --repo repo-a --kind constitution --resolve \
107
+ --escape-analysis "hook nuevo + test que cubre INV-X" >/dev/null
108
+ chk "blocker resuelto (con escape analysis) -> CONVERGED" 0 run converged --repo repo-a
109
+
110
+ echo "== T5 escalate/resolve-escalation registrados =="
111
+ # el cap_reason solo se muestra cuando el cap MUERDE (score > techo); con score bajo
112
+ # lo observable es el REGISTRO: la escalación existe sin resolved_at y luego con él.
113
+ run escalate --repo repo-a --reason "smoke" >/dev/null
114
+ "$PY" -c "import json,sys; e=json.load(open('QA-LEDGER.json'))['escalations']; sys.exit(0 if any(not x.get('resolved_at') for x in e) else 1)" \
115
+ && { PASS=$((PASS+1)); echo " ok escalación abierta registrada"; } || { FAIL=$((FAIL+1)); echo " FAIL escalación no registrada"; }
116
+ run resolve-escalation --repo repo-a >/dev/null
117
+ "$PY" -c "import json,sys; e=json.load(open('QA-LEDGER.json'))['escalations']; sys.exit(0 if all(x.get('resolved_at') for x in e) else 1)" \
118
+ && { PASS=$((PASS+1)); echo " ok resolved_at registrado al resolver"; } || { FAIL=$((FAIL+1)); echo " FAIL resolved_at ausente"; }
119
+
120
+ echo "== T6 gate-check: borrado de archivo de test entero =="
121
+ printf -- "diff --git a/src/test/java/FooTest.java b/src/test/java/FooTest.java\n--- a/src/test/java/FooTest.java\n+++ /dev/null\n@@ -1,3 +0,0 @@\n-import org.junit.jupiter.api.Test;\n-@Test\n-void testX() { assertEquals(1,1); }\n" > del.diff
122
+ chk "delete de test file -> BLOCKER" 1 run gate-check --diff del.diff
123
+
124
+ echo "== T7 gate-check: threshold bajado cross-hunk + borrado sin re-add =="
125
+ printf -- "diff --git a/pom.xml b/pom.xml\n--- a/pom.xml\n+++ b/pom.xml\n@@ -10,1 +10,0 @@\n- <coverage-minimum>0.80</coverage-minimum>\n@@ -90,0 +90,1 @@\n+ <coverage-minimum>0.50</coverage-minimum>\n" > thr.diff
126
+ chk "lowered cross-hunk -> BLOCKER" 1 run gate-check --diff thr.diff
127
+ printf -- "diff --git a/pom.xml b/pom.xml\n--- a/pom.xml\n+++ b/pom.xml\n@@ -10,1 +10,0 @@\n- <coverage-minimum>0.80</coverage-minimum>\n@@ -55,0 +55,1 @@\n+ <coverage-minimum>0.80</coverage-minimum>\n" > move.diff
128
+ chk "mover threshold igual -> CLEAN" 0 run gate-check --diff move.diff
129
+
130
+ echo "== T7b gate-check: dependencia nueva = senal BLANDA (kit 1.30.0) =="
131
+ # la CONSTITUTION dice '0 deps nuevas sin aprobacion'; gate-check la hace visible.
132
+ printf -- 'diff --git a/package.json b/package.json\n--- a/package.json\n+++ b/package.json\n@@ -10,3 +10,4 @@\n "dependencies": {\n+ "sketchy-lib": "^2.1.0",\n "react": "^18.0.0"\n' > dep.diff
133
+ run gate-check --diff dep.diff --json 2>/dev/null | "$PY" -c "
134
+ import json, sys
135
+ d = json.load(sys.stdin)
136
+ ok = (d['verdict'] == 'REVIEW' and len(d['new_dependencies']) == 1
137
+ and 'sketchy-lib' in d['new_dependencies'][0])
138
+ sys.exit(0 if ok else 1)" \
139
+ && { PASS=$((PASS+1)); echo " ok dep nueva -> REVIEW + listada en new_dependencies"; } \
140
+ || { FAIL=$((FAIL+1)); echo " FAIL dep nueva no detectada"; }
141
+ chk "dep nueva SIN --strict -> exit 0 (advisory)" 0 run gate-check --diff dep.diff
142
+ chk "dep nueva CON --strict -> exit 1 (gatea la senal blanda)" 1 run gate-check --diff dep.diff --strict
143
+ # un diff de codigo normal NO debe flaguear dep (sin falsos positivos)
144
+ printf -- 'diff --git a/src/mod.py b/src/mod.py\n--- a/src/mod.py\n+++ b/src/mod.py\n@@ -1,2 +1,3 @@\n def f(x):\n+ return x + 1\n' > nodep.diff
145
+ run gate-check --diff nodep.diff --json 2>/dev/null | "$PY" -c "
146
+ import json, sys
147
+ sys.exit(0 if json.load(sys.stdin)['new_dependencies'] == [] else 1)" \
148
+ && { PASS=$((PASS+1)); echo " ok diff de codigo normal no flaguea dep (sin falso positivo)"; } \
149
+ || { FAIL=$((FAIL+1)); echo " FAIL falso positivo de dep en codigo normal"; }
150
+
151
+ echo "== T8 golden-diff: NOT-RUN y DIVERGE =="
152
+ mkdir -p g && ( cd g && chk "cero fixtures -> NOT-RUN exit 2" 2 run golden-diff )
153
+ ( cd g && printf "x" > f.received.txt && chk "fixture sin aprobar -> DIVERGE exit 1" 1 run golden-diff )
154
+
155
+ echo "== T9 spec-check: estructural bloquea, completo OK =="
156
+ printf -- "# SPEC\n\n## Acceptance\n\n- [ ] when a then shall b exactly 80.00\n" > s1.md
157
+ chk "sin out-of-scope -> exit 1" 1 run spec-check --spec s1.md
158
+ printf -- "# SPEC\n\n## Out of scope\n\n- x\n\n## Acceptance\n\n- [ ] when a then shall b exactly 80.00\n" > s2.md
159
+ chk "spec completa -> exit 0" 0 run spec-check --spec s2.md
160
+
161
+ echo "== T10 simplicity: floor de dims pesadas (1.9x -> OVERBUILT) =="
162
+ { printf -- "diff --git a/src/A.java b/src/A.java\n--- a/src/A.java\n+++ b/src/A.java\n@@ -1,0 +1,190 @@\n"; for i in $(seq 1 190); do printf -- "+int x%d = %d;\n" "$i" "$i"; done; } > big.diff
163
+ chk "diff 1.9x budget -> OVERBUILT exit 1" 1 run simplicity-check --diff big.diff --max-lines-added 100 --max-net-lines 999 --max-files-changed 20 --max-hunk-added 999
164
+
165
+ echo "== T11 oscillation Jaccard (a,b -> c -> a,b) =="
166
+ run log-step --repo repo-b --tool code-review --iteration 1 --fingerprint "a,b" >/dev/null
167
+ run log-step --repo repo-b --tool code-review --iteration 2 --fingerprint "c" >/dev/null
168
+ run log-step --repo repo-b --tool code-review --iteration 3 --fingerprint "a,b" >/dev/null
169
+ chk "set repetido -> OSCILLATING exit 1" 1 run oscillation --repo repo-b --tool code-review
170
+
171
+ echo "== T12 converged: snapshot rojo MEDIDO veta verde narrado =="
172
+ mkdir -p repo-a/target/surefire-reports
173
+ printf '<testsuite name="F" tests="6" failures="2" errors="0" skipped="0"/>\n' > repo-a/target/surefire-reports/TEST-F.xml
174
+ run snapshot --repo repo-a >/dev/null
175
+ chk "snapshot rojo -> NOT converged" 1 run converged --repo repo-a
176
+
177
+ echo "== T13 adapter python: coverage Cobertura + junit ENVUELTO + LOC =="
178
+ mkdir -p repo-c/src/pkg repo-c/tests repo-c/reports
179
+ printf 'def f():\n return 1\nX = 2\n' > repo-c/src/pkg/mod.py
180
+ printf 'from src.pkg import mod\ndef test_f(): assert mod.f() == 1\n' > repo-c/tests/test_mod.py
181
+ cat > repo-c/coverage.xml <<'EOF'
182
+ <?xml version="1.0"?>
183
+ <coverage lines-valid="10" lines-covered="8" line-rate="0.8" version="7.4"></coverage>
184
+ EOF
185
+ cat > repo-c/reports/junit.xml <<'EOF'
186
+ <?xml version="1.0"?>
187
+ <testsuites><testsuite name="pytest" tests="5" failures="0" errors="0" skipped="1"/></testsuites>
188
+ EOF
189
+ SNAP=$(run snapshot --repo repo-c 2>&1)
190
+ echo "$SNAP" | grep -q "coverage=80.0%" && { PASS=$((PASS+1)); echo " ok coverage Cobertura 8/10 -> 80.0%"; } || { FAIL=$((FAIL+1)); echo " FAIL coverage python ($SNAP)"; }
191
+ echo "$SNAP" | grep -q "tests=5" && { PASS=$((PASS+1)); echo " ok junit ENVUELTO (testsuites>testsuite) -> 5 tests"; } || { FAIL=$((FAIL+1)); echo " FAIL test count python ($SNAP)"; }
192
+ echo "$SNAP" | grep -q "prod_loc=3, test_loc=2" && { PASS=$((PASS+1)); echo " ok LOC prod=3 / test=2 bien clasificado"; } || { FAIL=$((FAIL+1)); echo " FAIL LOC python ($SNAP)"; }
193
+
194
+ echo "== T14 python UNMEASURED pre-ingest / medido post-ingest =="
195
+ run readiness 2>/dev/null | grep "NEVER ran" | grep -q "repo-c" \
196
+ && { PASS=$((PASS+1)); echo " ok repo-c UNMEASURED antes del ingest"; } \
197
+ || { FAIL=$((FAIL+1)); echo " FAIL repo-c no aparece como UNMEASURED"; }
198
+ cat > repo-c/reports/ruff.json <<'EOF'
199
+ [{"code":"S101","filename":"src/pkg/mod.py","location":{"row":3}},
200
+ {"code":"E501","filename":"src/pkg/mod.py","location":{"row":1}},
201
+ {"code":null,"filename":"src/pkg/mod.py","location":{"row":9}}]
202
+ EOF
203
+ printf 'src/pkg/mod.py:2: error: Incompatible return value type (got "int", expected "str") [return-value]\n' > repo-c/reports/mypy.txt
204
+ ING=$(run ingest-gate --repo repo-c --iteration 1 2>&1)
205
+ echo "$ING" | grep -q "repo-c/ruff: reported=3 gated=2" && { PASS=$((PASS+1)); echo " ok ruff: 3 findings, 2 gateados (S101 + code:null syntax = HIGH; E501 = LOW)"; } || { FAIL=$((FAIL+1)); echo " FAIL ruff ($ING)"; }
206
+ echo "$ING" | grep -q "repo-c/mypy: reported=1 gated=1" && { PASS=$((PASS+1)); echo " ok mypy: error -> HIGH gateado"; } || { FAIL=$((FAIL+1)); echo " FAIL mypy ($ING)"; }
207
+ run readiness 2>/dev/null | grep "NEVER ran" | grep -q "repo-c" \
208
+ && { FAIL=$((FAIL+1)); echo " FAIL repo-c sigue UNMEASURED tras ingest"; } \
209
+ || { PASS=$((PASS+1)); echo " ok repo-c ya no es UNMEASURED tras ingest"; }
210
+
211
+ echo "== T15 adapter node: lcov + junit envuelto + LOC ts =="
212
+ mkdir -p repo-d/src repo-d/reports repo-d/coverage
213
+ printf 'export function f(): number {\n return 1;\n}\nexport const X = 2;\n' > repo-d/src/app.ts
214
+ printf 'import { f } from "./app";\ntest("f", () => expect(f()).toBe(1));\n' > repo-d/src/app.test.ts
215
+ printf 'SF:src/app.ts\nLF:10\nLH:9\nend_of_record\n' > repo-d/coverage/lcov.info
216
+ cat > repo-d/reports/junit.xml <<'EOF'
217
+ <?xml version="1.0"?>
218
+ <testsuites><testsuite name="jest" tests="7" failures="0" errors="0" skipped="0"/></testsuites>
219
+ EOF
220
+ SNAPD=$(run snapshot --repo repo-d 2>&1)
221
+ echo "$SNAPD" | grep -q "coverage=90.0%" && { PASS=$((PASS+1)); echo " ok coverage lcov 9/10 -> 90.0%"; } || { FAIL=$((FAIL+1)); echo " FAIL coverage node ($SNAPD)"; }
222
+ echo "$SNAPD" | grep -q "tests=7" && { PASS=$((PASS+1)); echo " ok junit envuelto (jest-junit) -> 7 tests"; } || { FAIL=$((FAIL+1)); echo " FAIL test count node ($SNAPD)"; }
223
+ echo "$SNAPD" | grep -q "prod_loc=4, test_loc=2" && { PASS=$((PASS+1)); echo " ok LOC prod=4 (.ts) / test=2 (.test.ts) bien clasificado"; } || { FAIL=$((FAIL+1)); echo " FAIL LOC node ($SNAPD)"; }
224
+
225
+ echo "== T16 node UNMEASURED pre-ingest / eslint+tsc post-ingest =="
226
+ run readiness 2>/dev/null | grep "NEVER ran" | grep -q "repo-d" \
227
+ && { PASS=$((PASS+1)); echo " ok repo-d UNMEASURED antes del ingest"; } \
228
+ || { FAIL=$((FAIL+1)); echo " FAIL repo-d no aparece como UNMEASURED"; }
229
+ cat > repo-d/reports/eslint.json <<'EOF'
230
+ [{"filePath":"src/app.ts","messages":[
231
+ {"ruleId":"security/detect-eval-with-expression","severity":1,"line":2},
232
+ {"ruleId":"prefer-const","severity":1,"line":4},
233
+ {"ruleId":null,"severity":1,"line":5},
234
+ {"ruleId":null,"severity":2,"line":9,"fatal":true}]}]
235
+ EOF
236
+ printf 'src/app.ts(2,3): error TS2322: Type mismatch.\nerror TS18003: No inputs were found in config file.\n' > repo-d/reports/tsc.txt
237
+ INGD=$(run ingest-gate --repo repo-d --iteration 1 2>&1)
238
+ echo "$INGD" | grep -q "repo-d/eslint: reported=4 gated=2" && { PASS=$((PASS+1)); echo " ok eslint: security floor + fatal = HIGH; null NO-fatal (ESLint 9) = MEDIUM, no bloquea"; } || { FAIL=$((FAIL+1)); echo " FAIL eslint ($INGD)"; }
239
+ echo "$INGD" | grep -q "repo-d/tsc: reported=2 gated=2" && { PASS=$((PASS+1)); echo " ok tsc: error con archivo + error GLOBAL sin archivo (tsconfig roto) = HIGH"; } || { FAIL=$((FAIL+1)); echo " FAIL tsc ($INGD)"; }
240
+ run readiness 2>/dev/null | grep "NEVER ran" | grep -q "repo-d" \
241
+ && { FAIL=$((FAIL+1)); echo " FAIL repo-d sigue UNMEASURED tras ingest"; } \
242
+ || { PASS=$((PASS+1)); echo " ok repo-d ya no es UNMEASURED tras ingest"; }
243
+
244
+ echo "== T17 adapter go: cover profile nativo + gotestsum junit + LOC _test.go =="
245
+ mkdir -p repo-e/pkg repo-e/reports
246
+ printf 'package pkg\nfunc F() int {\nreturn 1\n}\n' > repo-e/pkg/mod.go
247
+ printf 'package pkg\nfunc TestF(t *T) {}\n' > repo-e/pkg/mod_test.go
248
+ cat > repo-e/coverage.out <<'EOF'
249
+ mode: set
250
+ example.com/m/pkg/mod.go:2.15,4.2 3 1
251
+ example.com/m/pkg/mod.go:6.2,8.3 2 0
252
+ EOF
253
+ cat > repo-e/reports/junit.xml <<'EOF'
254
+ <?xml version="1.0"?>
255
+ <testsuites><testsuite name="gotestsum" tests="4" failures="0" errors="0" skipped="0"/></testsuites>
256
+ EOF
257
+ SNAPE=$(run snapshot --repo repo-e 2>&1)
258
+ echo "$SNAPE" | grep -q "coverage=60.0%" && { PASS=$((PASS+1)); echo " ok cover profile 3/5 stmts -> 60.0%"; } || { FAIL=$((FAIL+1)); echo " FAIL coverage go ($SNAPE)"; }
259
+ echo "$SNAPE" | grep -q "tests=4" && { PASS=$((PASS+1)); echo " ok gotestsum junit envuelto -> 4 tests"; } || { FAIL=$((FAIL+1)); echo " FAIL test count go ($SNAPE)"; }
260
+ echo "$SNAPE" | grep -q "prod_loc=4, test_loc=2" && { PASS=$((PASS+1)); echo " ok LOC prod=4 (mod.go) / test=2 (_test.go junto al codigo)"; } || { FAIL=$((FAIL+1)); echo " FAIL LOC go ($SNAPE)"; }
261
+
262
+ echo "== T18 go UNMEASURED pre-ingest / golangci (checkstyle reusado) post-ingest =="
263
+ run readiness 2>/dev/null | grep "NEVER ran" | grep -q "repo-e" \
264
+ && { PASS=$((PASS+1)); echo " ok repo-e UNMEASURED antes del ingest"; } \
265
+ || { FAIL=$((FAIL+1)); echo " FAIL repo-e no aparece como UNMEASURED"; }
266
+ cat > repo-e/reports/golangci.xml <<'EOF'
267
+ <?xml version="1.0"?>
268
+ <checkstyle version="5.0">
269
+ <file name="pkg/mod.go">
270
+ <error line="3" severity="error" message="G104: unhandled error" source="gosec"/>
271
+ <error line="7" severity="warning" message="var x is unused" source="unused"/>
272
+ </file>
273
+ </checkstyle>
274
+ EOF
275
+ INGE=$(run ingest-gate --repo repo-e --iteration 1 2>&1)
276
+ echo "$INGE" | grep -q "repo-e/golangci: reported=2 gated=1" && { PASS=$((PASS+1)); echo " ok golangci via parse_checkstyle: error=HIGH gateado, warning=MEDIUM"; } || { FAIL=$((FAIL+1)); echo " FAIL golangci ($INGE)"; }
277
+ run readiness 2>/dev/null | grep "NEVER ran" | grep -q "repo-e" \
278
+ && { FAIL=$((FAIL+1)); echo " FAIL repo-e sigue UNMEASURED tras ingest"; } \
279
+ || { PASS=$((PASS+1)); echo " ok repo-e ya no es UNMEASURED tras ingest"; }
280
+
281
+ echo "== T19 adapter rust: Cobertura (cargo llvm-cov) + nextest junit + LOC tests/ =="
282
+ mkdir -p repo-f/src repo-f/tests repo-f/reports
283
+ printf 'pub fn f() -> i32 {\n 1\n}\n' > repo-f/src/lib.rs
284
+ printf 'use core_lib::f;\n#[test] fn it_works() { assert_eq!(f(), 1); }\n' > repo-f/tests/it_test.rs
285
+ cat > repo-f/reports/coverage.xml <<'EOF'
286
+ <?xml version="1.0"?>
287
+ <coverage lines-valid="10" lines-covered="7" line-rate="0.7"></coverage>
288
+ EOF
289
+ cat > repo-f/reports/junit.xml <<'EOF'
290
+ <?xml version="1.0"?>
291
+ <testsuites><testsuite name="nextest" tests="3" failures="0" errors="0" skipped="0"/></testsuites>
292
+ EOF
293
+ SNAPF=$(run snapshot --repo repo-f 2>&1)
294
+ echo "$SNAPF" | grep -q "coverage=70.0%" && { PASS=$((PASS+1)); echo " ok Cobertura reusada (cargo llvm-cov) 7/10 -> 70.0%"; } || { FAIL=$((FAIL+1)); echo " FAIL coverage rust ($SNAPF)"; }
295
+ echo "$SNAPF" | grep -q "tests=3" && { PASS=$((PASS+1)); echo " ok nextest junit -> 3 tests"; } || { FAIL=$((FAIL+1)); echo " FAIL test count rust ($SNAPF)"; }
296
+ echo "$SNAPF" | grep -q "prod_loc=3, test_loc=2" && { PASS=$((PASS+1)); echo " ok LOC prod=3 (src/) / test=2 (tests/)"; } || { FAIL=$((FAIL+1)); echo " FAIL LOC rust ($SNAPF)"; }
297
+
298
+ echo "== T20 rust: clippy JSONL (error/warning/compile-error/summary/dup) =="
299
+ # el '1 warning emitted' de rustc es un diagnostico REAL (level warning, code
300
+ # null, spans []) — sin el skip por span, cada corrida con warnings crece un
301
+ # HIGH fantasma y el gate no converge jamas. El duplicado (lib + test target)
302
+ # debe dedupearse por finding ID.
303
+ cat > repo-f/reports/clippy.json <<'EOF'
304
+ {"reason":"compiler-message","message":{"level":"error","code":{"code":"clippy::unwrap_used"},"spans":[{"file_name":"src/lib.rs","line_start":2,"is_primary":true}]}}
305
+ {"reason":"compiler-message","message":{"level":"warning","code":{"code":"clippy::needless_return"},"spans":[{"file_name":"src/lib.rs","line_start":3,"is_primary":true}]}}
306
+ {"reason":"compiler-message","message":{"level":"warning","code":{"code":"clippy::needless_return"},"spans":[{"file_name":"src/lib.rs","line_start":3,"is_primary":true}]}}
307
+ {"reason":"compiler-message","message":{"level":"error","code":null,"spans":[{"file_name":"src/lib.rs","line_start":1,"is_primary":true}]}}
308
+ {"reason":"compiler-message","message":{"level":"warning","code":null,"spans":[],"message":"1 warning emitted"}}
309
+ {"reason":"compiler-message","message":{"level":"error","code":null,"spans":[],"message":"aborting due to 1 previous error"}}
310
+ {"reason":"build-finished","success":false}
311
+ EOF
312
+ INGF=$(run ingest-gate --repo repo-f --iteration 1 2>&1)
313
+ echo "$INGF" | grep -q "repo-f/clippy: reported=3 gated=2" && { PASS=$((PASS+1)); echo " ok clippy: 3 reales (summaries sin span NO cuentan, dup dedupeado)"; } || { FAIL=$((FAIL+1)); echo " FAIL clippy ($INGF)"; }
314
+
315
+ echo "== T21 adapter dotnet: Cobertura (coverlet) + junit logger + LOC .Tests =="
316
+ mkdir -p repo-g/src repo-g/Svc.Tests repo-g/reports
317
+ printf 'namespace Svc;\npublic class Api {\npublic int F() => 1;\n}\n' > repo-g/src/Api.cs
318
+ printf 'namespace Svc.Tests;\npublic class ApiTests { }\n' > repo-g/Svc.Tests/ApiTests.cs
319
+ cat > repo-g/reports/coverage.xml <<'EOF'
320
+ <?xml version="1.0"?>
321
+ <coverage lines-valid="12" lines-covered="9" line-rate="0.75"></coverage>
322
+ EOF
323
+ cat > repo-g/reports/junit.xml <<'EOF'
324
+ <?xml version="1.0"?>
325
+ <testsuites><testsuite name="dotnet" tests="6" failures="0" errors="0" skipped="0"/></testsuites>
326
+ EOF
327
+ SNAPG=$(run snapshot --repo repo-g 2>&1)
328
+ echo "$SNAPG" | grep -q "coverage=75.0%" && { PASS=$((PASS+1)); echo " ok Cobertura reusada (coverlet) 9/12 -> 75.0%"; } || { FAIL=$((FAIL+1)); echo " FAIL coverage dotnet ($SNAPG)"; }
329
+ echo "$SNAPG" | grep -q "tests=6" && { PASS=$((PASS+1)); echo " ok junit logger -> 6 tests"; } || { FAIL=$((FAIL+1)); echo " FAIL test count dotnet ($SNAPG)"; }
330
+ echo "$SNAPG" | grep -q "prod_loc=4, test_loc=2" && { PASS=$((PASS+1)); echo " ok LOC prod=4 (src/) / test=2 (Svc.Tests/)"; } || { FAIL=$((FAIL+1)); echo " FAIL LOC dotnet ($SNAPG)"; }
331
+
332
+ echo "== T22 dotnet: SARIF (Roslyn ErrorLog) error/warning/note =="
333
+ cat > repo-g/reports/analysis.sarif <<'EOF'
334
+ {"version":"2.1.0","runs":[{"tool":{"driver":{"name":"Roslyn"}},"results":[
335
+ {"ruleId":"CA2100","level":"error","locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/Api.cs"},"region":{"startLine":3}}}]},
336
+ {"ruleId":"CA1822","level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/Api.cs"},"region":{"startLine":2}}}]},
337
+ {"ruleId":"IDE0005","level":"note","locations":[{"physicalLocation":{"artifactLocation":{"uri":"src/Api.cs"},"region":{"startLine":1}}}]}]}]}
338
+ EOF
339
+ INGG=$(run ingest-gate --repo repo-g --iteration 1 2>&1)
340
+ echo "$INGG" | grep -q "repo-g/roslyn: reported=3 gated=1" && { PASS=$((PASS+1)); echo " ok SARIF: error=HIGH gateado; warning=MEDIUM; note=INFO"; } || { FAIL=$((FAIL+1)); echo " FAIL sarif ($INGG)"; }
341
+
342
+ echo "== T23 adapter cpp: gcovr Cobertura + ctest junit (root PLANO) + LOC tests/ =="
343
+ mkdir -p repo-h/src repo-h/tests repo-h/reports
344
+ printf '#include "core.h"\nint f() {\nreturn 1;\n}\n' > repo-h/src/core.cpp
345
+ printf '#include "core.h"\nTEST(Core, F) { EXPECT_EQ(f(), 1); }\n' > repo-h/tests/core_test.cpp
346
+ cat > repo-h/reports/coverage.xml <<'EOF'
347
+ <?xml version="1.0"?>
348
+ <coverage lines-valid="8" lines-covered="6" line-rate="0.75"></coverage>
349
+ EOF
350
+ cat > repo-h/reports/junit.xml <<'EOF'
351
+ <?xml version="1.0"?>
352
+ <testsuite name="ctest" tests="5" failures="0" errors="0" skipped="0"/>
353
+ EOF
354
+ SNAPH=$(run snapshot --repo repo-h 2>&1)
355
+ echo "$SNAPH" | grep -q "coverage=75.0%" && { PASS=$((PASS+1)); echo " ok gcovr Cobertura 6/8 -> 75.0%"; } || { FAIL=$((FAIL+1)); echo " FAIL coverage cpp ($SNAPH)"; }
356
+ echo "$SNAPH" | grep -q "tests=5" && { PASS=$((PASS+1)); echo " ok ctest junit root PLANO (testsuite) -> 5 tests"; } || { FAIL=$((FAIL+1)); echo " FAIL test count cpp ($SNAPH)"; }
357
+ echo "$SNAPH" | grep -q "prod_loc=4, test_loc=2" && { PASS=$((PASS+1)); echo " ok LOC prod=4 (src/) / test=2 (tests/ + _test.cpp)"; } || { FAIL=$((FAIL+1)); echo " FAIL LOC cpp ($SNAPH)"; }
358
+
359
+ echo "== T24 cpp: clang-tidy (error/warning/cert floor/.tpp) =="
360
+ cat > repo-h/reports/clang-tidy.txt <<'EOF'
361
+ src/core.cpp:3:1: error: use of undeclared identifier 'x' [clang-diagnostic-error]
362
+ src/core.cpp:2:5: warning: function 'f' should be marked const [readability-make-member-function-const]
363
+ src/core.cpp:3:8: warning: calling 'system' uses a command processor [cert-env33-c]
364
+ src/impl.tpp:4:2: warning: repeated branch body [bugprone-branch-clone]
365
+ 2 warnings generated.
366
+ EOF
367
+ INGH=$(run ingest-gate --repo repo-h --iteration 1 2>&1)
368
+ echo "$INGH" | grep -q "repo-h/clang-tidy: reported=4 gated=2" && { PASS=$((PASS+1)); echo " ok clang-tidy: error+cert floor=HIGH; readability+.tpp=MEDIUM; ruido stderr ignorado"; } || { FAIL=$((FAIL+1)); echo " FAIL clang-tidy ($INGH)"; }
369
+
370
+ echo "== T25 fixes 1.7.0: junit root-max (gotestsum) + go dedupe + backtest.cpp prod =="
371
+ # (a) gotestsum reporta errors solo en el ROOT <testsuites> — el max(root, hijos)
372
+ # tiene que leer los atributos del root, no solo sumar hijos.
373
+ cat > repo-e/reports/junit.xml <<'EOF'
374
+ <?xml version="1.0"?>
375
+ <testsuites tests="9" failures="0" errors="1" skipped="0"><testsuite name="gotestsum" tests="4" failures="0" errors="0" skipped="0"/></testsuites>
376
+ EOF
377
+ SNAPE2=$(run snapshot --repo repo-e 2>&1)
378
+ echo "$SNAPE2" | grep -q "tests=9" && { PASS=$((PASS+1)); echo " ok junit root-attrs mandan: tests=9 (hijos sumaban 4)"; } || { FAIL=$((FAIL+1)); echo " FAIL junit root-max ($SNAPE2)"; }
379
+ # (b) -coverpkg repite bloques entre targets: dedupe por bloque con max(hits).
380
+ # El bloque de 2 stmts (0 hits) reaparece con 1 hit -> 5/5 stmts = 100%.
381
+ printf 'example.com/m/pkg/mod.go:6.2,8.3 2 1\n' >> repo-e/coverage.out
382
+ SNAPE3=$(run snapshot --repo repo-e 2>&1)
383
+ echo "$SNAPE3" | grep -q "coverage=100.0%" && { PASS=$((PASS+1)); echo " ok cover profile dedupeado por bloque con max(hits) -> 100.0%"; } || { FAIL=$((FAIL+1)); echo " FAIL go dedupe ($SNAPE3)"; }
384
+ # (c) sufijo bare 'test.cpp' NO debe tragar backtest.cpp como test LOC.
385
+ printf 'int backtest() {\nreturn 2;\n}\n' > repo-h/src/backtest.cpp
386
+ SNAPH2=$(run snapshot --repo repo-h 2>&1)
387
+ echo "$SNAPH2" | grep -q "prod_loc=7, test_loc=2" && { PASS=$((PASS+1)); echo " ok backtest.cpp cuenta como PROD (CamelCase Test.cpp es el patron de test)"; } || { FAIL=$((FAIL+1)); echo " FAIL backtest.cpp ($SNAPH2)"; }
388
+
389
+ echo "== T26 adapter gradle: JaCoCo en paths gradle + test-results + LOC source sets =="
390
+ mkdir -p repo-i/src/main/kotlin repo-i/src/test/kotlin repo-i/src/integrationTest/kotlin repo-i/build/reports/jacoco/test repo-i/build/test-results/test repo-i/build/reports/detekt
391
+ printf 'package app\nfun f(): Int {\nreturn 1\n}\n' > repo-i/src/main/kotlin/App.kt
392
+ printf 'package app\nclass AppTest { }\n' > repo-i/src/test/kotlin/AppTest.kt
393
+ printf 'package app\nclass AppIT { }\n' > repo-i/src/integrationTest/kotlin/AppIT.kt
394
+ cat > repo-i/build/reports/jacoco/test/jacocoTestReport.xml <<'EOF'
395
+ <?xml version="1.0"?>
396
+ <report name="jvm-service"><counter type="LINE" missed="3" covered="9"/></report>
397
+ EOF
398
+ cat > repo-i/build/test-results/test/TEST-app.AppTest.xml <<'EOF'
399
+ <?xml version="1.0"?>
400
+ <testsuite name="app.AppTest" tests="8" failures="0" errors="0" skipped="0"/>
401
+ EOF
402
+ SNAPI=$(run snapshot --repo repo-i 2>&1)
403
+ echo "$SNAPI" | grep -q "coverage=75.0%" && { PASS=$((PASS+1)); echo " ok JaCoCo en build/reports/jacoco 9/12 -> 75.0%"; } || { FAIL=$((FAIL+1)); echo " FAIL coverage gradle ($SNAPI)"; }
404
+ echo "$SNAPI" | grep -q "tests=8" && { PASS=$((PASS+1)); echo " ok build/test-results TEST-*.xml -> 8 tests"; } || { FAIL=$((FAIL+1)); echo " FAIL test count gradle ($SNAPI)"; }
405
+ echo "$SNAPI" | grep -q "prod_loc=4, test_loc=4" && { PASS=$((PASS+1)); echo " ok LOC src/main=4 prod / src/test + src/integrationTest=4 test (source sets custom)"; } || { FAIL=$((FAIL+1)); echo " FAIL LOC gradle ($SNAPI)"; }
406
+
407
+ echo "== T27 gradle: detekt (checkstyle reusado, paths ABSOLUTOS como el default real) =="
408
+ ABSI="$("$PY" -c "import os;print(os.path.abspath('repo-i/src/main/kotlin/App.kt'))")"
409
+ cat > repo-i/build/reports/detekt/detekt.xml <<EOF
410
+ <?xml version="1.0"?>
411
+ <checkstyle version="4.3">
412
+ <file name="$ABSI">
413
+ <error line="2" severity="error" message="ForbiddenCall" source="detekt.ForbiddenCall"/>
414
+ <error line="3" severity="warning" message="MagicNumber" source="detekt.MagicNumber"/>
415
+ </file>
416
+ </checkstyle>
417
+ EOF
418
+ INGI=$(run ingest-gate --repo repo-i --iteration 1 2>&1)
419
+ echo "$INGI" | grep -q "repo-i/detekt: reported=2 gated=1" && { PASS=$((PASS+1)); echo " ok detekt via parse_checkstyle: error=HIGH gateado, warning=MEDIUM"; } || { FAIL=$((FAIL+1)); echo " FAIL detekt ($INGI)"; }
420
+ "$PY" -c "
421
+ import json, sys
422
+ node = json.load(open('QA-LEDGER.json'))['repos']['repo-i']
423
+ ids = [i for s in node['iterations'] if s.get('finding_ids') for i in s['finding_ids']]
424
+ ok = any(x.startswith('detekt:') and 'src/main/kotlin/App.kt' in x.replace(chr(92), '/') and not x.replace(chr(92), '/').count(':/') for x in ids)
425
+ sys.exit(0 if ok else 1)" \
426
+ && { PASS=$((PASS+1)); echo " ok IDs detekt repo-relativos (path absoluto relativizado)"; } \
427
+ || { FAIL=$((FAIL+1)); echo " FAIL IDs detekt no relativizados"; }
428
+ run readiness 2>/dev/null | grep "NEVER ran" | grep -q "repo-i" \
429
+ && { FAIL=$((FAIL+1)); echo " FAIL repo-i sigue UNMEASURED tras ingest"; } \
430
+ || { PASS=$((PASS+1)); echo " ok repo-i ya no es UNMEASURED tras ingest"; }
431
+
432
+ echo "== T28 adapter swift: lcov + xunit + LOC Sources/Tests + swiftlint =="
433
+ mkdir -p repo-j/Sources/Kit repo-j/Tests/KitTests repo-j/coverage repo-j/reports
434
+ printf 'public func f() -> Int {\nreturn 1\n}\n' > repo-j/Sources/Kit/Kit.swift
435
+ printf 'import XCTest\nfinal class KitTests: XCTestCase { }\n' > repo-j/Tests/KitTests/KitTests.swift
436
+ printf 'SF:Sources/Kit/Kit.swift\nLF:16\nLH:12\nend_of_record\n' > repo-j/coverage/lcov.info
437
+ cat > repo-j/reports/junit.xml <<'EOF'
438
+ <?xml version="1.0"?>
439
+ <testsuites><testsuite name="KitTests" tests="7" failures="0" errors="0" skipped="0"/></testsuites>
440
+ EOF
441
+ # Swift 6 / Swift Testing: --xunit-output escribe un SEGUNDO archivo con los
442
+ # resultados de Swift Testing — el engine debe SUMAR ambos (sets disjuntos);
443
+ # si lo ignorara, este failure real seria invisible (fail-open).
444
+ cat > repo-j/reports/junit-swift-testing.xml <<'EOF'
445
+ <?xml version="1.0"?>
446
+ <testsuites><testsuite name="SwiftTesting" tests="2" failures="1" errors="0" skipped="0"/></testsuites>
447
+ EOF
448
+ SNAPJ=$(run snapshot --repo repo-j 2>&1)
449
+ echo "$SNAPJ" | grep -q "coverage=75.0%" && { PASS=$((PASS+1)); echo " ok lcov reusado (llvm-cov export) 12/16 -> 75.0%"; } || { FAIL=$((FAIL+1)); echo " FAIL coverage swift ($SNAPJ)"; }
450
+ echo "$SNAPJ" | grep -q "tests=9" && { PASS=$((PASS+1)); echo " ok XCTest (7) + Swift Testing (2) SUMADOS -> 9 tests (failure real visible)"; } || { FAIL=$((FAIL+1)); echo " FAIL test count swift dual-file ($SNAPJ)"; }
451
+ echo "$SNAPJ" | grep -q "prod_loc=3, test_loc=2" && { PASS=$((PASS+1)); echo " ok LOC Sources/=3 / Tests/=2 (convencion SwiftPM)"; } || { FAIL=$((FAIL+1)); echo " FAIL LOC swift ($SNAPJ)"; }
452
+ ABSJ="$("$PY" -c "import os;print(os.path.abspath('repo-j/Sources/Kit/Kit.swift'))")"
453
+ cat > repo-j/reports/swiftlint.xml <<EOF
454
+ <?xml version="1.0"?>
455
+ <checkstyle version="4.3">
456
+ <file name="$ABSJ">
457
+ <error line="1" severity="error" message="Force Cast Violation" source="swiftlint.force_cast"/>
458
+ <error line="2" severity="warning" message="Line Length Violation" source="swiftlint.line_length"/>
459
+ </file>
460
+ </checkstyle>
461
+ EOF
462
+ INGJ=$(run ingest-gate --repo repo-j --iteration 1 2>&1)
463
+ echo "$INGJ" | grep -q "repo-j/swiftlint: reported=2 gated=1" && { PASS=$((PASS+1)); echo " ok swiftlint via parse_checkstyle: error=HIGH gateado, warning=MEDIUM"; } || { FAIL=$((FAIL+1)); echo " FAIL swiftlint ($INGJ)"; }
464
+ "$PY" -c "
465
+ import json, sys
466
+ node = json.load(open('QA-LEDGER.json'))['repos']['repo-j']
467
+ ids = [i for s in node['iterations'] if s.get('finding_ids') for i in s['finding_ids']]
468
+ ok = any(x.startswith('swiftlint:') and 'Sources/Kit/Kit.swift' in x.replace(chr(92), '/') and not x.replace(chr(92), '/').count(':/') for x in ids)
469
+ sys.exit(0 if ok else 1)" \
470
+ && { PASS=$((PASS+1)); echo " ok IDs swiftlint repo-relativos (sin colision por basename)"; } \
471
+ || { FAIL=$((FAIL+1)); echo " FAIL IDs swiftlint no relativizados"; }
472
+
473
+ echo "== T29 acceptance trazable: AC-n cierra por testcase MEDIDO, no por checkbox =="
474
+ # AC-1: checkbox [x] + testcase verde 'test_ac1_*' -> cierra MEDIDO.
475
+ # AC-2: checkbox [x] pero su testcase FALLA -> narrated-only, NO cierra.
476
+ # AC-3: sin marcar y sin test -> abierta. Dimension acceptance = 1/3.
477
+ printf -- "# ACCEPTANCE\n\n- [x] AC-01 alta de cliente valida\n- [x] AC-02 rechazo de duplicado\n- [ ] AC-03 baja logica\n" > ACCEPTANCE.md
478
+ cat > repo-c/reports/junit.xml <<'EOF'
479
+ <?xml version="1.0"?>
480
+ <testsuites><testsuite name="pytest" tests="3" failures="1" errors="0" skipped="0">
481
+ <testcase classname="tests.test_flow" name="test_ac1_alta_ok"/>
482
+ <testcase classname="tests.test_flow" name="test_ac_02_rechazo_duplicado"><failure message="boom"/></testcase>
483
+ <testcase classname="tests.test_misc" name="test_sin_criterio"/>
484
+ </testsuite></testsuites>
485
+ EOF
486
+ RDY=$(run readiness --json 2>/dev/null)
487
+ echo "$RDY" | "$PY" -c "
488
+ import json, sys
489
+ d = json.load(sys.stdin)
490
+ a = d['acceptance']
491
+ ok = (a['traceable'] is True and a['measured_closed'] == ['AC-1']
492
+ and a['narrated_only'] == ['AC-2']
493
+ and abs(d['dimensions']['acceptance']['raw'] - 0.333) < 0.01)
494
+ sys.exit(0 if ok else 1)" \
495
+ && { PASS=$((PASS+1)); echo " ok AC-1 cierra medido; AC-2 narrated-only (test rojo veta); dim=1/3"; } \
496
+ || { FAIL=$((FAIL+1)); echo " FAIL acceptance trazable ($(echo "$RDY" | "$PY" -c 'import json,sys;print(json.load(sys.stdin)["acceptance"])' 2>/dev/null))"; }
497
+ run readiness 2>/dev/null | grep -q "narrated-only: AC-2" \
498
+ && { PASS=$((PASS+1)); echo " ok warning narrated-only visible (measured beats narrated)"; } \
499
+ || { FAIL=$((FAIL+1)); echo " FAIL sin warning narrated-only"; }
500
+ # % TERMINADO medido (1.28.0): 1 de 3 AC cerrado por test verde = 33.3%, informativo
501
+ run readiness --json 2>/dev/null | "$PY" -c "import json,sys; sys.exit(0 if json.load(sys.stdin)['acceptance']['measured_pct']==33.3 else 1)" \
502
+ && { PASS=$((PASS+1)); echo " ok acceptance.measured_pct = 33.3% en --json (1 de 3 medido)"; } \
503
+ || { FAIL=$((FAIL+1)); echo " FAIL measured_pct mal computado"; }
504
+ run readiness 2>/dev/null | grep -q "acceptance medido: 33.3%" \
505
+ && { PASS=$((PASS+1)); echo " ok '% terminado' medido visible en la vista default"; } \
506
+ || { FAIL=$((FAIL+1)); echo " FAIL linea de acceptance medido ausente"; }
507
+ # sin trazabilidad AC-n NO hay % honesto: la linea no debe aparecer
508
+ printf -- "# ACCEPTANCE\n\n- [x] criterio uno\n- [ ] criterio dos\n" > ACCEPTANCE.md
509
+ run readiness 2>/dev/null | grep -q "acceptance medido:" \
510
+ && { FAIL=$((FAIL+1)); echo " FAIL muestra % medido sin AC-IDs (deshonesto)"; } \
511
+ || { PASS=$((PASS+1)); echo " ok sin AC-IDs no muestra % medido (honesto)"; }
512
+ printf -- "# ACCEPTANCE\n\n- [x] AC-01 alta de cliente valida\n- [x] AC-02 rechazo de duplicado\n- [ ] AC-03 baja logica\n" > ACCEPTANCE.md
513
+
514
+ echo "== T30 spec-check --acceptance: trazabilidad como FACT =="
515
+ chk "acceptance con AC-IDs -> exit 0" 0 run spec-check --acceptance ACCEPTANCE.md
516
+ printf -- "- [ ] criterio sin id\n" > acc-untraced.md
517
+ chk "cero criterios trazables -> BLOCKED exit 1" 1 run spec-check --acceptance acc-untraced.md
518
+ printf -- "- [ ] AC-01 a\n- [x] AC-1 b\n" > acc-dup.md
519
+ chk "IDs duplicados (AC-01 == AC-1 normalizado) -> exit 1" 1 run spec-check --acceptance acc-dup.md
520
+
521
+ echo "== T31 edges 1.10.0: regex sin falsos positivos + flaky de surefire =="
522
+ # HVAC2/mac1/track12 NO son tags AC; classname jamas taggea (solo el NOMBRE);
523
+ # flaky que paso tras retry (solo <flakyFailure>) = verde; fallo definitivo
524
+ # (<failure> + <rerunFailure>) = rojo y veta.
525
+ "$PY" -c "
526
+ import sys, os, tempfile
527
+ sys.path.insert(0, os.path.dirname(sys.argv[1]))
528
+ import qa_ledger as q
529
+ for name in ('testHVAC2Compressor', 'test_mac1_address', 'testMac1Parse', 'test_track12'):
530
+ assert q._AC_TAG.findall(name) == [], name
531
+ for name, want in (('test_ac1_alta', ['1']), ('testAC01X', ['1']), ('AC-01: alta', ['1'])):
532
+ assert q._AC_TAG.findall(name) == want, name
533
+ xml_ = '''<testsuites><testsuite name=\"s\">
534
+ <testcase classname=\"tests.test_ac9_flow\" name=\"test_sin_tag\"/>
535
+ <testcase classname=\"C\" name=\"test_ac1_flaky\"><flakyFailure message=\"retry\"/></testcase>
536
+ <testcase classname=\"C\" name=\"test_ac2_fallo\"><failure message=\"x\"/><rerunFailure message=\"r\"/></testcase>
537
+ </testsuite></testsuites>'''
538
+ d = tempfile.mkdtemp(); os.makedirs(os.path.join(d, 'reports'))
539
+ open(os.path.join(d, 'reports', 'junit.xml'), 'w').write(xml_)
540
+ t, _stale = q._ac_tags(d, 'python')
541
+ assert 'AC-9' not in t, 'classname no taggea'
542
+ assert t['AC-1'] == {'green': 1, 'red': 0}, 'flaky-que-paso = verde'
543
+ assert t['AC-2'] == {'green': 0, 'red': 1}, 'fallo-tras-reruns = rojo'
544
+ sys.exit(0)" "$QL" \
545
+ && { PASS=$((PASS+1)); echo " ok sin falsos positivos (HVAC2/mac1/track12); classname no taggea; flaky ok"; } \
546
+ || { FAIL=$((FAIL+1)); echo " FAIL edges regex/flaky 1.10.0"; }
547
+
548
+ echo "== T32 simplicity: tests FUERA del presupuesto (M9, Topic 51) =="
549
+ # Diff con 6 lineas de prod y 300 de test: el presupuesto solo ve las 6.
550
+ "$PY" -c "
551
+ import io, os, sys
552
+ sys.path.insert(0, os.path.dirname(sys.argv[1]))
553
+ import qa_ledger as q
554
+ prod = ''.join(f'+line {i}\n' for i in range(6))
555
+ test = ''.join(f'+assert {i}\n' for i in range(300))
556
+ diff = (
557
+ 'diff --git a/src/main/java/App.java b/src/main/java/App.java\n'
558
+ '--- a/src/main/java/App.java\n+++ b/src/main/java/App.java\n@@ -0,0 +1,6 @@\n' + prod +
559
+ 'diff --git a/src/test/java/AppTest.java b/src/test/java/AppTest.java\n'
560
+ '--- /dev/null\n+++ b/src/test/java/AppTest.java\n@@ -0,0 +1,300 @@\n' + test +
561
+ 'diff --git a/tests/test_flow.py b/tests/test_flow.py\n'
562
+ '--- /dev/null\n+++ b/tests/test_flow.py\n@@ -0,0 +1,2 @@\n+x = 1\n+y = 2\n'
563
+ )
564
+ m = q._simplicity_metrics(diff, 4)
565
+ assert m['lines_added'] == 6, m
566
+ assert m['files_changed'] == 1, m
567
+ assert m['test_lines_added'] == 302, m
568
+ assert m['test_files_changed'] == 2, m
569
+ # clasificador: convenciones de los 9 stacks, sin tragar backtest.cpp
570
+ tf = q._is_simplicity_test_file
571
+ assert tf('src/test/java/AppTest.java') and tf('tests/test_flow.py')
572
+ assert tf('src/integrationTest/kotlin/FlowTest.kt') and tf('lib/foo.spec.ts')
573
+ assert tf('Api.Tests/FooTests.cs') and tf('pkg/foo_test.go')
574
+ assert not tf('src/main/java/App.java') and not tf('src/backtest.cpp')
575
+ assert not tf('src/protest.cc') and not tf('Sources/Core/Engine.swift')
576
+ sys.exit(0)" "$QL" \
577
+ && { PASS=$((PASS+1)); echo " ok presupuesto solo prod (6 lineas, 1 archivo); tests contados aparte (+302 en 2)"; } \
578
+ || { FAIL=$((FAIL+1)); echo " FAIL tests dentro del presupuesto de simplicity"; }
579
+
580
+ echo "== T33 gate-check: borrado de tests go/dotnet/js tambien bloquea (clasificador unificado) =="
581
+ # gate-check reusa el clasificador de los 9 stacks + TESTDEF ampliado: la promesa
582
+ # 'borrar tests lo bloquea gate-check' vale para TODAS las convenciones, no solo JVM.
583
+ printf -- "diff --git a/pkg/foo_test.go b/pkg/foo_test.go\n--- a/pkg/foo_test.go\n+++ /dev/null\n@@ -1,2 +0,0 @@\n-func TestFoo(t *testing.T) {\n-\tassertEqual(t, 1, 1)\n" > del-go.diff
584
+ chk "delete de foo_test.go (Go) -> BLOCKER" 1 run gate-check --diff del-go.diff
585
+ printf -- "diff --git a/Api.Tests/CalcTests.cs b/Api.Tests/CalcTests.cs\n--- a/Api.Tests/CalcTests.cs\n+++ /dev/null\n@@ -1,2 +0,0 @@\n-[Fact]\n-public void Suma_Valida() { Assert.Equal(2, Calc.Suma(1,1)); }\n" > del-cs.diff
586
+ chk "delete de Api.Tests/*.cs (xunit [Fact]) -> BLOCKER" 1 run gate-check --diff del-cs.diff
587
+ printf -- "diff --git a/__tests__/flow.test.ts b/__tests__/flow.test.ts\n--- a/__tests__/flow.test.ts\n+++ /dev/null\n@@ -1,2 +0,0 @@\n-it('valida el flujo', () => {\n- expect(flow()).toBe(true);\n" > del-ts.diff
588
+ chk "delete de __tests__/*.test.ts (it/expect) -> BLOCKER" 1 run gate-check --diff del-ts.diff
589
+
590
+ echo "== T34 gate-check: secret-scan (Topic 43) — secretos agregados bloquean como hecho =="
591
+ printf -- "diff --git a/src/cfg.py b/src/cfg.py\n--- a/src/cfg.py\n+++ b/src/cfg.py\n@@ -0,0 +1,1 @@\n+AWS_KEY = \"AKIAIOSFODNN7EXAMPLE\"\n" > sec-akia.diff
592
+ chk "AWS access key agregada -> BLOCKER" 1 run gate-check --diff sec-akia.diff
593
+ printf -- "diff --git a/deploy/id_rsa b/deploy/id_rsa\n--- /dev/null\n+++ b/deploy/id_rsa\n@@ -0,0 +1,1 @@\n+-----BEGIN PRIVATE KEY-----\n" > sec-pem.diff
594
+ chk "clave privada PEM agregada -> BLOCKER" 1 run gate-check --diff sec-pem.diff
595
+ printf -- "diff --git a/certs/client.p12 b/certs/client.p12\nindex 0000000..1111111 100644\nBinary files a/certs/client.p12 and b/certs/client.p12 differ\n" > sec-p12.diff
596
+ chk "contenedor .p12 binario agregado -> BLOCKER" 1 run gate-check --diff sec-p12.diff
597
+ printf -- "diff --git a/certs/old.p12 b/certs/old.p12\ndeleted file mode 100644\nindex 1111111..0000000\nBinary files a/certs/old.p12 and /dev/null differ\n" > sec-del.diff
598
+ chk "BORRAR un .p12 no bloquea (sacar secretos es bueno)" 0 run gate-check --diff sec-del.diff
599
+ printf -- "diff --git a/src/cfg.py b/src/cfg.py\n--- a/src/cfg.py\n+++ b/src/cfg.py\n@@ -0,0 +1,1 @@\n+password = \"hunter2secreto\"\n" > sec-lit.diff
600
+ chk "literal password generico -> REVIEW exit 0 (advisory)" 0 run gate-check --diff sec-lit.diff
601
+ chk "literal password generico + --strict -> exit 1" 1 run gate-check --diff sec-lit.diff --strict
602
+
603
+ echo "== T35 ledger atomico: checksum de integridad + carga blindada =="
604
+ # el ledger recien escrito trae integrity y carga verificado
605
+ chk "ledger con integrity carga OK" 0 run summary
606
+ # mutacion EXTERNA (JSON valido, contenido cambiado, hash viejo) -> bloquea
607
+ "$PY" -c "
608
+ import json, sys
609
+ d = json.load(open('QA-LEDGER.json', encoding='utf-8'))
610
+ assert 'integrity' in d and d['integrity']['sha256'], 'falta integrity en ledger nuevo'
611
+ d['config']['defaults']['coverage_threshold'] = 1
612
+ json.dump(d, open('QA-LEDGER.json', 'w', encoding='utf-8'))"
613
+ chk "mutacion externa (checksum roto) -> bloquea exit 1" 1 run summary
614
+ run summary 2>&1 | grep -qi "checksum" \
615
+ && { PASS=$((PASS+1)); echo " ok mensaje de checksum presente (no traceback)"; } \
616
+ || { FAIL=$((FAIL+1)); echo " FAIL sin mensaje de checksum"; }
617
+ # aceptacion humana explicita: borrar 'integrity' -> legacy, carga sin verificar
618
+ "$PY" -c "
619
+ import json
620
+ d = json.load(open('QA-LEDGER.json', encoding='utf-8'))
621
+ del d['integrity']
622
+ json.dump(d, open('QA-LEDGER.json', 'w', encoding='utf-8'))"
623
+ chk "legacy sin integrity -> carga OK (adopcion incremental)" 0 run summary
624
+ # JSON corrupto (escritura parcial) -> mensaje de recuperacion, no traceback
625
+ "$PY" -c "open('QA-LEDGER.json','a',encoding='utf-8').write('{trunc')"
626
+ chk "JSON corrupto -> exit 1 con mensaje" 1 run summary
627
+ run summary 2>&1 | grep -q "Traceback" \
628
+ && { FAIL=$((FAIL+1)); echo " FAIL traceback crudo en ledger corrupto"; } \
629
+ || { PASS=$((PASS+1)); echo " ok sin traceback: mensaje de recuperacion"; }
630
+ # restaurar el ledger para lo que venga despues (re-init limpio)
631
+ run init --config uscha.config.json >/dev/null 2>&1
632
+
633
+ echo "== T36 plateau/stop-signal: advisory sobre el historico (Know When to Stop) =="
634
+ # (a) stall: findings gateados SUBIENDO 3 ciclos COMPLETOS en repo-a -> re-planear.
635
+ # Con qa_tools_order configurado solo cuentan ciclos con TODAS las tools logueadas.
636
+ for i in 1 2 3; do
637
+ run log-step --repo repo-a --tool code-review --iteration $i \
638
+ --reported $((i+3)) --gated-reported $((i+3)) --tests-passed true >/dev/null 2>&1
639
+ for t in judgment-day improve; do
640
+ run log-step --repo repo-a --tool $t --iteration $i \
641
+ --gated-reported 0 --tests-passed true >/dev/null 2>&1
642
+ done
643
+ done
644
+ run readiness 2>/dev/null | grep -q "stall: repo-a" \
645
+ && { PASS=$((PASS+1)); echo " ok stall detectado (findings 4->5->6, iterar no acerca)"; } \
646
+ || { FAIL=$((FAIL+1)); echo " FAIL sin aviso de stall"; }
647
+ # (b) mismo patron pero con el ULTIMO ciclo INCOMPLETO (1 de 3 tools) -> no cuenta,
648
+ # la serie completa queda corta y el stall NO dispara (sin contaminacion parcial)
649
+ run log-step --repo repo-a --tool code-review --iteration 4 \
650
+ --gated-reported 9 --tests-passed true >/dev/null 2>&1
651
+ run readiness 2>/dev/null | grep -q "stall: repo-a" \
652
+ && { PASS=$((PASS+1)); echo " ok ciclo 4 parcial no rompe la serie (stall sigue por ciclos 1-3)"; } \
653
+ || { FAIL=$((FAIL+1)); echo " FAIL ciclo parcial altero la deteccion"; }
654
+ # (c) serie BAJANDO no es stall (hay progreso)
655
+ run init --config uscha.config.json >/dev/null 2>&1
656
+ for i in 1 2 3; do
657
+ run log-step --repo repo-a --tool code-review --iteration $i \
658
+ --reported $((7-i*2)) --gated-reported $((7-i*2)) --tests-passed true >/dev/null 2>&1
659
+ for t in judgment-day improve; do
660
+ run log-step --repo repo-a --tool $t --iteration $i \
661
+ --gated-reported 0 --tests-passed true >/dev/null 2>&1
662
+ done
663
+ done
664
+ run readiness 2>/dev/null | grep -q "stall: repo-a" \
665
+ && { FAIL=$((FAIL+1)); echo " FAIL stall con serie bajando (5->3->1 es progreso)"; } \
666
+ || { PASS=$((PASS+1)); echo " ok serie bajando (5->3->1) no dispara stall"; }
667
+ # (c) stop-signal: repo unico convergido, cero facts bloqueantes -> candidato a PR
668
+ printf '{ "defaults": { "acceptance_file": "ACCEPTANCE.md", "qa_tools_order": ["code-review","judgment-day","improve"] },\n "repos": [ {"name":"solo","path":"repo-c","type":"python"} ], "integration": {"enabled": false} }\n' > c-solo.json
669
+ run init --config c-solo.json --out L-solo.json >/dev/null 2>&1
670
+ for t in code-review judgment-day improve; do
671
+ run log-step --ledger L-solo.json --repo solo --tool $t --iteration 1 \
672
+ --gated-reported 0 --files-changed 0 --tests-passed true >/dev/null 2>&1
673
+ done
674
+ RDY=$(run readiness --ledger L-solo.json --json 2>/dev/null)
675
+ echo "$RDY" | "$PY" -c "
676
+ import json, sys
677
+ d = json.load(sys.stdin)
678
+ a = d['advice']
679
+ sys.exit(0 if a['stop_signal'] is True and a['stalled_repos'] == [] else 1)" \
680
+ && { PASS=$((PASS+1)); echo " ok stop-signal: convergido + cero facts bloqueantes -> candidato a PR"; } \
681
+ || { FAIL=$((FAIL+1)); echo " FAIL stop-signal no emitido ($(echo "$RDY" | "$PY" -c 'import json,sys;print(json.load(sys.stdin).get("advice"))' 2>/dev/null))"; }
682
+
683
+ echo "== T37 golden scrub: volatiles declarados enmascaran, el masking es VISIBLE =="
684
+ # Nota INV-GOLDEN-01: crear un .approved es un acto HUMANO incluso en tests —
685
+ # igual que el path CLEAN byte-a-byte, el path CLEAN-via-scrub NO se auto-testea.
686
+ # La mecanica de scrub se prueba a nivel FUNCION (sin fixtures aprobados).
687
+ "$PY" -c "
688
+ import sys, os, re
689
+ sys.path.insert(0, os.path.dirname(sys.argv[1]))
690
+ import qa_ledger as q
691
+ rules = [(re.compile(r'\d{4}-\d{2}-\d{2}T[0-9:Z.+-]+'), '<TS>', 'ts')]
692
+ counts = {}
693
+ a = q._scrub(b'ok at 2026-07-03T10:00:00Z\nvalor=42\n', rules, counts)
694
+ b = q._scrub(b'ok at 2026-07-01T09:30:00Z\nvalor=42\n', rules, counts)
695
+ assert a == b == b'ok at <TS>\nvalor=42\n', (a, b)
696
+ assert counts['ts'] == 2, counts # el masking se CUENTA, no es magia
697
+ # divergencia real (mas alla del volatil) NO se enmascara
698
+ c = q._scrub(b'ok at 2026-07-03T10:00:00Z\nvalor=99\n', rules, counts)
699
+ assert c != a
700
+ # binario: intacto, sigue byte a byte
701
+ raw = bytes([0xff, 0xfe, 0x00, 0x42])
702
+ assert q._scrub(raw, rules, counts) == raw
703
+ sys.exit(0)" "$QL" \
704
+ && { PASS=$((PASS+1)); echo " ok scrub enmascara volatiles, cuenta sustituciones, binario intacto"; } \
705
+ || { FAIL=$((FAIL+1)); echo " FAIL mecanica de scrub"; }
706
+ mkdir -p gsc
707
+ printf "ok at 2026-07-03T10:00:00Z\n" > gsc/out.received.txt
708
+ printf '{ "rules": [ {"pattern": "\\\\d{4}-\\\\d{2}-\\\\d{2}T[0-9:Z.+-]+", "replace": "<TS>"} ] }\n' > gsc/golden.scrub.json
709
+ # el scrub NO fabrica aprobacion: .received sin .approved sigue DIVERGE
710
+ chk "scrub activo sin .approved -> sigue DIVERGE exit 1" 1 run golden-diff --dir gsc
711
+ # scrub invalido = error de config explicito, jamas se saltea en silencio
712
+ printf '{ "rules": [ {"pattern": "([", "replace": "x"} ] }\n' > gsc/golden.scrub.json
713
+ chk "scrub invalido (regex rota) -> exit 2 (config error)" 2 run golden-diff --dir gsc
714
+ printf '[ {"pattern": "x", "replace": "y"} ]\n' > gsc/golden.scrub.json
715
+ chk "scrub con shape invalida (lista a secas) -> exit 2, no traceback" 2 run golden-diff --dir gsc
716
+ printf '{}\n' > gsc/golden.scrub.json
717
+ chk "scrub sin key rules (typo) -> exit 2, no degrada a cero reglas" 2 run golden-diff --dir gsc
718
+ # gate-check: editar el scrub es señal blanda visible
719
+ printf -- "diff --git a/fixtures/golden.scrub.json b/fixtures/golden.scrub.json\n--- a/fixtures/golden.scrub.json\n+++ b/fixtures/golden.scrub.json\n@@ -0,0 +1,1 @@\n+{ \"rules\": [ {\"pattern\": \".*\", \"replace\": \"\"} ] }\n" > scrub-edit.diff
720
+ run gate-check --diff scrub-edit.diff 2>/dev/null | grep -q "scrub" \
721
+ && { PASS=$((PASS+1)); echo " ok gate-check flaggea edicion de reglas de scrub (REVIEW)"; } \
722
+ || { FAIL=$((FAIL+1)); echo " FAIL edicion de scrub invisible para gate-check"; }
723
+ chk "edicion de scrub + --strict -> exit 1" 1 run gate-check --diff scrub-edit.diff --strict
724
+ printf -- "diff --git a/fixtures/golden.scrub.json b/fixtures/golden.scrub.json\n--- a/fixtures/golden.scrub.json\n+++ /dev/null\n@@ -1,1 +0,0 @@\n-{ \"rules\": [ {\"pattern\": \"x\", \"replace\": \"y\"} ] }\n" > scrub-del.diff
725
+ run gate-check --diff scrub-del.diff 2>/dev/null | grep -q "scrub" \
726
+ && { PASS=$((PASS+1)); echo " ok BORRAR el scrub tambien se flaggea (borrar reglas es editarlas)"; } \
727
+ || { FAIL=$((FAIL+1)); echo " FAIL borrado de scrub invisible"; }
728
+
729
+ echo "== T38 regression-check: cierre sin test = NARRADO, jamas medido (Find Bugs Once) =="
730
+ # fix SIN tocar tests: solo produccion cambiada
731
+ printf -- "diff --git a/src/main/java/App.java b/src/main/java/App.java\n--- a/src/main/java/App.java\n+++ b/src/main/java/App.java\n@@ -0,0 +1,1 @@\n+if (x != null) { return x.trim(); }\n" > fix-sin-test.diff
732
+ chk "cierre sin test -> NARRATED, advisory exit 0" 0 run regression-check --repo repo-a --fixed 2 --diff fix-sin-test.diff
733
+ run regression-check --repo repo-a --fixed 2 --diff fix-sin-test.diff 2>/dev/null | grep -q "NARRATED" \
734
+ && { PASS=$((PASS+1)); echo " ok verdict NARRATED visible (cierre narrado, no medido)"; } \
735
+ || { FAIL=$((FAIL+1)); echo " FAIL sin verdict NARRATED"; }
736
+ chk "cierre sin test + --strict -> exit 1" 1 run regression-check --repo repo-a --fixed 2 --diff fix-sin-test.diff --strict
737
+ # fix CON test que reproduce: MEASURED
738
+ printf -- "diff --git a/src/main/java/App.java b/src/main/java/App.java\n--- a/src/main/java/App.java\n+++ b/src/main/java/App.java\n@@ -0,0 +1,1 @@\n+if (x != null) { return x.trim(); }\ndiff --git a/src/test/java/AppTest.java b/src/test/java/AppTest.java\n--- a/src/test/java/AppTest.java\n+++ b/src/test/java/AppTest.java\n@@ -0,0 +1,2 @@\n+@Test\n+void testNullInputRegression() { assertNull(app.run(null)); }\n" > fix-con-test.diff
739
+ chk "cierre con test nuevo -> MEASURED exit 0 (aun con --strict)" 0 run regression-check --repo repo-a --fixed 2 --diff fix-con-test.diff --strict
740
+ # nada cerrado -> N/A, nada que exigir
741
+ chk "fixed 0 -> N/A exit 0 (aun con --strict)" 0 run regression-check --repo repo-a --fixed 0 --diff fix-sin-test.diff --strict
742
+ # gaming barato: UNA linea EN BLANCO en un test file NO es evidencia
743
+ printf -- "diff --git a/src/test/java/AppTest.java b/src/test/java/AppTest.java\n--- a/src/test/java/AppTest.java\n+++ b/src/test/java/AppTest.java\n@@ -0,0 +1,1 @@\n+\n" > fix-blank.diff
744
+ chk "linea en blanco en test file NO es evidencia -> NARRATED --strict exit 1" 1 \
745
+ run regression-check --repo repo-a --fixed 2 --diff fix-blank.diff --strict
746
+ # evidencia debil (linea de test sin testdef ni assert) -> MEASURED pero avisa
747
+ printf -- "diff --git a/src/test/java/AppTest.java b/src/test/java/AppTest.java\n--- a/src/test/java/AppTest.java\n+++ b/src/test/java/AppTest.java\n@@ -0,0 +1,1 @@\n+// nota\n" > fix-weak.diff
748
+ run regression-check --repo repo-a --fixed 2 --diff fix-weak.diff 2>/dev/null | grep -q "DEBIL" \
749
+ && { PASS=$((PASS+1)); echo " ok evidencia debil (sin testdef/assert) marcada para el ojo humano"; } \
750
+ || { FAIL=$((FAIL+1)); echo " FAIL evidencia debil invisible"; }
751
+
752
+ echo "== T39 procedencia de umbrales: requerimiento (config) vs default del kit =="
753
+ # cap DECLARADO en config (tests_red: 1 — siempre muerde con el junit rojo de repo-c)
754
+ printf '{ "defaults": { "acceptance_file": "ACCEPTANCE.md", "readiness_caps": {"tests_red": 1} },\n "repos": [ {"name":"solo","path":"repo-c","type":"python"} ], "integration": {"enabled": false} }\n' > c-caps.json
755
+ run init --config c-caps.json --out L-caps.json >/dev/null 2>&1
756
+ run snapshot --ledger L-caps.json --repo solo >/dev/null 2>&1
757
+ RDY=$(run readiness --ledger L-caps.json --json 2>/dev/null)
758
+ echo "$RDY" | "$PY" -c "
759
+ import json, sys
760
+ d = json.load(sys.stdin)
761
+ td = d['thresholds_declared']
762
+ ok = (d['cap_source'] == 'requerimiento (config)'
763
+ and td['readiness_caps'] == ['tests_red']
764
+ and td['coverage_threshold'] is False)
765
+ sys.exit(0 if ok else 1)" \
766
+ && { PASS=$((PASS+1)); echo " ok cap declarado en config etiquetado 'requerimiento (config)'"; } \
767
+ || { FAIL=$((FAIL+1)); echo " FAIL procedencia en readiness ($(echo "$RDY" | "$PY" -c 'import json,sys;d=json.load(sys.stdin);print(d.get("cap_source"),d.get("thresholds_declared"))' 2>/dev/null))"; }
768
+ run readiness --ledger L-caps.json 2>/dev/null | grep -q "requerimiento (config)" \
769
+ && { PASS=$((PASS+1)); echo " ok etiqueta de procedencia visible en el texto del cap"; } \
770
+ || { FAIL=$((FAIL+1)); echo " FAIL etiqueta de procedencia ausente en texto"; }
771
+ # el sandbox principal no declara caps: la lista de declarados queda vacia
772
+ run readiness --json 2>/dev/null | "$PY" -c "
773
+ import json, sys
774
+ d = json.load(sys.stdin)
775
+ sys.exit(0 if d['thresholds_declared']['readiness_caps'] == [] else 1)" \
776
+ && { PASS=$((PASS+1)); echo " ok sin caps declarados -> lista vacia (defaults = opinion del kit)"; } \
777
+ || { FAIL=$((FAIL+1)); echo " FAIL thresholds_declared del sandbox principal"; }
778
+ # simplicity: sin config -> todos default (aviso); con presupuesto CLI -> declarado
779
+ printf -- "diff --git a/src/A.java b/src/A.java\n--- a/src/A.java\n+++ b/src/A.java\n@@ -0,0 +1,1 @@\n+int x = 1;\n" > simp-tiny.diff
780
+ run simplicity-check --diff simp-tiny.diff 2>/dev/null | grep -q "defaults del kit" \
781
+ && { PASS=$((PASS+1)); echo " ok simplicity avisa: presupuestos = opinion del kit"; } \
782
+ || { FAIL=$((FAIL+1)); echo " FAIL sin aviso de presupuestos default"; }
783
+ run simplicity-check --diff simp-tiny.diff --max-lines-added 100 --json 2>/dev/null | "$PY" -c "
784
+ import json, sys
785
+ d = json.load(sys.stdin)
786
+ sys.exit(0 if d['budgets_declared'] == ['max_lines_added'] else 1)" \
787
+ && { PASS=$((PASS+1)); echo " ok presupuesto declarado por CLI listado en budgets_declared"; } \
788
+ || { FAIL=$((FAIL+1)); echo " FAIL budgets_declared no refleja el CLI"; }
789
+
790
+ echo "== T40 phase: FSM DERIVADA del ledger — el estado se computa, no se declara =="
791
+ mkdir -p repo-x
792
+ printf '{ "defaults": { "acceptance_file": "ACCEPTANCE.md", "qa_tools_order": ["code-review","judgment-day","improve"] },\n "repos": [ {"name":"fsm","path":"repo-x","type":"go"} ], "integration": {"enabled": false} }\n' > c-fsm.json
793
+ run init --config c-fsm.json --out L-fsm.json >/dev/null 2>&1
794
+ chk "ledger virgen -> plan" 0 run phase --ledger L-fsm.json --repo fsm --require plan
795
+ run snapshot --ledger L-fsm.json --repo fsm >/dev/null 2>&1
796
+ chk "snapshot medido sin QA -> build" 0 run phase --ledger L-fsm.json --repo fsm --require build
797
+ run log-step --ledger L-fsm.json --repo fsm --tool code-review --iteration 1 \
798
+ --gated-reported 2 --tests-passed true >/dev/null 2>&1
799
+ chk "pasos de QA sin converger -> qa" 0 run phase --ledger L-fsm.json --repo fsm --require qa
800
+ chk "pedir pr-ready con findings abiertos -> exit 1 (los hechos mandan)" 1 \
801
+ run phase --ledger L-fsm.json --repo fsm --require pr-ready
802
+ run escalate --ledger L-fsm.json --repo fsm --reason "duda de diseño" >/dev/null 2>&1
803
+ chk "escalacion abierta -> escalated (pisa todo)" 0 run phase --ledger L-fsm.json --repo fsm --require escalated
804
+ run resolve-escalation --ledger L-fsm.json --repo fsm --note ok >/dev/null 2>&1
805
+ for t in code-review judgment-day improve; do
806
+ run log-step --ledger L-fsm.json --repo fsm --tool $t --iteration 2 \
807
+ --gated-reported 0 --files-changed 0 --tests-passed true >/dev/null 2>&1
808
+ done
809
+ chk "convergido + limpio -> pr-ready" 0 run phase --ledger L-fsm.json --repo fsm --require pr-ready
810
+
811
+ echo "== T41 spike/*: codigo descartable por contrato — jamas pasa el gate de PR =="
812
+ # repo-x convergido (pr-ready por hechos, de T40) pero en rama spike/* -> DENEGADO
813
+ git init -q repo-x 2>/dev/null
814
+ git -C repo-x symbolic-ref HEAD refs/heads/spike/idea-loca
815
+ chk "pr-ready por hechos PERO rama spike/* -> exit 1" 1 \
816
+ run phase --ledger L-fsm.json --repo fsm --require pr-ready
817
+ run phase --ledger L-fsm.json --repo fsm --require pr-ready 2>/dev/null | grep -q "ADR con las lecciones" \
818
+ && { PASS=$((PASS+1)); echo " ok mensaje del contrato de spike visible (ADR, no PR)"; } \
819
+ || { FAIL=$((FAIL+1)); echo " FAIL sin mensaje de contrato de spike"; }
820
+ # misma rama consultada SIN --require: informa, no bloquea
821
+ chk "phase sin --require en rama spike -> exit 0 (informa, no gatea)" 0 \
822
+ run phase --ledger L-fsm.json --repo fsm
823
+ git -C repo-x symbolic-ref HEAD refs/heads/main
824
+ chk "rama normal -> pr-ready OK de nuevo" 0 \
825
+ run phase --ledger L-fsm.json --repo fsm --require pr-ready
826
+ # default sin --fixed: lee la suma de 'fixed' de la ultima iteracion del ledger
827
+ run init --config uscha.config.json >/dev/null 2>&1
828
+ run log-step --repo repo-a --tool code-review --iteration 1 --fixed 3 --tests-passed true >/dev/null 2>&1
829
+ run regression-check --repo repo-a --diff fix-sin-test.diff --json 2>/dev/null | "$PY" -c "
830
+ import json, sys
831
+ d = json.load(sys.stdin)
832
+ sys.exit(0 if d['fixed'] == 3 and d['verdict'] == 'NARRATED' else 1)" \
833
+ && { PASS=$((PASS+1)); echo " ok sin --fixed lee la ultima iteracion del ledger (fixed=3)"; } \
834
+ || { FAIL=$((FAIL+1)); echo " FAIL lookup de fixed en el ledger"; }
835
+
836
+ echo "== T42 doctor: diagnostico de la instalacion (flutter-doctor spirit) =="
837
+ chk "doctor en sandbox con config -> exit 0 (avisos no fallan)" 0 run doctor
838
+ run doctor --json 2>/dev/null | "$PY" -c "
839
+ import json, sys
840
+ d = json.load(sys.stdin)
841
+ sk = next(c for c in d['checks'] if c['title'].startswith('skills'))
842
+ ok = (d['errors'] == 0 and d['global_install'] is False
843
+ and sk['level'] == 'ok'
844
+ and any(c['title'].startswith('proyecto:') for c in d['checks'])
845
+ and any(c['title'].startswith('ACCEPTANCE') and c['level'] == 'ok'
846
+ for c in d['checks']))
847
+ sys.exit(0 if ok else 1)" \
848
+ && { PASS=$((PASS+1)); echo " ok doctor: 6/6 skills, install por proyecto, config y ACCEPTANCE leidos"; } \
849
+ || { FAIL=$((FAIL+1)); echo " FAIL doctor json"; }
850
+ # ledger corrupto = ERROR (no aviso): el doctor debe salir 1
851
+ "$PY" -c "open('QA-LEDGER.json','a',encoding='utf-8').write('{trunc')"
852
+ chk "doctor con ledger corrupto -> exit 1" 1 run doctor
853
+ run init --config uscha.config.json >/dev/null 2>&1
854
+
855
+ echo "== T43 rubric layer: agnostico — el grader.json se llena A MANO, sin LLM =="
856
+ cat > RUBRIC.md <<'EOF'
857
+ # RUBRIC — smoke
858
+ threshold: 0.80
859
+ ## Criterios
860
+ - [ ] RB-01 (peso 3) — errores sanos
861
+ - [ ] RB-02 (peso 1) — convenciones del repo
862
+ ## Criterios negativos
863
+ - [ ] RB-NEG-01 (peso 2) — comentarios que narran el cambio
864
+ EOF
865
+ chk "spec-check --rubric valida -> exit 0" 0 run spec-check --rubric RUBRIC.md
866
+ printf -- "# RUBRIC\n- [ ] RB-01 a\n- [ ] RB-1 b\n" > rub-dup.md
867
+ chk "IDs duplicados (RB-01 == RB-1) -> exit 1" 1 run spec-check --rubric rub-dup.md
868
+ printf -- "# RUBRIC\n- [ ] RB-01 a\n" > rub-nothr.md
869
+ chk "sin threshold -> exit 1" 1 run spec-check --rubric rub-nothr.md
870
+ # grade a mano: RB-01 pass con evidencia (3), RB-02 pass SIN evidencia (no puntua),
871
+ # negativo no aparece -> score 3/4 = 0.75 < 0.80 -> BELOW
872
+ cat > grader.json <<'EOF'
873
+ { "criteria": [
874
+ {"id": "RB-01", "verdict": "pass", "evidence": "src/x.py:42 — timeout+retry", "note": "ok"},
875
+ {"id": "RB-02", "verdict": "pass", "evidence": "", "note": "sin cita"},
876
+ {"id": "RB-NEG-01", "verdict": "pass", "evidence": "", "note": "no aparece"} ] }
877
+ EOF
878
+ chk "BELOW threshold sin gate declarado -> advisory exit 0" 0 \
879
+ run rubric-ingest --repo repo-a --report grader.json --rubric RUBRIC.md
880
+ run rubric-ingest --repo repo-a --report grader.json --rubric RUBRIC.md --json 2>/dev/null | "$PY" -c "
881
+ import json, sys
882
+ d = json.load(sys.stdin)
883
+ ok = (d['verdict'] == 'BELOW' and abs(d['score'] - 0.75) < 0.001
884
+ and d['unsupported'] == ['RB-2'] and d['gated'] is False)
885
+ sys.exit(0 if ok else 1)" \
886
+ && { PASS=$((PASS+1)); echo " ok evidence-or-nothing: pass sin cita no puntua (0.75 < 0.80)"; } \
887
+ || { FAIL=$((FAIL+1)); echo " FAIL contrato del grader"; }
888
+ run readiness 2>/dev/null | grep -q "rubrica repo-a" \
889
+ && { PASS=$((PASS+1)); echo " ok readiness muestra el grade como advisory (no dimension)"; } \
890
+ || { FAIL=$((FAIL+1)); echo " FAIL rubrica invisible en readiness"; }
891
+ # gate DECLARADO: below-threshold bloquea convergencia via ledger.
892
+ # Primero repo-a CONVERGE (ciclo limpio) — sin esto el check seria VACUO
893
+ # (converged ya sale 1 en un ledger virgen por 'no agent steps').
894
+ for t in code-review judgment-day improve; do
895
+ run log-step --repo repo-a --tool $t --iteration 9 \
896
+ --gated-reported 0 --files-changed 0 --tests-passed true >/dev/null 2>&1
897
+ done
898
+ chk "repo-a converge ANTES del gate (sanidad del fixture)" 0 run converged --repo repo-a
899
+ chk "BELOW con --gate -> exit 1" 1 \
900
+ run rubric-ingest --repo repo-a --report grader.json --rubric RUBRIC.md --gate
901
+ chk "convergencia bloqueada por rubric:grade gateado" 1 run converged --repo repo-a
902
+ # threshold malformado ('0.8.0') = ausente, no traceback
903
+ printf -- "# RUBRIC\nthreshold: 0.8.0\n- [ ] RB-01 x\n" > rub-badthr.md
904
+ chk "threshold malformado -> exit 1 sin traceback" 1 run spec-check --rubric rub-badthr.md
905
+ # dos veredictos para el mismo criterio (RB-01 y RB-1) = contrato roto
906
+ printf -- '{ "criteria": [ {"id": "RB-01", "verdict": "pass", "evidence": "x:1", "note": ""}, {"id": "RB-1", "verdict": "fail", "evidence": "", "note": ""} ] }\n' > grader-dup.json
907
+ chk "IDs duplicados en el reporte -> exit 1 (un veredicto por criterio)" 1 \
908
+ run rubric-ingest --repo repo-a --report grader-dup.json --rubric RUBRIC.md
909
+ # grade limpio (todo con evidencia, negativo sin matchear) -> PASS y limpia el gate
910
+ cat > grader-ok.json <<'EOF'
911
+ { "criteria": [
912
+ {"id": "RB-01", "verdict": "pass", "evidence": "src/x.py:42 — ok", "note": "ok"},
913
+ {"id": "RB-02", "verdict": "pass", "evidence": "src/y.py:7 — snake_case", "note": "ok"},
914
+ {"id": "RB-NEG-01", "verdict": "pass", "evidence": "", "note": "no aparece"} ] }
915
+ EOF
916
+ chk "grade limpio con --gate -> exit 0 (PASS)" 0 \
917
+ run rubric-ingest --repo repo-a --report grader-ok.json --rubric RUBRIC.md --gate
918
+ chk "gate limpio libera la convergencia (latest-wins)" 0 run converged --repo repo-a
919
+ # ID inexistente = error de contrato
920
+ printf -- '{ "criteria": [ {"id": "RB-99", "verdict": "pass", "evidence": "x:1", "note": ""} ] }\n' > grader-bad.json
921
+ chk "ID inexistente en la rubrica -> exit 1" 1 \
922
+ run rubric-ingest --repo repo-a --report grader-bad.json --rubric RUBRIC.md
923
+ # negativo CON evidencia resta peso: 4/4 - 2 = 2/4 = 0.5
924
+ cat > grader-neg.json <<'EOF'
925
+ { "criteria": [
926
+ {"id": "RB-01", "verdict": "pass", "evidence": "src/x.py:42 — ok", "note": "ok"},
927
+ {"id": "RB-02", "verdict": "pass", "evidence": "src/y.py:7 — ok", "note": "ok"},
928
+ {"id": "RB-NEG-01", "verdict": "fail", "evidence": "src/z.py:9 — 'now correctly...'", "note": "narra"} ] }
929
+ EOF
930
+ run rubric-ingest --repo repo-a --report grader-neg.json --rubric RUBRIC.md --json 2>/dev/null | "$PY" -c "
931
+ import json, sys
932
+ d = json.load(sys.stdin)
933
+ sys.exit(0 if d['verdict'] == 'BELOW' and abs(d['score'] - 0.5) < 0.001 else 1)" \
934
+ && { PASS=$((PASS+1)); echo " ok negativo con evidencia resta peso (4-2)/4 = 0.50"; } \
935
+ || { FAIL=$((FAIL+1)); echo " FAIL semantica de negativos"; }
936
+
937
+ echo "== T45 anti-ceremonia (1.25.0): readiness = veredicto unico; --verbose expande; gates colapsados =="
938
+ printf '{ "defaults": { "acceptance_file": "ACCEPTANCE.md" },\n "repos": [ {"name":"solo","path":"repo-c","type":"python"} ], "integration": {"enabled": false} }\n' > ac-cfg.json
939
+ run init --config ac-cfg.json --out L-ac.json >/dev/null
940
+ # default = 1 veredicto, SIN la tabla de rutina (dimensiones/by-repo son ceremonia)
941
+ run readiness --ledger L-ac.json 2>/dev/null | grep -q "^READINESS:" \
942
+ && { PASS=$((PASS+1)); echo " ok default emite el veredicto (READINESS:)"; } \
943
+ || { FAIL=$((FAIL+1)); echo " FAIL default no emite el veredicto"; }
944
+ run readiness --ledger L-ac.json 2>/dev/null | grep -q -- "--- dimensions" \
945
+ && { FAIL=$((FAIL+1)); echo " FAIL default filtra la tabla de dimensiones (ceremonia)"; } \
946
+ || { PASS=$((PASS+1)); echo " ok default colapsa las dimensiones"; }
947
+ run readiness --ledger L-ac.json 2>/dev/null | grep -q -- "--- by repo" \
948
+ && { FAIL=$((FAIL+1)); echo " FAIL default filtra el by-repo (ceremonia)"; } \
949
+ || { PASS=$((PASS+1)); echo " ok default colapsa el by-repo"; }
950
+ # --verbose expande el detalle
951
+ run readiness --ledger L-ac.json --verbose 2>/dev/null | grep -q -- "--- dimensions" \
952
+ && { PASS=$((PASS+1)); echo " ok --verbose expande las dimensiones"; } \
953
+ || { FAIL=$((FAIL+1)); echo " FAIL --verbose no expande las dimensiones"; }
954
+ run readiness --ledger L-ac.json --verbose 2>/dev/null | grep -q -- "--- by repo" \
955
+ && { PASS=$((PASS+1)); echo " ok --verbose expande el by-repo"; } \
956
+ || { FAIL=$((FAIL+1)); echo " FAIL --verbose no expande el by-repo"; }
957
+ # un gate BLOQUEANTE persistido aparece nombrado en la linea colapsada
958
+ run log-gate --repo solo --iteration 1 --kind simplicity --verdict fail --count 3 --ledger L-ac.json >/dev/null 2>&1
959
+ run readiness --ledger L-ac.json 2>/dev/null | grep -- "--- gates:" | grep -q "solo/gate:simplicity" \
960
+ && { PASS=$((PASS+1)); echo " ok gate bloqueante colapsado y nombrado"; } \
961
+ || { FAIL=$((FAIL+1)); echo " FAIL gate bloqueante no nombrado en la linea de gates"; }
962
+ # gate limpio (latest-wins) -> la linea reporta ninguno bloqueando
963
+ run log-gate --repo solo --iteration 2 --kind simplicity --verdict pass --ledger L-ac.json >/dev/null 2>&1
964
+ run readiness --ledger L-ac.json 2>/dev/null | grep -- "--- gates:" | grep -q "ninguno bloqueando" \
965
+ && { PASS=$((PASS+1)); echo " ok gate limpio libera la linea de gates (latest-wins)"; } \
966
+ || { FAIL=$((FAIL+1)); echo " FAIL gate limpio no libera la linea de gates"; }
967
+ # --json expone gates[] (aditivo: presentacion sobre hechos, no recomputa el KPI)
968
+ run readiness --ledger L-ac.json --json 2>/dev/null | "$PY" -c "
969
+ import json, sys
970
+ d = json.load(sys.stdin)
971
+ g = d.get('gates')
972
+ ok = (isinstance(g, list) and len(g) == 1 and g[0]['tool'] == 'gate:simplicity'
973
+ and g[0]['blocking'] is False)
974
+ sys.exit(0 if ok else 1)" \
975
+ && { PASS=$((PASS+1)); echo " ok --json expone gates[] aditivo (latest limpio, blocking False)"; } \
976
+ || { FAIL=$((FAIL+1)); echo " FAIL contrato gates[] en --json"; }
977
+
978
+ echo "== T46 REUSE-FIRST (1.26.0): waste-check Type-1/2 clon-vs-repo, advisory-first, determinista =="
979
+ mkdir -p wrepo/util
980
+ cat > wrepo/util/money.py <<'EOF'
981
+ def compute_total(items, rate):
982
+ subtotal = sum(x.price for x in items)
983
+ taxed = subtotal * (1 + rate)
984
+ shipping = 5 if taxed < 100 else 0
985
+ grand = round(taxed + shipping, 2)
986
+ return grand
987
+ EOF
988
+ # LEAN: archivo nuevo, codigo unico (no clona nada del repo)
989
+ cat > lean.diff <<'EOF'
990
+ diff --git a/service/report.py b/service/report.py
991
+ new file mode 100644
992
+ --- /dev/null
993
+ +++ b/service/report.py
994
+ @@ -0,0 +1,6 @@
995
+ +def build_report(rows, header):
996
+ + lines = [header.upper(), "----------"]
997
+ + for entry in rows:
998
+ + lines.append(entry.render_line())
999
+ + joined = "\n".join(lines)
1000
+ + return joined
1001
+ EOF
1002
+ # CLON VS REPO: archivo nuevo que reimplementa wrepo/util/money.py exacto
1003
+ cat > clone.diff <<'EOF'
1004
+ diff --git a/service/checkout.py b/service/checkout.py
1005
+ new file mode 100644
1006
+ --- /dev/null
1007
+ +++ b/service/checkout.py
1008
+ @@ -0,0 +1,6 @@
1009
+ +def compute_total(items, rate):
1010
+ + subtotal = sum(x.price for x in items)
1011
+ + taxed = subtotal * (1 + rate)
1012
+ + shipping = 5 if taxed < 100 else 0
1013
+ + grand = round(taxed + shipping, 2)
1014
+ + return grand
1015
+ EOF
1016
+ # CLON INTERNO: el mismo bloque repetido dentro del propio diff (no en el repo)
1017
+ cat > internal.diff <<'EOF'
1018
+ diff --git a/service/dup.py b/service/dup.py
1019
+ new file mode 100644
1020
+ --- /dev/null
1021
+ +++ b/service/dup.py
1022
+ @@ -0,0 +1,12 @@
1023
+ +def parse_alpha(text, mode):
1024
+ + tokens = text.split(mode)
1025
+ + cleaned = [t.strip() for t in tokens]
1026
+ + filtered = [t for t in cleaned if t]
1027
+ + counted = len(filtered)
1028
+ + return counted, filtered
1029
+ +def parse_beta(text, mode):
1030
+ + tokens = text.split(mode)
1031
+ + cleaned = [t.strip() for t in tokens]
1032
+ + filtered = [t for t in cleaned if t]
1033
+ + counted = len(filtered)
1034
+ + return counted, filtered
1035
+ EOF
1036
+ # CLON EN TEST: excluido (como simplicity) -> no cuenta
1037
+ cat > clone-test.diff <<'EOF'
1038
+ diff --git a/tests/test_checkout.py b/tests/test_checkout.py
1039
+ new file mode 100644
1040
+ --- /dev/null
1041
+ +++ b/tests/test_checkout.py
1042
+ @@ -0,0 +1,6 @@
1043
+ +def compute_total(items, rate):
1044
+ + subtotal = sum(x.price for x in items)
1045
+ + taxed = subtotal * (1 + rate)
1046
+ + shipping = 5 if taxed < 100 else 0
1047
+ + grand = round(taxed + shipping, 2)
1048
+ + return grand
1049
+ EOF
1050
+ chk "lean.diff -> exit 0 (advisory)" 0 run waste-check --diff lean.diff --repo-root wrepo
1051
+ run waste-check --diff lean.diff --repo-root wrepo --json 2>/dev/null | "$PY" -c "
1052
+ import json, sys
1053
+ d = json.load(sys.stdin)
1054
+ sys.exit(0 if d['verdict'] == 'LEAN' and d['metrics']['dup_windows_vs_repo'] == 0 else 1)" \
1055
+ && { PASS=$((PASS+1)); echo " ok codigo unico -> LEAN, 0 clones vs repo"; } \
1056
+ || { FAIL=$((FAIL+1)); echo " FAIL lean mal clasificado"; }
1057
+ chk "clon vs repo -> exit 0 SIN gate (advisory-first)" 0 run waste-check --diff clone.diff --repo-root wrepo
1058
+ run waste-check --diff clone.diff --repo-root wrepo --json 2>/dev/null | "$PY" -c "
1059
+ import json, sys
1060
+ d = json.load(sys.stdin)
1061
+ ok = (d['verdict'] == 'WASTEFUL' and d['metrics']['dup_windows_vs_repo'] >= 1
1062
+ and d['gate'] is False and any('money.py' in f for f in d['flags']))
1063
+ sys.exit(0 if ok else 1)" \
1064
+ && { PASS=$((PASS+1)); echo " ok clon vs repo -> WASTEFUL + flag nombra el original (money.py)"; } \
1065
+ || { FAIL=$((FAIL+1)); echo " FAIL clon vs repo no detectado"; }
1066
+ chk "clon vs repo con --gate -> exit 1 (BLOCKER declarado)" 1 run waste-check --diff clone.diff --repo-root wrepo --gate
1067
+ run waste-check --diff internal.diff --repo-root wrepo --json 2>/dev/null | "$PY" -c "
1068
+ import json, sys
1069
+ d = json.load(sys.stdin)
1070
+ sys.exit(0 if d['metrics']['dup_windows_internal'] >= 1 else 1)" \
1071
+ && { PASS=$((PASS+1)); echo " ok clon interno detectado (dup_windows_internal >= 1)"; } \
1072
+ || { FAIL=$((FAIL+1)); echo " FAIL clon interno no detectado"; }
1073
+ run waste-check --diff clone-test.diff --repo-root wrepo --json 2>/dev/null | "$PY" -c "
1074
+ import json, sys
1075
+ d = json.load(sys.stdin)
1076
+ sys.exit(0 if d['verdict'] == 'LEAN' and d['metrics']['dup_windows_vs_repo'] == 0 else 1)" \
1077
+ && { PASS=$((PASS+1)); echo " ok clon en archivo de test EXCLUIDO (como simplicity)"; } \
1078
+ || { FAIL=$((FAIL+1)); echo " FAIL test no excluido del waste-check"; }
1079
+ # SELF-MATCH: un diff que TOCA util/money.py y le agrega una copia de su PROPIO bloque
1080
+ # no debe matchearse contra si mismo (el archivo tocado se excluye del escaneo del repo)
1081
+ cat > selfmod.diff <<'EOF'
1082
+ diff --git a/util/money.py b/util/money.py
1083
+ --- a/util/money.py
1084
+ +++ b/util/money.py
1085
+ @@ -6,0 +7,6 @@
1086
+ +def compute_total(items, rate):
1087
+ + subtotal = sum(x.price for x in items)
1088
+ + taxed = subtotal * (1 + rate)
1089
+ + shipping = 5 if taxed < 100 else 0
1090
+ + grand = round(taxed + shipping, 2)
1091
+ + return grand
1092
+ EOF
1093
+ run waste-check --diff selfmod.diff --repo-root wrepo --json 2>/dev/null | "$PY" -c "
1094
+ import json, sys
1095
+ d = json.load(sys.stdin)
1096
+ sys.exit(0 if d['metrics']['dup_windows_vs_repo'] == 0 else 1)" \
1097
+ && { PASS=$((PASS+1)); echo " ok archivo tocado excluido del escaneo (no auto-match)"; } \
1098
+ || { FAIL=$((FAIL+1)); echo " FAIL self-match: el archivo tocado se comparo consigo mismo"; }
1099
+ # determinismo: misma entrada -> mismo score (sin azar, sin red, sin LLM)
1100
+ W1=$(run waste-check --diff clone.diff --repo-root wrepo --json 2>/dev/null | "$PY" -c "import json,sys;print(json.load(sys.stdin)['score'])")
1101
+ W2=$(run waste-check --diff clone.diff --repo-root wrepo --json 2>/dev/null | "$PY" -c "import json,sys;print(json.load(sys.stdin)['score'])")
1102
+ [ "$W1" = "$W2" ] && [ -n "$W1" ] \
1103
+ && { PASS=$((PASS+1)); echo " ok determinista (score $W1 estable)"; } \
1104
+ || { FAIL=$((FAIL+1)); echo " FAIL no determinista ($W1 vs $W2)"; }
1105
+ # log-gate --kind waste persiste y entra al rollup de readiness (1.25.0)
1106
+ run log-gate --repo solo --iteration 3 --kind waste --verdict fail --count 2 --ledger L-ac.json >/dev/null 2>&1
1107
+ run readiness --ledger L-ac.json 2>/dev/null | grep -- "--- gates:" | grep -q "gate:waste" \
1108
+ && { PASS=$((PASS+1)); echo " ok gate:waste persistido aparece en el veredicto unico"; } \
1109
+ || { FAIL=$((FAIL+1)); echo " FAIL gate:waste no entra al rollup de readiness"; }
1110
+
1111
+ echo "== T47 FTY (1.27.0): first-time yield pasivo en summary, informativo, no gatea =="
1112
+ printf '{ "defaults": {"qa_tools_order":["code-review"]},\n "repos": [ {"name":"fa","path":"repo-c","type":"python"},{"name":"fb","path":"repo-d","type":"python"} ], "integration": {"enabled": false} }\n' > fty-cfg.json
1113
+ run init --config fty-cfg.json --out L-fty.json >/dev/null
1114
+ # fa: limpio al ciclo 1 (first-time). fb: necesito 2 ciclos (no first-time).
1115
+ run log-step --repo fa --tool code-review --iteration 1 --gated-reported 0 --tests-passed true --ledger L-fty.json >/dev/null 2>&1
1116
+ run log-step --repo fb --tool code-review --iteration 1 --gated-reported 3 --tests-passed true --ledger L-fty.json >/dev/null 2>&1
1117
+ run log-step --repo fb --tool code-review --iteration 2 --gated-reported 0 --tests-passed true --ledger L-fty.json >/dev/null 2>&1
1118
+ run summary --ledger L-fty.json --json 2>/dev/null | "$PY" -c "
1119
+ import json, sys
1120
+ f = json.load(sys.stdin)['first_time_yield']
1121
+ ok = (f['repos_through_qa'] == 2 and f['repos_first_time'] == 1 and f['pct'] == 50.0)
1122
+ sys.exit(0 if ok else 1)" \
1123
+ && { PASS=$((PASS+1)); echo " ok FTY 50% (fa limpio al 1er ciclo, fb necesito 2)"; } \
1124
+ || { FAIL=$((FAIL+1)); echo " FAIL FTY mal computado"; }
1125
+ run summary --ledger L-fty.json 2>/dev/null | grep -q "first-time yield: 50.0%" \
1126
+ && { PASS=$((PASS+1)); echo " ok FTY visible en el texto del summary"; } \
1127
+ || { FAIL=$((FAIL+1)); echo " FAIL FTY no aparece en summary"; }
1128
+ # una escalacion (aunque el repo converja) lo saca del first-time yield
1129
+ run escalate --repo fa --reason "design doubt" --ledger L-fty.json >/dev/null 2>&1
1130
+ run summary --ledger L-fty.json --json 2>/dev/null | "$PY" -c "
1131
+ import json, sys
1132
+ f = json.load(sys.stdin)['first_time_yield']
1133
+ sys.exit(0 if f['repos_first_time'] == 0 and f['pct'] == 0.0 else 1)" \
1134
+ && { PASS=$((PASS+1)); echo " ok escalacion saca al repo del first-time yield"; } \
1135
+ || { FAIL=$((FAIL+1)); echo " FAIL escalacion no afecta FTY"; }
1136
+
1137
+ echo "== T48 pit-check: mutation gate desde un mutations.xml (efectividad, no coverage) =="
1138
+ cat > mutations.xml <<'EOF'
1139
+ <mutations>
1140
+ <mutation detected="true" status="KILLED"><sourceFile>a.py</sourceFile></mutation>
1141
+ <mutation detected="true" status="KILLED"><sourceFile>a.py</sourceFile></mutation>
1142
+ <mutation detected="true" status="KILLED"><sourceFile>a.py</sourceFile></mutation>
1143
+ <mutation detected="true" status="SURVIVED"><sourceFile>a.py</sourceFile></mutation>
1144
+ <mutation detected="false" status="SURVIVED"><sourceFile>b.py</sourceFile></mutation>
1145
+ <mutation detected="false" status="NO_COVERAGE"><sourceFile>b.py</sourceFile></mutation>
1146
+ <mutation detected="false" status="NON_VIABLE"><sourceFile>c.py</sourceFile></mutation>
1147
+ <mutation detected="false" status="RUN_ERROR"><sourceFile>c.py</sourceFile></mutation>
1148
+ </mutations>
1149
+ EOF
1150
+ # total = killed(4: 3 KILLED + 1 detected-override) + survived(1) + no_cov(1) = 6
1151
+ # NON_VIABLE + RUN_ERROR excluidos del denominador -> excluded=2
1152
+ # mutation_score = 100*4/6 = 66.7 ; test_strength = 100*4/(4+1) = 80.0
1153
+ chk "score 66.7 >= min-score 60 -> PASS exit 0" 0 run pit-check --report mutations.xml --min-score 60
1154
+ chk "score 66.7 < min-score 70 -> BELOW-GATE exit 1" 1 run pit-check --report mutations.xml --min-score 70
1155
+ run pit-check --report mutations.xml --min-score 60 --json 2>/dev/null | "$PY" -c "
1156
+ import json, sys
1157
+ m = json.load(sys.stdin)['metrics']
1158
+ ok = (m['total'] == 6 and m['killed'] == 4 and m['survived'] == 1
1159
+ and m['no_coverage'] == 1 and m['excluded'] == 2
1160
+ and abs(m['mutation_score'] - 66.7) < 0.05
1161
+ and abs(m['test_strength'] - 80.0) < 0.05)
1162
+ sys.exit(0 if ok else 1)" \
1163
+ && { PASS=$((PASS+1)); echo " ok NON_VIABLE/RUN_ERROR fuera del denominador; detected=true cuenta como killed"; } \
1164
+ || { FAIL=$((FAIL+1)); echo " FAIL metricas del mutation report mal computadas"; }
1165
+ chk "report inexistente -> exit 2 (no evidencia)" 2 run pit-check --report no-such.xml --min-score 60
1166
+
1167
+ echo "== T49 check-coverage: gate de umbral (OK / BELOW / sin report = fail-closed) =="
1168
+ printf '{ "defaults": { "acceptance_file": "ACCEPTANCE.md" },\n "repos": [ {"name":"cov","path":"covrepo","type":"python"}, {"name":"nocov","path":"nocovrepo","type":"python"} ], "integration": {"enabled": false} }\n' > cc-cfg.json
1169
+ mkdir -p covrepo nocovrepo
1170
+ # Cobertura: line-rate 0.85 -> 85% (lines-valid/lines-covered coherentes con line-rate)
1171
+ cat > covrepo/coverage.xml <<'EOF'
1172
+ <?xml version="1.0"?>
1173
+ <coverage lines-valid="20" lines-covered="17" line-rate="0.85" version="7.4"></coverage>
1174
+ EOF
1175
+ run init --config cc-cfg.json --out L-cc.json >/dev/null
1176
+ chk "coverage 85% >= threshold 60 -> OK exit 0" 0 run check-coverage --repo cov --threshold 60 --ledger L-cc.json
1177
+ chk "coverage 85% < threshold 90 -> BELOW exit 1" 1 run check-coverage --repo cov --threshold 90 --ledger L-cc.json
1178
+ chk "sin report de coverage -> fail-closed exit 1" 1 run check-coverage --repo nocov --threshold 60 --ledger L-cc.json
1179
+
1180
+ echo "== T50 rebuild: baseline escribe la firma; compare puntua COVERS / DIVERGE =="
1181
+ mkdir -p rbrepo/src rbrepo/reports
1182
+ cat > rbrepo/src/mod.py <<'EOF'
1183
+ def add(a, b):
1184
+ return a + b
1185
+ def mul(a, b):
1186
+ return a * b
1187
+ EOF
1188
+ cat > rbrepo/coverage.xml <<'EOF'
1189
+ <?xml version="1.0"?>
1190
+ <coverage lines-valid="10" lines-covered="8" line-rate="0.8" version="7.4"></coverage>
1191
+ EOF
1192
+ cat > rbrepo/reports/junit.xml <<'EOF'
1193
+ <testsuites><testsuite name="S" tests="5" failures="0" errors="0" skipped="0"/></testsuites>
1194
+ EOF
1195
+ printf -- "# ACCEPTANCE\n\n- [x] uno\n- [x] dos\n" > ACCEPTANCE-rb.md
1196
+ printf '{ "defaults": { "acceptance_file": "ACCEPTANCE-rb.md" },\n "repos": [ {"name":"rb","path":"rbrepo","type":"python"} ], "integration": {"enabled": false} }\n' > rb-cfg.json
1197
+ # baseline: escribe la firma
1198
+ chk "baseline escribe REBUILD-BASELINE -> exit 0" 0 run rebuild --mode baseline --config rb-cfg.json --out RB.json
1199
+ "$PY" -c "import json,sys; d=json.load(open('RB.json',encoding='utf-8')); sys.exit(0 if 'rb' in d['repos'] and d['repos']['rb']['tests']['total']==5 else 1)" \
1200
+ && { PASS=$((PASS+1)); echo " ok la firma baseline capturo el repo (tests total=5)"; } \
1201
+ || { FAIL=$((FAIL+1)); echo " FAIL baseline no capturo la firma esperada"; }
1202
+ # compare sobre el MISMO arbol -> todas las dimensiones 1.0 -> COVERS
1203
+ chk "compare mismo arbol -> COVERS exit 0" 0 run rebuild --mode compare --baseline RB.json --json
1204
+ run rebuild --mode compare --baseline RB.json --json 2>/dev/null | "$PY" -c "
1205
+ import json, sys
1206
+ d = json.load(sys.stdin)
1207
+ sys.exit(0 if d['verdict'] == 'COVERS' and d['dimensions']['tests'] == 1.0 else 1)" \
1208
+ && { PASS=$((PASS+1)); echo " ok arbol sin cambios -> COVERS (tests dim 1.0)"; } \
1209
+ || { FAIL=$((FAIL+1)); echo " FAIL compare del mismo arbol no da COVERS"; }
1210
+ # el arbol 'regenerado' rompe tests -> la dimension dominante cae -> NO COVERS + gap
1211
+ cat > rbrepo/reports/junit.xml <<'EOF'
1212
+ <testsuites><testsuite name="S" tests="5" failures="4" errors="0" skipped="0"/></testsuites>
1213
+ EOF
1214
+ chk "compare con tests que fallan -> exit 1 (no COVERS)" 1 run rebuild --mode compare --baseline RB.json --json
1215
+ run rebuild --mode compare --baseline RB.json --json 2>/dev/null | "$PY" -c "
1216
+ import json, sys
1217
+ d = json.load(sys.stdin)
1218
+ ok = (d['verdict'] != 'COVERS' and d['dimensions']['tests'] < 0.5
1219
+ and any('fail' in g for g in d['gaps']))
1220
+ sys.exit(0 if ok else 1)" \
1221
+ && { PASS=$((PASS+1)); echo " ok tests rotos al regenerar -> DIVERGE/PARTIAL + gap reportado"; } \
1222
+ || { FAIL=$((FAIL+1)); echo " FAIL compare no detecto la divergencia de tests"; }
1223
+
1224
+ echo "== T44 sync quintuple de version: VERSION = config = plugin.json = marketplace.json =="
1225
+ "$PY" -c "
1226
+ import json, sys, os, io
1227
+ kit = os.path.dirname(os.path.dirname(os.path.dirname(sys.argv[1]))) # <kit>/.claude/skills/x -> <kit>
1228
+ repo = os.path.dirname(kit)
1229
+ v_file = io.open(os.path.join(kit, 'VERSION'), encoding='utf-8').read().split()[-1]
1230
+ v_cfg = json.load(io.open(os.path.join(kit, 'uscha.config.json'), encoding='utf-8'))['version']
1231
+ v_plug = json.load(io.open(os.path.join(kit, '.claude-plugin', 'plugin.json'), encoding='utf-8'))['version']
1232
+ mk = json.load(io.open(os.path.join(repo, '.claude-plugin', 'marketplace.json'), encoding='utf-8'))
1233
+ v_mkt = mk['plugins'][0]['version']
1234
+ vs = {v_file, v_cfg, v_plug, v_mkt}
1235
+ print(' versiones:', v_file, v_cfg, v_plug, v_mkt)
1236
+ sys.exit(0 if len(vs) == 1 else 1)" "$(dirname "$QL")" \
1237
+ && { PASS=$((PASS+1)); echo " ok las cuatro fuentes de version coinciden"; } \
1238
+ || { FAIL=$((FAIL+1)); echo " FAIL drift de version entre VERSION/config/plugin/marketplace"; }
1239
+
1240
+ echo "== T51 freshness (1.31.0): reporte JUnit mas viejo que el codigo = STALE -> AC UNMEASURED =="
1241
+ mkdir -p repo-fresh/reports
1242
+ printf 'def alta():\n return True\n' > repo-fresh/alta.py
1243
+ cat > repo-fresh/reports/junit.xml <<'EOF'
1244
+ <?xml version="1.0"?>
1245
+ <testsuites><testsuite name="pytest" tests="1" failures="0" errors="0" skipped="0">
1246
+ <testcase classname="tests.test_flow" name="test_ac1_alta_ok"/>
1247
+ </testsuite></testsuites>
1248
+ EOF
1249
+ printf -- "# ACCEPTANCE\n\n- [x] AC-01 alta\n" > acc-fresh.md
1250
+ printf '{ "defaults": { "acceptance_file": "acc-fresh.md" },\n "repos": [ {"name":"fresh","path":"repo-fresh","type":"python"} ], "integration": {"enabled": false} }\n' > fresh.json
1251
+ run init --config fresh.json --out L-fresh.json >/dev/null 2>&1
1252
+ # FRESCO: reporte mas nuevo que la fuente -> AC-1 cierra medido, sin stale
1253
+ touch -t 202601010800 repo-fresh/alta.py
1254
+ touch -t 202601010900 repo-fresh/reports/junit.xml
1255
+ run readiness --ledger L-fresh.json --json 2>/dev/null | "$PY" -c "
1256
+ import json, sys
1257
+ a = json.load(sys.stdin)['acceptance']
1258
+ sys.exit(0 if a['measured_closed'] == ['AC-1'] and a['stale_reports'] == [] else 1)" \
1259
+ && { PASS=$((PASS+1)); echo " ok reporte fresco: AC-1 cierra medido, stale_reports vacio"; } \
1260
+ || { FAIL=$((FAIL+1)); echo " FAIL fresco no cierra o reporta stale de mas (falso positivo)"; }
1261
+ # STALE: la fuente pasa a ser mas nueva que el reporte -> reporte descartado
1262
+ touch -t 202601011200 repo-fresh/alta.py
1263
+ run readiness --ledger L-fresh.json --json 2>/dev/null | "$PY" -c "
1264
+ import json, sys
1265
+ a = json.load(sys.stdin)['acceptance']
1266
+ sys.exit(0 if a['measured_closed'] == [] and len(a['stale_reports']) == 1 else 1)" \
1267
+ && { PASS=$((PASS+1)); echo " ok codigo mas nuevo: reporte STALE descartado, AC-1 UNMEASURED"; } \
1268
+ || { FAIL=$((FAIL+1)); echo " FAIL stale no descartado (falso-verde por evidencia vieja)"; }
1269
+ run readiness --ledger L-fresh.json 2>/dev/null | grep -q "STALE descartados" \
1270
+ && { PASS=$((PASS+1)); echo " ok advisory de reportes STALE visible en la vista default"; } \
1271
+ || { FAIL=$((FAIL+1)); echo " FAIL sin advisory de STALE"; }
1272
+
1273
+ echo "== T52 gate de doc-version (1.31.0): READMEs (marcador uscha:version) = VERSION =="
1274
+ "$PY" -c "
1275
+ import sys, os, io, re
1276
+ skdir = sys.argv[1] # <kit>/.claude/skills/uscha-devloop
1277
+ kit = os.path.dirname(os.path.dirname(os.path.dirname(skdir)))
1278
+ repo = os.path.dirname(kit)
1279
+ ver = io.open(os.path.join(kit, 'VERSION'), encoding='utf-8').read().split()[-1]
1280
+ docs = [os.path.join(repo, 'README.md'), os.path.join(kit, 'README.md')]
1281
+ rx = re.compile(r'v?(\d+\.\d+\.\d+)')
1282
+ bad = []
1283
+ for d in docs:
1284
+ marked = [l for l in io.open(d, encoding='utf-8') if 'uscha:version' in l]
1285
+ if not marked:
1286
+ bad.append(d + ': SIN marcador uscha:version'); continue
1287
+ m = rx.search(marked[0])
1288
+ got = m.group(1) if m else None
1289
+ if got != ver:
1290
+ bad.append(d + ': marcador dice ' + str(got) + ' != VERSION ' + ver)
1291
+ print(' VERSION:', ver, '· READMEs marcados y en sync:', len(docs) - len(bad))
1292
+ for b in bad: print(' ', b)
1293
+ sys.exit(1 if bad else 0)" "$(dirname "$QL")" \
1294
+ && { PASS=$((PASS+1)); echo " ok los READMEs declaran la version actual (doc drift bloqueado)"; } \
1295
+ || { FAIL=$((FAIL+1)); echo " FAIL doc-version drift: un README no coincide con VERSION"; }
1296
+
1297
+ echo "== T53 dashboard (1.32.0): contrato mirador desde el ledger, truth-pass (null sin fuente) =="
1298
+ mkdir -p repo-mir/reports docs/adr-mir
1299
+ printf 'def a():\n return 1\n' > repo-mir/x.py
1300
+ printf -- "# ADR-001 Append-only\n\nStatus: accepted\n" > docs/adr-mir/ADR-001.md
1301
+ printf -- "# ADR-002 Rollback\n\nStatus: proposed\n" > docs/adr-mir/ADR-002.md
1302
+ printf -- "# ACCEPTANCE\n\n- [x] AC-01 alta\n" > acc-mir.md
1303
+ printf '{ "defaults": { "acceptance_file": "acc-mir.md" },\n "repos": [ {"name":"backend-api","path":"repo-mir","type":"python"} ], "integration": {"enabled": false} }\n' > mir.json
1304
+ run init --config mir.json --out L-mir.json >/dev/null 2>&1
1305
+ run dashboard --ledger L-mir.json --adr-dir docs/adr-mir --json 2>/dev/null | "$PY" -c "
1306
+ import json, sys
1307
+ d = json.load(sys.stdin)
1308
+ keys = ['project','generated','readiness','subscores','phases','specs','adrs','inv','capas','loops','snapshots','evidence']
1309
+ assert all(k in d for k in keys), 'faltan claves: ' + str([k for k in keys if k not in d])
1310
+ assert isinstance(d['readiness']['score'], (int, float)), 'readiness.score'
1311
+ assert d['specs'] == [] and d['capas'] == [], 'specs/capas deben ser [] (truth-pass, sin fuente)'
1312
+ assert len(d['adrs']) == 2 and d['adrs'][0]['status'] == 'done' and d['adrs'][1]['status'] == 'prog', 'adrs glob'
1313
+ assert d['snapshots'] == [], 'snapshots vacio antes de --record'
1314
+ assert len(d['phases']) == 8 and len(d['inv']) == 7, 'esqueleto phases(8)/inv(7)'
1315
+ sys.exit(0)" \
1316
+ && { PASS=$((PASS+1)); echo " ok contrato completo; specs/capas []; adrs del glob; snapshots vacio pre-record"; } \
1317
+ || { FAIL=$((FAIL+1)); echo " FAIL contrato dashboard mal formado"; }
1318
+ DS=$(run dashboard --ledger L-mir.json --adr-dir docs/adr-mir --json 2>/dev/null | "$PY" -c "import json,sys;print(json.load(sys.stdin)['readiness']['score'])")
1319
+ RS=$(run readiness --ledger L-mir.json --json 2>/dev/null | "$PY" -c "import json,sys;print(json.load(sys.stdin)['score'])")
1320
+ [ "$DS" = "$RS" ] \
1321
+ && { PASS=$((PASS+1)); echo " ok readiness del dashboard == readiness --json ($DS, reuso verbatim sin drift)"; } \
1322
+ || { FAIL=$((FAIL+1)); echo " FAIL drift readiness dashboard($DS) vs readiness($RS)"; }
1323
+ run readiness --ledger L-mir.json --record >/dev/null 2>&1
1324
+ run dashboard --ledger L-mir.json --adr-dir docs/adr-mir --json 2>/dev/null | "$PY" -c "
1325
+ import json, sys
1326
+ s = json.load(sys.stdin)['snapshots']
1327
+ sys.exit(0 if len(s) == 1 and isinstance(s[0]['readiness'], (int, float)) and 'date' in s[0] else 1)" \
1328
+ && { PASS=$((PASS+1)); echo " ok readiness --record puebla el time-lapse (add-on prospectivo)"; } \
1329
+ || { FAIL=$((FAIL+1)); echo " FAIL time-lapse no se poblo tras --record"; }
1330
+ # inv mapea el gate persistido por su kind REAL (pit-check, no 'pit'): sin este check
1331
+ # un typo de kind deja el invariante en null en silencio (regresion muda).
1332
+ run log-gate --ledger L-mir.json --repo backend-api --iteration 1 --kind pit-check --verdict fail --count 2 >/dev/null 2>&1
1333
+ run log-gate --ledger L-mir.json --repo backend-api --iteration 1 --kind simplicity --verdict pass >/dev/null 2>&1
1334
+ run dashboard --ledger L-mir.json --adr-dir docs/adr-mir --json 2>/dev/null | "$PY" -c "
1335
+ import json, sys
1336
+ inv = {i['name']: i['status'] for i in json.load(sys.stdin)['inv']}
1337
+ sys.exit(0 if inv.get('Tests efectivos') == 'miss' and inv.get('Simplicidad') == 'ok' else 1)" \
1338
+ && { PASS=$((PASS+1)); echo " ok inv mapea el gate por su kind real (pit-check->Tests efectivos miss, simplicity->ok)"; } \
1339
+ || { FAIL=$((FAIL+1)); echo " FAIL inv no mapea el gate persistido (kind mal escrito?)"; }
1340
+
1341
+ echo "== T54 telemetry-extract (1.33.0): transcript CC -> sidecar (vendor adapter, FUERA del engine) =="
1342
+ EXTRACT="$(dirname "$(dirname "$QL")")/uscha-mirador/telemetry-extract.py"
1343
+ mkdir -p tele
1344
+ cat > tele/t.jsonl <<'EOF'
1345
+ {"type":"assistant","timestamp":"2026-07-05T20:00:00Z","message":{"model":"claude-opus-4-8","usage":{"input_tokens":10000,"cache_read_input_tokens":40000,"output_tokens":3000}}}
1346
+ {"type":"assistant","timestamp":"2026-07-05T20:10:00Z","message":{"model":"claude-haiku-4-5","usage":{"input_tokens":4000,"output_tokens":1500}}}
1347
+ {bad json se saltea}
1348
+ EOF
1349
+ "$PY" "$EXTRACT" tele/t.jsonl --sidecar tele/telemetry.jsonl >/dev/null 2>&1
1350
+ "$PY" -c "
1351
+ import json, sys
1352
+ d = json.loads(open('tele/telemetry.jsonl', encoding='utf-8').readline())
1353
+ ok = (d['tokens_in'] == 54000 and d['tokens_out'] == 4500 and d['ms'] == 600000
1354
+ and len(d['by_model']) == 2
1355
+ and {m['model'] for m in d['by_model']} == {'claude-opus-4-8', 'claude-haiku-4-5'})
1356
+ sys.exit(0 if ok else 1)" \
1357
+ && { PASS=$((PASS+1)); echo " ok suma tokens (input+cache) + wall time + by_model desde el transcript"; } \
1358
+ || { FAIL=$((FAIL+1)); echo " FAIL extractor no resume bien el transcript CC"; }
1359
+ # upsert idempotente (1.34.0): re-correr con la misma sesion NO duplica la linea
1360
+ "$PY" "$EXTRACT" tele/t.jsonl --sidecar tele/telemetry.jsonl >/dev/null 2>&1
1361
+ "$PY" -c "
1362
+ import sys
1363
+ n = sum(1 for l in open('tele/telemetry.jsonl', encoding='utf-8') if l.strip())
1364
+ sys.exit(0 if n == 1 else 1)" \
1365
+ && { PASS=$((PASS+1)); echo " ok re-correr el extractor hace UPSERT (1 linea, no infla el total en watch)"; } \
1366
+ || { FAIL=$((FAIL+1)); echo " FAIL el extractor duplico la linea de sesion (watch-mode inflaria)"; }
1367
+
1368
+ echo "== T55 dashboard project (1.34.0): config.project gana; sin el, join de repos =="
1369
+ printf '{ "project": "My Project", "defaults": { "acceptance_file": "acc-mir.md" },\n "repos": [ {"name":"backend-api","path":"repo-mir","type":"python"} ], "integration": {"enabled": false} }\n' > mirp.json
1370
+ run init --config mirp.json --out L-mirp.json >/dev/null 2>&1
1371
+ run dashboard --ledger L-mirp.json --json 2>/dev/null | "$PY" -c "import json,sys; sys.exit(0 if json.load(sys.stdin)['project']=='My Project' else 1)" \
1372
+ && { PASS=$((PASS+1)); echo " ok config.project -> dashboard.project"; } \
1373
+ || { FAIL=$((FAIL+1)); echo " FAIL no toma project del config"; }
1374
+ run dashboard --ledger L-mir.json --json 2>/dev/null | "$PY" -c "import json,sys; sys.exit(0 if json.load(sys.stdin)['project']=='backend-api' else 1)" \
1375
+ && { PASS=$((PASS+1)); echo " ok sin project en config -> fallback al nombre del repo (truth-pass)"; } \
1376
+ || { FAIL=$((FAIL+1)); echo " FAIL fallback de project mal"; }
1377
+
1378
+ echo "== T56 mirador-render (1.34.0): dashboard + telemetria mergeada + inject + meta-refresh =="
1379
+ RENDER="$(dirname "$(dirname "$QL")")/uscha-mirador/mirador-render.py"
1380
+ TPL="$(dirname "$(dirname "$QL")")/uscha-mirador/mirador.template.html"
1381
+ "$PY" "$RENDER" --engine "$QL" --ledger L-mirp.json --template "$TPL" --out mir-out.html --sidecar tele/telemetry.jsonl --refresh 30 >/dev/null 2>&1
1382
+ "$PY" -c "
1383
+ import re, json, sys
1384
+ h = open('mir-out.html', encoding='utf-8').read()
1385
+ m = re.search(r'const DATA = (\{.*\});\n/\*MIRADOR_DATA_END', h, re.S)
1386
+ d = json.loads(m.group(1))
1387
+ ok = (d['project'] == 'My Project' and 'telemetry' in d
1388
+ and d['telemetry']['tokens_in'] == 54000
1389
+ and 'http-equiv=\"refresh\" content=\"30\"' in h)
1390
+ sys.exit(0 if ok else 1)" \
1391
+ && { PASS=$((PASS+1)); echo " ok render standalone: project + telemetria + meta-refresh en mirador.html"; } \
1392
+ || { FAIL=$((FAIL+1)); echo " FAIL mirador-render no produjo el HTML esperado"; }
1393
+
1394
+ echo "== T57 skill-count no-drift (1.34.0): USCHA_SKILLS del doctor == dirs uscha-* en disco =="
1395
+ SKILLS_DIR="$(dirname "$(dirname "$QL")")"
1396
+ "$PY" -c "
1397
+ import sys, os
1398
+ sys.path.insert(0, os.path.dirname(sys.argv[1]))
1399
+ import qa_ledger as q
1400
+ listed = set(q.USCHA_SKILLS)
1401
+ ondisk = {d for d in os.listdir(sys.argv[2])
1402
+ if d.startswith('uscha-') and os.path.isdir(os.path.join(sys.argv[2], d))}
1403
+ sys.exit(0 if listed == ondisk else 1)" "$QL" "$SKILLS_DIR" \
1404
+ && { PASS=$((PASS+1)); echo " ok el doctor lista exactamente las skills uscha-* en disco (sin drift)"; } \
1405
+ || { FAIL=$((FAIL+1)); echo " FAIL USCHA_SKILLS != dirs uscha-* en disco (skill nueva sin registrar en el doctor?)"; }
1406
+
1407
+ echo "== T58 execution-policy (1.35.0): routing por fase sin contaminar readiness =="
1408
+ cat > ep.json <<'EOF'
1409
+ { "version": "1.35.0",
1410
+ "defaults": {
1411
+ "execution_policy": {
1412
+ "default": { "tier": "standard", "effort": "medium" },
1413
+ "phases": {
1414
+ "qa": { "method": "checker fresco", "tier": "checker", "model": "gpt-5.5", "effort": "high", "uncorrelated": true },
1415
+ "build": { "method": "implementar plan", "tier": "standard", "effort": "medium" }
1416
+ }
1417
+ }
1418
+ },
1419
+ "repos": [ {"name":"repo-c","path":"repo-c","type":"python"} ],
1420
+ "integration": {"enabled": false} }
1421
+ EOF
1422
+ run init --config ep.json --out L-ep.json >/dev/null 2>&1
1423
+ run execution-policy --ledger L-ep.json --phase qa --json 2>/dev/null | "$PY" -c "
1424
+ import json, sys
1425
+ d = json.load(sys.stdin)
1426
+ ok = (d['phase'] == 'qa' and d['method'] == 'checker fresco'
1427
+ and d['tier'] == 'checker' and d['model'] == 'gpt-5.5'
1428
+ and d['effort'] == 'high' and d['uncorrelated'] is True)
1429
+ sys.exit(0 if ok else 1)" \
1430
+ && { PASS=$((PASS+1)); echo " ok CLI JSON devuelve metodologia/model/effort declarados para qa"; } \
1431
+ || { FAIL=$((FAIL+1)); echo " FAIL execution-policy --json no respeta config.defaults.execution_policy"; }
1432
+ run execution-policy --ledger L-ep.json --phase qa 2>/dev/null | grep -q "EXECUTION qa: checker fresco | tier=checker model=gpt-5.5 effort=high" \
1433
+ && { PASS=$((PASS+1)); echo " ok CLI humano emite una linea operable por fase"; } \
1434
+ || { FAIL=$((FAIL+1)); echo " FAIL execution-policy no imprime la linea de fase esperada"; }
1435
+ run dashboard --ledger L-ep.json --json 2>/dev/null | "$PY" -c "
1436
+ import json, sys
1437
+ d = json.load(sys.stdin)
1438
+ ph = {p['key']: p['execution'] for p in d['phases']}
1439
+ ok = ('execution_policy' in d
1440
+ and d['execution_policy']['source'] == 'config.defaults.execution_policy'
1441
+ and ph['qa']['model'] == 'gpt-5.5' and ph['qa']['effort'] == 'high'
1442
+ and ph['build']['method'] == 'implementar plan'
1443
+ and isinstance(d['readiness']['score'], (int, float)))
1444
+ sys.exit(0 if ok else 1)" \
1445
+ && { PASS=$((PASS+1)); echo " ok dashboard expone execution_policy y anota phases sin ser score"; } \
1446
+ || { FAIL=$((FAIL+1)); echo " FAIL dashboard no expone execution_policy por fase"; }
1447
+
1448
+ echo "== T59 mirador-render (1.35.0): bird's-eye muestra policy model/effort =="
1449
+ "$PY" "$RENDER" --engine "$QL" --ledger L-ep.json --template "$TPL" --out ep-mir.html >/dev/null 2>&1
1450
+ "$PY" -c "
1451
+ import re, json, sys
1452
+ h = open('ep-mir.html', encoding='utf-8').read()
1453
+ m = re.search(r'const DATA = (\{.*\});\n/\*MIRADOR_DATA_END', h, re.S)
1454
+ d = json.loads(m.group(1))
1455
+ ok = ('id=\"exec\"' in h and 'Execution policy' in h
1456
+ and d['execution_policy']['phases']['qa']['model'] == 'gpt-5.5'
1457
+ and d['execution_policy']['phases']['qa']['effort'] == 'high')
1458
+ sys.exit(0 if ok else 1)" \
1459
+ && { PASS=$((PASS+1)); echo " ok mirador renderiza el panel y preserva model/effort en DATA"; } \
1460
+ || { FAIL=$((FAIL+1)); echo " FAIL mirador no muestra execution_policy"; }
1461
+
1462
+ echo "== T60 discovery-intake (1.36.0): production finding reabre discovery =="
1463
+ printf -- "# ACCEPTANCE\n\n- [ ] AC-01 checkout total correcto\n" > acc-intake.md
1464
+ printf '{ "version": "1.36.0", "defaults": { "acceptance_file": "acc-intake.md" },\n "repos": [ {"name":"repo-c","path":"repo-c","type":"python"} ], "integration": {"enabled": false} }\n' > intake.json
1465
+ run init --config intake.json --out L-intake.json >/dev/null 2>&1
1466
+ run production-finding --ledger L-intake.json --repo repo-c --severity HIGH --source sentry --title "checkout total wrong" --evidence "Sentry INC-1" 2>/dev/null | grep -q "PF-001" \
1467
+ && { PASS=$((PASS+1)); echo " ok production-finding crea PF-001 con evidencia de produccion"; } \
1468
+ || { FAIL=$((FAIL+1)); echo " FAIL production-finding no crea PF-001"; }
1469
+ run readiness --ledger L-intake.json --json 2>/dev/null | "$PY" -c "
1470
+ import json, sys
1471
+ d = json.load(sys.stdin)
1472
+ pf = d['discovery_intake']['production_findings']
1473
+ ok = (len(pf) == 1 and pf[0]['id'] == 'PF-001' and pf[0]['severity'] == 'HIGH'
1474
+ and d['facts']['production_findings_open'] == 1)
1475
+ sys.exit(0 if ok else 1)" \
1476
+ && { PASS=$((PASS+1)); echo " ok readiness expone production findings como discovery_intake"; } \
1477
+ || { FAIL=$((FAIL+1)); echo " FAIL readiness no expone production findings"; }
1478
+ run readiness --ledger L-intake.json 2>/dev/null | grep -q "production findings open" \
1479
+ && { PASS=$((PASS+1)); echo " ok readiness default avisa que discovery debe reabrirse"; } \
1480
+ || { FAIL=$((FAIL+1)); echo " FAIL readiness default no avisa production finding"; }
1481
+ run production-finding --ledger L-intake.json --id PF-001 --resolve --note "fed into SPEC next cycle" >/dev/null 2>&1
1482
+ run readiness --ledger L-intake.json --json 2>/dev/null | "$PY" -c "
1483
+ import json, sys
1484
+ d = json.load(sys.stdin)
1485
+ sys.exit(0 if d['discovery_intake']['production_findings'] == [] and d['facts']['production_findings_open'] == 0 else 1)" \
1486
+ && { PASS=$((PASS+1)); echo " ok resolver PF-001 limpia el intake abierto"; } \
1487
+ || { FAIL=$((FAIL+1)); echo " FAIL resolver PF-001 no limpia discovery_intake"; }
1488
+
1489
+ echo "== T61 spec-doubt (1.36.0): SPEC-WRONG bloquea atajos y exige humano =="
1490
+ run spec-doubt --ledger L-intake.json --repo repo-c --kind spec-wrong --severity HIGH --note "AC dice sin impuesto, codigo real lo incluye" --evidence "demo con usuario" 2>/dev/null | grep -q "SD-001" \
1491
+ && { PASS=$((PASS+1)); echo " ok spec-doubt crea SD-001 como duda de SPEC"; } \
1492
+ || { FAIL=$((FAIL+1)); echo " FAIL spec-doubt no crea SD-001"; }
1493
+ run readiness --ledger L-intake.json --json 2>/dev/null | "$PY" -c "
1494
+ import json, sys
1495
+ d = json.load(sys.stdin)
1496
+ sd = d['discovery_intake']['spec_doubts']
1497
+ ok = (len(sd) == 1 and sd[0]['id'] == 'SD-001' and sd[0]['kind'] == 'spec-wrong'
1498
+ and d['facts']['spec_doubts_open'] == 1)
1499
+ sys.exit(0 if ok else 1)" \
1500
+ && { PASS=$((PASS+1)); echo " ok readiness expone spec-doubt como discovery_intake"; } \
1501
+ || { FAIL=$((FAIL+1)); echo " FAIL readiness no expone spec-doubt"; }
1502
+ run phase --ledger L-intake.json --repo repo-c --json 2>/dev/null | "$PY" -c "
1503
+ import json, sys
1504
+ d = json.load(sys.stdin)
1505
+ sys.exit(0 if d['phase'] == 'escalated' and any('spec-doubt' in e for e in d['evidence']) else 1)" \
1506
+ && { PASS=$((PASS+1)); echo " ok phase deriva escalated si hay spec-doubt abierto"; } \
1507
+ || { FAIL=$((FAIL+1)); echo " FAIL phase ignora spec-doubt abierto"; }
1508
+ run spec-doubt --ledger L-intake.json --id SD-001 --resolve --decision "SPEC amended" --note "acceptance updated" >/dev/null 2>&1
1509
+ run readiness --ledger L-intake.json --json 2>/dev/null | "$PY" -c "
1510
+ import json, sys
1511
+ d = json.load(sys.stdin)
1512
+ sys.exit(0 if d['discovery_intake']['spec_doubts'] == [] and d['facts']['spec_doubts_open'] == 0 else 1)" \
1513
+ && { PASS=$((PASS+1)); echo " ok resolver SD-001 limpia el intake abierto"; } \
1514
+ || { FAIL=$((FAIL+1)); echo " FAIL resolver SD-001 no limpia discovery_intake"; }
1515
+
1516
+ echo "== T62 ADR experiments (1.37.0): hipotesis visible, advisory, no score =="
1517
+ mkdir -p docs/adr
1518
+ cat > docs/adr/ADR-001-checkout-path.md <<'EOF'
1519
+ # ADR-001: Checkout path
1520
+ ## Status: Experiment
1521
+ ## Context
1522
+ Tenemos dos caminos viables y la respuesta depende de feedback real.
1523
+ ## Decision
1524
+ Probar el nuevo checkout para aprender con bajo blast radius.
1525
+ ## Hypothesis
1526
+ El checkout nuevo reduce abandonos sin subir errores.
1527
+ ## Feedback Signal
1528
+ Conversion rate y errores de pago en produccion.
1529
+ ## Review By: 2099-01-01
1530
+ ## Promote Criteria
1531
+ Conversion estable o mejor y cero incidentes HIGH/BLOCKER.
1532
+ ## Rollback / Supersede Criteria
1533
+ Suben errores de pago o aparece production-finding gateado.
1534
+ ## Implementation Plan
1535
+ - Affected paths: checkout/*
1536
+ ## Verification
1537
+ - [ ] Revisar senales de feedback.
1538
+ EOF
1539
+ cat > docs/adr/ADR-002-bad-experiment.md <<'EOF'
1540
+ # ADR-002: Bad experiment
1541
+ ## Status: Experiment
1542
+ ## Context
1543
+ Esto declara experimento pero no dice como se mide ni como se cierra.
1544
+ ## Hypothesis
1545
+ Tal vez mejora.
1546
+ ## Review By: 2000-01-01
1547
+ EOF
1548
+ run dashboard --ledger L-intake.json --json 2>/dev/null | "$PY" -c "
1549
+ import json, sys
1550
+ d = json.load(sys.stdin)
1551
+ adrs = {a['id']: a for a in d['adrs']}
1552
+ good = adrs['ADR-001']
1553
+ bad = adrs['ADR-002']
1554
+ summary = d['adr_experiments']
1555
+ ok = (
1556
+ good['status'] == 'prog'
1557
+ and good['adr_status'] == 'experiment'
1558
+ and good['experiment_valid'] is True
1559
+ and good['review_by'] == '2099-01-01'
1560
+ and good['expired'] is False
1561
+ and bad['experiment_valid'] is False
1562
+ and bad['expired'] is True
1563
+ and 'feedback_signal' in bad['experiment_missing']
1564
+ and summary['open'] == 2
1565
+ and summary['malformed'] == 1
1566
+ and summary['expired'] == 1
1567
+ and isinstance(d['readiness']['score'], (int, float))
1568
+ )
1569
+ sys.exit(0 if ok else 1)" \
1570
+ && { PASS=$((PASS+1)); echo " ok dashboard expone experiment ADR valido/malformado como advisory"; } \
1571
+ || { FAIL=$((FAIL+1)); echo " FAIL dashboard no modela ADR experiment correctamente"; }
1572
+ "$PY" "$RENDER" --engine "$QL" --ledger L-intake.json --template "$TPL" --out exp-mir.html >/dev/null 2>&1
1573
+ "$PY" -c "
1574
+ import re, json, sys
1575
+ h = open('exp-mir.html', encoding='utf-8').read()
1576
+ m = re.search(r'const DATA = (\{.*\});\n/\*MIRADOR_DATA_END', h, re.S)
1577
+ d = json.loads(m.group(1))
1578
+ ok = ('experiment' in h and 'ADR-001' in h
1579
+ and d['adr_experiments']['open'] == 2
1580
+ and d['adrs'][0]['adr_status'] == 'experiment')
1581
+ sys.exit(0 if ok else 1)" \
1582
+ && { PASS=$((PASS+1)); echo " ok mirador renderiza ADR experiment sin cambiar readiness"; } \
1583
+ || { FAIL=$((FAIL+1)); echo " FAIL mirador no muestra ADR experiment"; }
1584
+
1585
+ echo "== T63 spec-change-request (1.38.0): evidence -> human-signed contract change =="
1586
+ run spec-change-request --ledger L-intake.json --repo repo-c --source SD-001 --requested-change "AC-01 debe incluir impuesto" --evidence "demo + SD-001" --spec ACCEPTANCE.md 2>/dev/null | grep -q "SCR-001" \
1587
+ && { PASS=$((PASS+1)); echo " ok spec-change-request crea SCR-001 desde evidencia"; } \
1588
+ || { FAIL=$((FAIL+1)); echo " FAIL spec-change-request no crea SCR-001"; }
1589
+ run readiness --ledger L-intake.json --json 2>/dev/null | "$PY" -c "
1590
+ import json, sys
1591
+ d = json.load(sys.stdin)
1592
+ scr = d['discovery_intake']['spec_change_requests']
1593
+ ok = (len(scr) == 1 and scr[0]['id'] == 'SCR-001' and scr[0]['source'] == 'SD-001'
1594
+ and d['facts']['spec_change_requests_open'] == 1)
1595
+ sys.exit(0 if ok else 1)" \
1596
+ && { PASS=$((PASS+1)); echo " ok readiness expone SCR abierto como puente contractual"; } \
1597
+ || { FAIL=$((FAIL+1)); echo " FAIL readiness no expone SCR abierto"; }
1598
+ run phase --ledger L-intake.json --repo repo-c --json 2>/dev/null | "$PY" -c "
1599
+ import json, sys
1600
+ d = json.load(sys.stdin)
1601
+ sys.exit(0 if d['phase'] == 'escalated' and any('SCR-001' in e for e in d['evidence']) else 1)" \
1602
+ && { PASS=$((PASS+1)); echo " ok phase deriva escalated si hay SCR humano pendiente"; } \
1603
+ || { FAIL=$((FAIL+1)); echo " FAIL phase ignora SCR abierto"; }
1604
+ run spec-change-request --ledger L-intake.json --id SCR-001 --resolve --decision accepted --note "ACCEPTANCE amended" --amended ACCEPTANCE.md >/dev/null 2>&1
1605
+ run readiness --ledger L-intake.json --json 2>/dev/null | "$PY" -c "
1606
+ import json, sys
1607
+ d = json.load(sys.stdin)
1608
+ sys.exit(0 if d['discovery_intake']['spec_change_requests'] == [] and d['facts']['spec_change_requests_open'] == 0 else 1)" \
1609
+ && { PASS=$((PASS+1)); echo " ok resolver SCR-001 limpia el puente contractual abierto"; } \
1610
+ || { FAIL=$((FAIL+1)); echo " FAIL resolver SCR-001 no limpia discovery_intake"; }
1611
+
1612
+ echo "== T64 golden labels (1.38.0): intended vs observed-accidental visible =="
1613
+ mkdir -p gold
1614
+ printf 'legacy bug preserved\n' > gold/qr.received.txt
1615
+ cp gold/qr.received.txt gold/qr.approved.txt
1616
+ cat > golden-labels.json <<'EOF'
1617
+ {
1618
+ "fixtures": {
1619
+ "gold/qr.approved.txt": {
1620
+ "classification": "observed-accidental",
1621
+ "note": "legacy QR bug preserved for migration only"
1622
+ }
1623
+ }
1624
+ }
1625
+ EOF
1626
+ run golden-diff --dir gold --labels golden-labels.json --json 2>/dev/null | "$PY" -c "
1627
+ import json, sys
1628
+ d = json.load(sys.stdin)
1629
+ labels = d['golden_labels']
1630
+ ok = (d['verdict'] == 'CLEAN'
1631
+ and labels['observed_accidental'] == 1
1632
+ and labels['intended'] == 0
1633
+ and labels['unknown'] == 0
1634
+ and d['fixtures'][0]['classification'] == 'observed-accidental')
1635
+ sys.exit(0 if ok else 1)" \
1636
+ && { PASS=$((PASS+1)); echo " ok golden-diff clasifica golden observado-accidental sin debilitar el byte compare"; } \
1637
+ || { FAIL=$((FAIL+1)); echo " FAIL golden-diff no expone labels intended/accidental"; }
1638
+ run golden-diff --dir gold --json 2>/dev/null | "$PY" -c "
1639
+ import json, sys
1640
+ d = json.load(sys.stdin)
1641
+ sys.exit(0 if d['golden_labels']['unknown'] == 1 else 1)" \
1642
+ && { PASS=$((PASS+1)); echo " ok golden sin labels queda unknown, no inventa intencion"; } \
1643
+ || { FAIL=$((FAIL+1)); echo " FAIL golden-diff sin labels no queda unknown"; }
1644
+
1645
+ echo "== T65 calibration summary (1.38.0): post-merge facts calibran la retro =="
1646
+ run summary --ledger L-intake.json --json 2>/dev/null | "$PY" -c "
1647
+ import json, sys
1648
+ d = json.load(sys.stdin)
1649
+ cal = d['post_merge_calibration']
1650
+ ok = (cal['production_findings']['total'] == 1
1651
+ and cal['production_findings']['resolved'] == 1
1652
+ and cal['spec_doubts']['total'] == 1
1653
+ and cal['spec_doubts']['resolved'] == 1
1654
+ and cal['spec_change_requests']['total'] == 1
1655
+ and cal['spec_change_requests']['accepted'] == 1
1656
+ and cal['contract_reopen_signals'] == 3)
1657
+ sys.exit(0 if ok else 1)" \
1658
+ && { PASS=$((PASS+1)); echo " ok summary expone calibracion post-merge desde PF/SD/SCR"; } \
1659
+ || { FAIL=$((FAIL+1)); echo " FAIL summary no expone calibracion post-merge"; }
1660
+
1661
+ echo "== T66 universal installer (1.40.1): Codex plugin + Claude adapter, dry-run safe =="
1662
+ INST_HOME="$SB/home-installer"
1663
+ mkdir -p "$INST_HOME"
1664
+ "$PY" "$KIT/install-uscha.py" version --json 2>/dev/null | "$PY" -c "
1665
+ import json, sys
1666
+ d = json.load(sys.stdin)
1667
+ ok = (d['source_version'] == '1.40.1' and 'codex' in d['targets'] and 'claude' in d['targets'])
1668
+ sys.exit(0 if ok else 1)" \
1669
+ && { PASS=$((PASS+1)); echo " ok install-uscha version expone version fuente y targets"; } \
1670
+ || { FAIL=$((FAIL+1)); echo " FAIL install-uscha version no expone targets/version"; }
1671
+ "$PY" "$KIT/install-uscha.py" install --target both --home "$INST_HOME" --dry-run --json 2>/dev/null | "$PY" -c "
1672
+ import json, sys
1673
+ d = json.load(sys.stdin)
1674
+ ops = '\n'.join(o['path'].replace(chr(92), '/') for o in d['operations'])
1675
+ ok = (d['dry_run'] is True and 'plugins/uscha' in ops and '.agents/plugins/marketplace.json' in ops and '.claude/skills/uscha-devloop' in ops)
1676
+ sys.exit(0 if ok else 1)" \
1677
+ && { PASS=$((PASS+1)); echo " ok dry-run planifica Codex plugin y Claude skills sin escribir"; } \
1678
+ || { FAIL=$((FAIL+1)); echo " FAIL dry-run no muestra plan universal"; }
1679
+ [ ! -e "$INST_HOME/.agents/plugins/uscha" ] && [ ! -e "$INST_HOME/.claude/skills/uscha-devloop" ] \
1680
+ && { PASS=$((PASS+1)); echo " ok dry-run no crea instalacion"; } \
1681
+ || { FAIL=$((FAIL+1)); echo " FAIL dry-run escribio archivos"; }
1682
+ "$PY" "$KIT/install-uscha.py" install --target codex --home "$INST_HOME" --json >/dev/null 2>&1
1683
+ INST_HOME="$INST_HOME" "$PY" -c "
1684
+ import json, os, pathlib, sys
1685
+ h = pathlib.Path(os.environ['INST_HOME'])
1686
+ manifest = h/'plugins/uscha/.codex-plugin/plugin.json'
1687
+ market = h/'.agents/plugins/marketplace.json'
1688
+ engine = h/'plugins/uscha/skills/uscha-devloop/qa_ledger.py'
1689
+ marker = h/'plugins/uscha/uscha-install.json'
1690
+ ok = (manifest.exists() and market.exists() and engine.exists() and
1691
+ json.load(open(manifest, encoding='utf-8'))['version'] == '1.40.1' and
1692
+ json.load(open(marker, encoding='utf-8'))['target'] == 'codex')
1693
+ sys.exit(0 if ok else 1)" \
1694
+ && { PASS=$((PASS+1)); echo " ok install codex crea plugin personal, marketplace y marker"; } \
1695
+ || { FAIL=$((FAIL+1)); echo " FAIL install codex incompleto"; }
1696
+ "$PY" "$KIT/install-uscha.py" doctor --target codex --home "$INST_HOME" --json 2>/dev/null | "$PY" -c "
1697
+ import json, sys
1698
+ d = json.load(sys.stdin)
1699
+ ok = (d['source_version'] == '1.40.1' and d['targets']['codex']['installed'] is True and d['targets']['codex']['version_match'] is True)
1700
+ sys.exit(0 if ok else 1)" \
1701
+ && { PASS=$((PASS+1)); echo " ok doctor detecta Codex instalado y version match"; } \
1702
+ || { FAIL=$((FAIL+1)); echo " FAIL doctor no detecta install Codex"; }
1703
+ diff -qr "$KIT/.claude/skills" "$KIT/skills" -x __pycache__ >/dev/null 2>&1 \
1704
+ && { PASS=$((PASS+1)); echo " ok Codex plugin skills mirror stays synced with canonical skills"; } \
1705
+ || { FAIL=$((FAIL+1)); echo " FAIL uscha-kit/skills drifted from .claude/skills"; }
1706
+
1707
+
1708
+ echo "== T67 npm router (1.40.1): npx package delegates to canonical installer =="
1709
+ if command -v node >/dev/null 2>&1; then
1710
+ node "$ROOT/bin/uscha.js" version --json 2>/dev/null | "$PY" -c "
1711
+ import json, sys
1712
+ d = json.load(sys.stdin)
1713
+ ok = (d['source_version'] == '1.40.1' and 'codex' in d['targets'] and 'claude' in d['targets'])
1714
+ sys.exit(0 if ok else 1)" \
1715
+ && { PASS=$((PASS+1)); echo " ok npm router expone version/targets desde install-uscha.py"; } \
1716
+ || { FAIL=$((FAIL+1)); echo " FAIL npm router no delega correctamente al installer"; }
1717
+ else
1718
+ FAIL=$((FAIL+1)); echo " FAIL node no esta disponible para probar el router npm"
1719
+ fi
1720
+ if command -v npm >/dev/null 2>&1; then
1721
+ (cd "$ROOT" && npm_config_cache="$SB/npm-cache" npm pack --dry-run --json 2>/dev/null) | "$PY" -c "
1722
+ import json, sys
1723
+ d = json.load(sys.stdin)[0]
1724
+ files = {f['path'] for f in d['files']}
1725
+ ok = (d['name'] == '@andresmassello/uscha' and d['version'] == '1.40.1'
1726
+ and 'bin/uscha.js' in files and 'uscha-kit/install-uscha.py' in files
1727
+ and '.atl/skill-registry.md' not in files and 'handoff.md' not in files and 'mirador.html' not in files
1728
+ and not any('__pycache__' in f or f.endswith(('.pyc', '.pyo')) for f in files))
1729
+ sys.exit(0 if ok else 1)" \
1730
+ && { PASS=$((PASS+1)); echo " ok npm pack dry-run incluye router/kit y excluye artefactos locales"; } \
1731
+ || { FAIL=$((FAIL+1)); echo " FAIL npm pack dry-run no tiene el contenido esperado"; }
1732
+ else
1733
+ FAIL=$((FAIL+1)); echo " FAIL npm no esta disponible para probar package dry-run"
1734
+ fi
1735
+
1736
+ echo ""
1737
+ echo "RESULTADO: $PASS ok · $FAIL fail"
1738
+ cd / && rm -rf "$SB"
1739
+ [ "$FAIL" -eq 0 ]