@andresmassello/uscha 1.40.2 → 1.43.0

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 (39) hide show
  1. package/README.md +6 -6
  2. package/bin/uscha.js +19 -7
  3. package/package.json +1 -1
  4. package/uscha-kit/.claude/skills/uscha-adr-refine/SKILL.md +2 -2
  5. package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +3 -1
  6. package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +729 -155
  7. package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +22 -7
  8. package/uscha-kit/.claude/skills/uscha-mirador/mirador-render.py +44 -5
  9. package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.ps1 +1 -1
  10. package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.sh +1 -1
  11. package/uscha-kit/.claude/skills/uscha-mirador/mirador.template.html +666 -586
  12. package/uscha-kit/.claude-plugin/plugin.json +1 -1
  13. package/uscha-kit/.codex-plugin/plugin.json +1 -1
  14. package/uscha-kit/CHANGELOG-1.41.0.md +18 -0
  15. package/uscha-kit/CHANGELOG-1.41.1.md +53 -0
  16. package/uscha-kit/CHANGELOG-1.41.2.md +34 -0
  17. package/uscha-kit/CHANGELOG-1.41.3.md +30 -0
  18. package/uscha-kit/CHANGELOG-1.42.0.md +41 -0
  19. package/uscha-kit/CHANGELOG-1.43.0.md +37 -0
  20. package/uscha-kit/INSTALL.md +120 -101
  21. package/uscha-kit/README.md +24 -13
  22. package/uscha-kit/VERSION +1 -1
  23. package/uscha-kit/WORKBENCH.md +19 -5
  24. package/uscha-kit/hooks/block-approved-writes.py +25 -0
  25. package/uscha-kit/install-uscha.py +534 -267
  26. package/uscha-kit/skills/uscha-adr-refine/SKILL.md +2 -2
  27. package/uscha-kit/skills/uscha-devloop/SKILL.md +3 -1
  28. package/uscha-kit/skills/uscha-devloop/qa_ledger.py +729 -155
  29. package/uscha-kit/skills/uscha-mirador/SKILL.md +22 -7
  30. package/uscha-kit/skills/uscha-mirador/mirador-render.py +44 -5
  31. package/uscha-kit/skills/uscha-mirador/mirador-watch.ps1 +1 -1
  32. package/uscha-kit/skills/uscha-mirador/mirador-watch.sh +1 -1
  33. package/uscha-kit/skills/uscha-mirador/mirador.template.html +666 -586
  34. package/uscha-kit/templates/CONSTITUTION.md +4 -4
  35. package/uscha-kit/templates/docs/adr/README.md +19 -19
  36. package/uscha-kit/tests/ledger-integrity-regressions.py +136 -0
  37. package/uscha-kit/tests/smoke-engine.sh +1312 -29
  38. package/uscha-kit/uscha.config.json +1 -1
  39. package/uscha-kit/workbench-doctor.sh +47 -3
@@ -49,6 +49,192 @@ if [ -z "$PY" ]; then
49
49
  done
50
50
  fi
51
51
  [ -n "$PY" ] || { echo "FAIL: no hay Python funcional en PATH"; exit 1; }
52
+ if [ "${USCHA_INSTALLER_P0_D_ONLY:-0}" = "1" ]; then
53
+ "$PY" - "$KIT/install-uscha.py" <<'PY'
54
+ import json
55
+ import os
56
+ import pathlib
57
+ import subprocess
58
+ import sys
59
+ import tempfile
60
+
61
+ installer = pathlib.Path(sys.argv[1])
62
+
63
+
64
+ def run(*args):
65
+ return subprocess.run(
66
+ [sys.executable, str(installer), *map(str, args)],
67
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8")
68
+
69
+
70
+ with tempfile.TemporaryDirectory(prefix="uscha-installer-p0-d-") as tmp:
71
+ root = pathlib.Path(tmp)
72
+ repo = root / "repo-symlink"
73
+ outside = root / "outside-sentinel.json"
74
+ repo.mkdir()
75
+ outside.write_bytes(b"outside-sentinel\n")
76
+ (repo / "uscha.config.json").symlink_to(outside)
77
+ before = outside.read_bytes()
78
+
79
+ result = run("init", "--repo", repo, "--force", "--json")
80
+
81
+ assert result.returncode != 0, (result.returncode, result.stdout, result.stderr)
82
+ assert "traceback" not in result.stderr.lower(), result.stderr
83
+ assert outside.read_bytes() == before, "managed-target symlink modified outside sentinel"
84
+ assert (repo / "uscha.config.json").is_symlink(), "managed-target symlink was replaced"
85
+ for name in ("CLAUDE.md", "CONSTITUTION.md", ".gitattributes"):
86
+ assert not (repo / name).exists(), (name, "init wrote after symlink hazard")
87
+ broken_repo = root / "repo-broken-symlink"
88
+ broken_repo.mkdir()
89
+ missing_outside = root / "missing-outside-sentinel.json"
90
+ broken_target = broken_repo / "uscha.config.json"
91
+ broken_target.symlink_to(missing_outside)
92
+ result = run("init", "--repo", broken_repo, "--force", "--json")
93
+ assert result.returncode != 0, (result.returncode, result.stdout, result.stderr)
94
+ assert broken_target.is_symlink(), "broken managed-target symlink was replaced"
95
+ assert not missing_outside.exists(), "broken symlink target was created outside repo"
96
+ for name in ("CLAUDE.md", "CONSTITUTION.md", ".gitattributes"):
97
+ assert not (broken_repo / name).exists(), (name, "init wrote after broken symlink hazard")
98
+ print("P0-D RED/GREEN 1: init rejects existing and broken managed-target symlinks without writes")
99
+
100
+ repo = root / "repo-late-conflict"
101
+ repo.mkdir()
102
+ conflict = repo / ".gitattributes"
103
+ conflict.write_bytes(b"late-conflict-sentinel\n")
104
+ before = conflict.read_bytes()
105
+
106
+ result = run("init", "--repo", repo, "--json")
107
+
108
+ assert result.returncode != 0, (result.returncode, result.stdout, result.stderr)
109
+ assert conflict.read_bytes() == before, "late conflict was modified"
110
+ for name in ("uscha.config.json", "CLAUDE.md", "CONSTITUTION.md"):
111
+ assert not (repo / name).exists(), (name, "written before late conflict discovery")
112
+ dry_repo = root / "repo-init-dry-run"
113
+ result = run("init", "--repo", dry_repo, "--dry-run", "--json")
114
+ assert result.returncode == 0, (result.returncode, result.stdout, result.stderr)
115
+ assert not dry_repo.exists(), "init dry-run wrote the repository"
116
+ print("P0-D RED/GREEN 2: late init conflict prevents all earlier writes; dry-run stays write-free")
117
+
118
+ fault = root / "codex-marker-fault"
119
+ fault.mkdir()
120
+ (fault / "sitecustomize.py").write_text(r"""
121
+ import os
122
+
123
+ _replace = os.replace
124
+ _failed = False
125
+
126
+
127
+ def fail_at_codex_marker(source, target):
128
+ global _failed
129
+ normalized = os.path.normcase(os.path.normpath(os.fspath(target)))
130
+ suffix = os.path.normcase(os.path.normpath(os.path.join("plugins", "uscha", "uscha-install.json")))
131
+ if not _failed and normalized.endswith(suffix):
132
+ _failed = True
133
+ with open(os.environ["USCHA_FAULT_WITNESS"], "w", encoding="utf-8", newline="\n") as handle:
134
+ handle.write("late\n")
135
+ raise OSError("deterministic late Codex marker failure")
136
+ return _replace(source, target)
137
+
138
+
139
+ os.replace = fail_at_codex_marker
140
+ """, encoding="utf-8", newline="\n")
141
+
142
+ marketplace_bytes = (b'{\n "name": "personal", "interface": {"displayName": "Mine"},\n'
143
+ b' "plugins": [{"name":"other","source":{"source":"local","path":"./plugins/other"},'
144
+ b'"policy":{"installation":"AVAILABLE","authentication":"ON_INSTALL"},"category":"Productivity"}]\n}\n')
145
+
146
+ for prior_marketplace in (marketplace_bytes, None):
147
+ label = "existing" if prior_marketplace is not None else "absent"
148
+ home = root / ("home-codex-rollback-" + label)
149
+ plugin = home / "plugins" / "uscha"
150
+ market = home / ".agents" / "plugins" / "marketplace.json"
151
+ plugin.mkdir(parents=True)
152
+ (plugin / "sentinel.bin").write_bytes(b"prior-plugin-tree\x00\xff")
153
+ if prior_marketplace is not None:
154
+ market.parent.mkdir(parents=True)
155
+ market.write_bytes(prior_marketplace)
156
+ before_plugin = sorted((p.relative_to(plugin).as_posix(), p.read_bytes())
157
+ for p in plugin.rglob("*") if p.is_file())
158
+ witness = fault / ("witness-" + label + ".txt")
159
+ env = os.environ.copy()
160
+ env["PYTHONPATH"] = str(fault)
161
+ env["USCHA_FAULT_WITNESS"] = str(witness)
162
+
163
+ result = subprocess.run(
164
+ [sys.executable, str(installer), "install", "--target", "codex",
165
+ "--home", str(home), "--json"], stdout=subprocess.PIPE,
166
+ stderr=subprocess.PIPE, text=True, encoding="utf-8", env=env)
167
+
168
+ assert result.returncode != 0, (label, result.returncode, result.stdout, result.stderr)
169
+ assert witness.read_text(encoding="utf-8") == "late\n", (label, "fault not reached")
170
+ after_plugin = sorted((p.relative_to(plugin).as_posix(), p.read_bytes())
171
+ for p in plugin.rglob("*") if p.is_file())
172
+ assert after_plugin == before_plugin, (label, "prior plugin tree not restored")
173
+ if prior_marketplace is None:
174
+ assert not market.exists(), "new marketplace remained after rollback"
175
+ assert not (home / ".agents").exists(), "new marketplace directories remained after rollback"
176
+ else:
177
+ assert market.read_bytes() == prior_marketplace, "marketplace bytes not restored"
178
+ assert not list((home / "plugins").glob(".uscha.*-*")), (label, "Codex transaction residue")
179
+ print("P0-D RED/GREEN 3: late Codex marker failure restores plugin and marketplace states")
180
+
181
+ home = root / "home-claude-narrow-matcher"
182
+ result = run("install", "--target", "claude", "--home", home, "--json")
183
+ assert result.returncode == 0, (result.stdout, result.stderr)
184
+ settings_path = home / ".claude" / "settings.json"
185
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
186
+ canonical = settings["hooks"]["PreToolUse"][-1]
187
+ canonical["matcher"] = "Write"
188
+ settings["theme"] = "sentinel"
189
+ settings_path.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8", newline="\n")
190
+
191
+ result = run("doctor", "--target", "claude", "--home", home, "--json")
192
+ assert result.returncode != 0, "doctor accepted exact command under a narrow matcher"
193
+
194
+ result = run("install", "--target", "claude", "--home", home, "--json")
195
+ assert result.returncode == 0, (result.stdout, result.stderr)
196
+ result = run("install", "--target", "claude", "--home", home, "--json")
197
+ assert result.returncode == 0, (result.stdout, result.stderr)
198
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
199
+ groups = settings["hooks"]["PreToolUse"]
200
+ commands = [item.get("command") for group in groups if group.get("matcher") == "*"
201
+ for item in group.get("hooks", []) if item.get("type") == "command"
202
+ and "block-approved-writes.py" in item.get("command", "")]
203
+ assert len(commands) == 1, (groups, "canonical matcher registration count")
204
+ assert settings["theme"] == "sentinel", "unrelated Claude settings were not preserved"
205
+ assert any(group.get("matcher") == "Write" for group in groups), "narrow registration was removed"
206
+ result = run("doctor", "--target", "claude", "--home", home, "--json")
207
+ assert result.returncode == 0, (result.stdout, result.stderr)
208
+ print("P0-D RED/GREEN 4: narrow hook matcher requires one canonical registration")
209
+
210
+ malformed_settings = [
211
+ ("root-list", []),
212
+ ("hooks-scalar", {"hooks": 7}),
213
+ ("pretool-scalar", {"hooks": {"PreToolUse": 7}}),
214
+ ("group-list", {"hooks": {"PreToolUse": [[]]}}),
215
+ ("group-hooks-scalar", {"hooks": {"PreToolUse": [{"matcher": "*", "hooks": 7}]}}),
216
+ ("item-list", {"hooks": {"PreToolUse": [{"matcher": "*", "hooks": [[]]}]}}),
217
+ ]
218
+ for label, payload in malformed_settings:
219
+ home = root / ("home-malformed-" + label)
220
+ claude = home / ".claude"
221
+ settings_path = claude / "settings.json"
222
+ claude.mkdir(parents=True)
223
+ original = (json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + "\n").encode("utf-8")
224
+ settings_path.write_bytes(original)
225
+ before_paths = sorted(p.relative_to(home).as_posix() for p in home.rglob("*"))
226
+
227
+ result = run("install", "--target", "claude", "--home", home, "--json")
228
+
229
+ assert result.returncode != 0, (label, result.stdout, result.stderr)
230
+ assert "traceback" not in result.stderr.lower(), (label, result.stderr)
231
+ assert settings_path.read_bytes() == original, (label, "settings bytes changed")
232
+ after_paths = sorted(p.relative_to(home).as_posix() for p in home.rglob("*"))
233
+ assert after_paths == before_paths, (label, before_paths, after_paths)
234
+ print("P0-D RED/GREEN 5: malformed settings shapes fail cleanly without writes")
235
+ PY
236
+ exit $?
237
+ fi
52
238
  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
239
  cd "$SB"
54
240
 
@@ -301,12 +487,12 @@ echo "== T20 rust: clippy JSONL (error/warning/compile-error/summary/dup) =="
301
487
  # HIGH fantasma y el gate no converge jamas. El duplicado (lib + test target)
302
488
  # debe dedupearse por finding ID.
303
489
  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"}}
490
+ {"reason":"compiler-message","message":{"message":"unwrap used","level":"error","code":{"code":"clippy::unwrap_used"},"spans":[{"file_name":"src/lib.rs","line_start":2,"is_primary":true}]}}
491
+ {"reason":"compiler-message","message":{"message":"needless return","level":"warning","code":{"code":"clippy::needless_return"},"spans":[{"file_name":"src/lib.rs","line_start":3,"is_primary":true}]}}
492
+ {"reason":"compiler-message","message":{"message":"needless return","level":"warning","code":{"code":"clippy::needless_return"},"spans":[{"file_name":"src/lib.rs","line_start":3,"is_primary":true}]}}
493
+ {"reason":"compiler-message","message":{"message":"compile error","level":"error","code":null,"spans":[{"file_name":"src/lib.rs","line_start":1,"is_primary":true}]}}
494
+ {"reason":"compiler-message","message":{"message":"1 warning emitted","level":"warning","code":null,"spans":[]}}
495
+ {"reason":"compiler-message","message":{"message":"aborting due to 1 previous error","level":"error","code":null,"spans":[]}}
310
496
  {"reason":"build-finished","success":false}
311
497
  EOF
312
498
  INGF=$(run ingest-gate --repo repo-f --iteration 1 2>&1)
@@ -788,14 +974,17 @@ sys.exit(0 if d['budgets_declared'] == ['max_lines_added'] else 1)" \
788
974
  || { FAIL=$((FAIL+1)); echo " FAIL budgets_declared no refleja el CLI"; }
789
975
 
790
976
  echo "== T40 phase: FSM DERIVADA del ledger — el estado se computa, no se declara =="
791
- mkdir -p repo-x
977
+ mkdir -p repo-x/reports
978
+ cat > repo-x/reports/junit.xml <<'EOF'
979
+ <testsuite tests="1" failures="0" errors="0" skipped="0"/>
980
+ EOF
792
981
  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
982
  run init --config c-fsm.json --out L-fsm.json >/dev/null 2>&1
794
983
  chk "ledger virgen -> plan" 0 run phase --ledger L-fsm.json --repo fsm --require plan
795
984
  run snapshot --ledger L-fsm.json --repo fsm >/dev/null 2>&1
796
985
  chk "snapshot medido sin QA -> build" 0 run phase --ledger L-fsm.json --repo fsm --require build
797
986
  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
987
+ --reported 2 --gated-reported 2 --tests-passed true >/dev/null 2>&1
799
988
  chk "pasos de QA sin converger -> qa" 0 run phase --ledger L-fsm.json --repo fsm --require qa
800
989
  chk "pedir pr-ready con findings abiertos -> exit 1 (los hechos mandan)" 1 \
801
990
  run phase --ledger L-fsm.json --repo fsm --require pr-ready
@@ -1221,22 +1410,24 @@ sys.exit(0 if ok else 1)" \
1221
1410
  && { PASS=$((PASS+1)); echo " ok tests rotos al regenerar -> DIVERGE/PARTIAL + gap reportado"; } \
1222
1411
  || { FAIL=$((FAIL+1)); echo " FAIL compare no detecto la divergencia de tests"; }
1223
1412
 
1224
- echo "== T44 sync quintuple de version: VERSION = config = plugin.json = marketplace.json =="
1413
+ echo "== T44 sync seis fuentes de version + changelog: VERSION/config/Claude/marketplace/package/Codex =="
1225
1414
  "$PY" -c "
1226
1415
  import json, sys, os, io
1227
1416
  kit = os.path.dirname(os.path.dirname(os.path.dirname(sys.argv[1]))) # <kit>/.claude/skills/x -> <kit>
1228
1417
  repo = os.path.dirname(kit)
1229
1418
  v_file = io.open(os.path.join(kit, 'VERSION'), encoding='utf-8').read().split()[-1]
1230
1419
  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']
1420
+ v_claude = json.load(io.open(os.path.join(kit, '.claude-plugin', 'plugin.json'), encoding='utf-8'))['version']
1421
+ v_codex = json.load(io.open(os.path.join(kit, '.codex-plugin', 'plugin.json'), encoding='utf-8'))['version']
1422
+ v_pkg = json.load(io.open(os.path.join(repo, 'package.json'), encoding='utf-8'))['version']
1232
1423
  mk = json.load(io.open(os.path.join(repo, '.claude-plugin', 'marketplace.json'), encoding='utf-8'))
1233
1424
  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
-
1425
+ versions = [v_file, v_cfg, v_claude, v_mkt, v_pkg, v_codex]
1426
+ changelog = os.path.join(kit, 'CHANGELOG-1.43.0.md')
1427
+ print(' versiones:', *versions)
1428
+ sys.exit(0 if len(set(versions)) == 1 and os.path.isfile(changelog) else 1)" "$(dirname "$QL")" \
1429
+ && { PASS=$((PASS+1)); echo " ok las seis fuentes coinciden y existe CHANGELOG-1.43.0.md"; } \
1430
+ || { FAIL=$((FAIL+1)); echo " FAIL drift de version o falta CHANGELOG-1.43.0.md"; }
1240
1431
  echo "== T51 freshness (1.31.0): reporte JUnit mas viejo que el codigo = STALE -> AC UNMEASURED =="
1241
1432
  mkdir -p repo-fresh/reports
1242
1433
  printf 'def alta():\n return True\n' > repo-fresh/alta.py
@@ -1378,7 +1569,7 @@ run dashboard --ledger L-mir.json --json 2>/dev/null | "$PY" -c "import json,sys
1378
1569
  echo "== T56 mirador-render (1.34.0): dashboard + telemetria mergeada + inject + meta-refresh =="
1379
1570
  RENDER="$(dirname "$(dirname "$QL")")/uscha-mirador/mirador-render.py"
1380
1571
  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
1572
+ "$PY" "$RENDER" --engine "$QL" --ledger L-mirp.json --template "$TPL" --out mir-out.html --sidecar tele/telemetry.jsonl --refresh 30 --no-open >/dev/null 2>&1
1382
1573
  "$PY" -c "
1383
1574
  import re, json, sys
1384
1575
  h = open('mir-out.html', encoding='utf-8').read()
@@ -1446,7 +1637,7 @@ sys.exit(0 if ok else 1)" \
1446
1637
  || { FAIL=$((FAIL+1)); echo " FAIL dashboard no expone execution_policy por fase"; }
1447
1638
 
1448
1639
  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
1640
+ "$PY" "$RENDER" --engine "$QL" --ledger L-ep.json --template "$TPL" --out ep-mir.html --no-open >/dev/null 2>&1
1450
1641
  "$PY" -c "
1451
1642
  import re, json, sys
1452
1643
  h = open('ep-mir.html', encoding='utf-8').read()
@@ -1522,12 +1713,12 @@ cat > docs/adr/ADR-001-checkout-path.md <<'EOF'
1522
1713
  Tenemos dos caminos viables y la respuesta depende de feedback real.
1523
1714
  ## Decision
1524
1715
  Probar el nuevo checkout para aprender con bajo blast radius.
1525
- ## Hypothesis
1716
+ ## HIPÓTESIS
1526
1717
  El checkout nuevo reduce abandonos sin subir errores.
1527
- ## Feedback Signal
1718
+ ## SEÑAL DE FEEDBACK
1528
1719
  Conversion rate y errores de pago en produccion.
1529
1720
  ## Review By: 2099-01-01
1530
- ## Promote Criteria
1721
+ ## CRITERIOS DE PROMOCIÓN
1531
1722
  Conversion estable o mejor y cero incidentes HIGH/BLOCKER.
1532
1723
  ## Rollback / Supersede Criteria
1533
1724
  Suben errores de pago o aparece production-finding gateado.
@@ -1658,13 +1849,13 @@ sys.exit(0 if ok else 1)" \
1658
1849
  && { PASS=$((PASS+1)); echo " ok summary expone calibracion post-merge desde PF/SD/SCR"; } \
1659
1850
  || { FAIL=$((FAIL+1)); echo " FAIL summary no expone calibracion post-merge"; }
1660
1851
 
1661
- echo "== T66 universal installer (1.40.2): Codex plugin + Claude adapter, dry-run safe =="
1852
+ echo "== T66 universal installer (1.41.0): Codex plugin + Claude adapter, dry-run safe =="
1662
1853
  INST_HOME="$SB/home-installer"
1663
1854
  mkdir -p "$INST_HOME"
1664
1855
  "$PY" "$KIT/install-uscha.py" version --json 2>/dev/null | "$PY" -c "
1665
1856
  import json, sys
1666
1857
  d = json.load(sys.stdin)
1667
- ok = (d['source_version'] == '1.40.2' and 'codex' in d['targets'] and 'claude' in d['targets'])
1858
+ ok = (d['source_version'] == '1.43.0' and 'codex' in d['targets'] and 'claude' in d['targets'])
1668
1859
  sys.exit(0 if ok else 1)" \
1669
1860
  && { PASS=$((PASS+1)); echo " ok install-uscha version expone version fuente y targets"; } \
1670
1861
  || { FAIL=$((FAIL+1)); echo " FAIL install-uscha version no expone targets/version"; }
@@ -1688,7 +1879,7 @@ market = h/'.agents/plugins/marketplace.json'
1688
1879
  engine = h/'plugins/uscha/skills/uscha-devloop/qa_ledger.py'
1689
1880
  marker = h/'plugins/uscha/uscha-install.json'
1690
1881
  ok = (manifest.exists() and market.exists() and engine.exists() and
1691
- json.load(open(manifest, encoding='utf-8'))['version'] == '1.40.2' and
1882
+ json.load(open(manifest, encoding='utf-8'))['version'] == '1.43.0' and
1692
1883
  json.load(open(marker, encoding='utf-8'))['target'] == 'codex')
1693
1884
  sys.exit(0 if ok else 1)" \
1694
1885
  && { PASS=$((PASS+1)); echo " ok install codex crea plugin personal, marketplace y marker"; } \
@@ -1696,7 +1887,7 @@ sys.exit(0 if ok else 1)" \
1696
1887
  "$PY" "$KIT/install-uscha.py" doctor --target codex --home "$INST_HOME" --json 2>/dev/null | "$PY" -c "
1697
1888
  import json, sys
1698
1889
  d = json.load(sys.stdin)
1699
- ok = (d['source_version'] == '1.40.2' and d['targets']['codex']['installed'] is True and d['targets']['codex']['version_match'] is True)
1890
+ ok = (d['source_version'] == '1.43.0' and d['targets']['codex']['installed'] is True and d['targets']['codex']['version_match'] is True)
1700
1891
  sys.exit(0 if ok else 1)" \
1701
1892
  && { PASS=$((PASS+1)); echo " ok doctor detecta Codex instalado y version match"; } \
1702
1893
  || { FAIL=$((FAIL+1)); echo " FAIL doctor no detecta install Codex"; }
@@ -1705,12 +1896,12 @@ diff -qr "$KIT/.claude/skills" "$KIT/skills" -x __pycache__ >/dev/null 2>&1 \
1705
1896
  || { FAIL=$((FAIL+1)); echo " FAIL uscha-kit/skills drifted from .claude/skills"; }
1706
1897
 
1707
1898
 
1708
- echo "== T67 npm router (1.40.2): npx package delegates to canonical installer =="
1899
+ echo "== T67 npm router (1.41.0): npx package delegates to canonical installer =="
1709
1900
  if command -v node >/dev/null 2>&1; then
1710
1901
  node "$ROOT/bin/uscha.js" version --json 2>/dev/null | "$PY" -c "
1711
1902
  import json, sys
1712
1903
  d = json.load(sys.stdin)
1713
- ok = (d['source_version'] == '1.40.2' and 'codex' in d['targets'] and 'claude' in d['targets'])
1904
+ ok = (d['source_version'] == '1.43.0' and 'codex' in d['targets'] and 'claude' in d['targets'])
1714
1905
  sys.exit(0 if ok else 1)" \
1715
1906
  && { PASS=$((PASS+1)); echo " ok npm router expone version/targets desde install-uscha.py"; } \
1716
1907
  || { FAIL=$((FAIL+1)); echo " FAIL npm router no delega correctamente al installer"; }
@@ -1722,7 +1913,7 @@ if command -v npm >/dev/null 2>&1; then
1722
1913
  import json, sys
1723
1914
  d = json.load(sys.stdin)[0]
1724
1915
  files = {f['path'] for f in d['files']}
1725
- ok = (d['name'] == '@andresmassello/uscha' and d['version'] == '1.40.2'
1916
+ ok = (d['name'] == '@andresmassello/uscha' and d['version'] == '1.43.0'
1726
1917
  and 'bin/uscha.js' in files and 'uscha-kit/install-uscha.py' in files
1727
1918
  and '.atl/skill-registry.md' not in files and 'handoff.md' not in files and 'mirador.html' not in files
1728
1919
  and not any('__pycache__' in f or f.endswith(('.pyc', '.pyo')) for f in files))
@@ -1733,7 +1924,1099 @@ else
1733
1924
  FAIL=$((FAIL+1)); echo " FAIL npm no esta disponible para probar package dry-run"
1734
1925
  fi
1735
1926
 
1927
+ echo "== T68 pr-ready exige evidencia de tests medida y verde =="
1928
+ mkdir -p repo-pr-evidence
1929
+ printf '{ "defaults": {"qa_tools_order":["code-review","judgment-day","improve"]},\n "repos": [ {"name":"pr-evidence","path":"repo-pr-evidence","type":"python"} ], "integration": {"enabled": false} }\n' > c-pr-evidence.json
1930
+ run init --config c-pr-evidence.json --out L-pr-evidence.json >/dev/null 2>&1
1931
+ for t in code-review judgment-day improve; do
1932
+ run log-step --ledger L-pr-evidence.json --repo pr-evidence --tool "$t" \
1933
+ --iteration 1 --gated-reported 0 --files-changed 0 >/dev/null 2>&1
1934
+ done
1935
+ chk "QA narrada sin snapshot/reporte medido NO queda pr-ready" 1 \
1936
+ run phase --ledger L-pr-evidence.json --repo pr-evidence --require pr-ready
1937
+ run phase --ledger L-pr-evidence.json --repo pr-evidence 2>/dev/null \
1938
+ | grep -q "falta evidencia medida de tests" \
1939
+ && { PASS=$((PASS+1)); echo " ok phase explains missing measured test evidence"; } \
1940
+ || { FAIL=$((FAIL+1)); echo " FAIL phase does not explain missing measured test evidence"; }
1941
+
1942
+ echo "== T69 reportes malformados fallan cerrados =="
1943
+ mkdir -p repo-pr-evidence/reports
1944
+ cat > repo-pr-evidence/reports/ruff.json <<'EOF'
1945
+ [{"code":"S101","filename":"app.py","location":{"row":1}}]
1946
+ EOF
1947
+ run ingest-gate --ledger L-pr-evidence.json --repo pr-evidence --iteration 2 \
1948
+ --ruff repo-pr-evidence/reports/ruff.json >/dev/null 2>&1
1949
+ cp L-pr-evidence.json L-pr-evidence-before-invalid.json
1950
+ printf '{malformed\n' > repo-pr-evidence/reports/ruff.json
1951
+ chk "Ruff JSON malformado -> evidencia invalida exit 2" 2 \
1952
+ run ingest-gate --ledger L-pr-evidence.json --repo pr-evidence --iteration 3 \
1953
+ --ruff repo-pr-evidence/reports/ruff.json
1954
+ cmp -s L-pr-evidence-before-invalid.json L-pr-evidence.json \
1955
+ && { PASS=$((PASS+1)); echo " ok Ruff invalido no reemplaza findings previos con estado limpio"; } \
1956
+ || { FAIL=$((FAIL+1)); echo " FAIL invalid Ruff changed the ledger"; }
1957
+ cp L-pr-evidence.json L-pr-evidence-before-schema-invalid.json
1958
+ printf '[42]\n' > repo-pr-evidence/reports/ruff.json
1959
+ chk "Ruff JSON schema-invalid -> invalid evidence exit 2" 2 \
1960
+ run ingest-gate --ledger L-pr-evidence.json --repo pr-evidence --iteration 4 \
1961
+ --ruff repo-pr-evidence/reports/ruff.json
1962
+ cmp -s L-pr-evidence-before-schema-invalid.json L-pr-evidence.json \
1963
+ && { PASS=$((PASS+1)); echo " ok Ruff schema-invalid leaves ledger unchanged"; } \
1964
+ || { FAIL=$((FAIL+1)); echo " FAIL Ruff schema-invalid changed the ledger"; }
1965
+ cat > repo-pr-evidence/reports/ruff.json <<'EOF'
1966
+ [{"code":"S101","filename":"app.py","location":"oops"}]
1967
+ EOF
1968
+ cp L-pr-evidence.json L-pr-evidence-before-nested-schema-invalid.json
1969
+ chk "Ruff nested schema-invalid -> invalid evidence exit 2" 2 \
1970
+ run ingest-gate --ledger L-pr-evidence.json --repo pr-evidence --iteration 5 \
1971
+ --ruff repo-pr-evidence/reports/ruff.json
1972
+ cmp -s L-pr-evidence-before-nested-schema-invalid.json L-pr-evidence.json \
1973
+ && { PASS=$((PASS+1)); echo " ok Ruff nested schema-invalid leaves ledger unchanged"; } \
1974
+ || { FAIL=$((FAIL+1)); echo " FAIL Ruff nested schema-invalid changed the ledger"; }
1975
+ for bad_ruff in \
1976
+ '[{"code":42,"filename":"app.py","location":{"row":1}}]' \
1977
+ '[{"code":"S101","filename":[],"location":{"row":1}}]' \
1978
+ '[{"code":"S101","filename":"app.py","location":{"row":"one"}}]'
1979
+ do
1980
+ printf '%s\n' "$bad_ruff" > repo-pr-evidence/reports/ruff.json
1981
+ cp L-pr-evidence.json L-pr-evidence-before-invalid-ruff-field.json
1982
+ chk "Ruff invalid field type -> invalid evidence exit 2" 2 \
1983
+ run ingest-gate --ledger L-pr-evidence.json --repo pr-evidence --iteration 6 \
1984
+ --ruff repo-pr-evidence/reports/ruff.json
1985
+ cmp -s L-pr-evidence-before-invalid-ruff-field.json L-pr-evidence.json \
1986
+ && { PASS=$((PASS+1)); echo " ok Ruff invalid field leaves ledger unchanged"; } \
1987
+ || { FAIL=$((FAIL+1)); echo " FAIL Ruff invalid field changed the ledger"; }
1988
+ done
1989
+ printf '<testsuite tests="1"' > repo-pr-evidence/reports/junit.xml
1990
+ cp L-pr-evidence.json L-pr-evidence-before-invalid-junit.json
1991
+ chk "JUnit XML malformado -> evidencia invalida exit 2" 2 \
1992
+ run snapshot --ledger L-pr-evidence.json --repo pr-evidence
1993
+ cmp -s L-pr-evidence-before-invalid-junit.json L-pr-evidence.json \
1994
+ && { PASS=$((PASS+1)); echo " ok JUnit invalido no persiste snapshot falsamente limpio"; } \
1995
+ || { FAIL=$((FAIL+1)); echo " FAIL invalid JUnit changed the ledger"; }
1996
+ printf '<testsuites><foo/></testsuites>\n' > repo-pr-evidence/reports/junit.xml
1997
+ cp L-pr-evidence.json L-pr-evidence-before-invalid-junit-structure.json
1998
+ chk "JUnit structure-invalid -> invalid evidence exit 2" 2 \
1999
+ run snapshot --ledger L-pr-evidence.json --repo pr-evidence
2000
+ cmp -s L-pr-evidence-before-invalid-junit-structure.json L-pr-evidence.json \
2001
+ && { PASS=$((PASS+1)); echo " ok JUnit structure-invalid leaves ledger unchanged"; } \
2002
+ || { FAIL=$((FAIL+1)); echo " FAIL JUnit structure-invalid changed the ledger"; }
2003
+
2004
+ printf '<testsuite tests="1" failures="0" errors="0" skipped="-1"/>\n' \
2005
+ > repo-pr-evidence/reports/junit.xml
2006
+ cp L-pr-evidence.json L-pr-evidence-before-invalid-junit-counters.json
2007
+ chk "JUnit negative counters -> invalid evidence exit 2" 2 \
2008
+ run snapshot --ledger L-pr-evidence.json --repo pr-evidence
2009
+ cmp -s L-pr-evidence-before-invalid-junit-counters.json L-pr-evidence.json \
2010
+ && { PASS=$((PASS+1)); echo " ok JUnit invalid counters leave ledger unchanged"; } \
2011
+ || { FAIL=$((FAIL+1)); echo " FAIL JUnit invalid counters changed the ledger"; }
2012
+
2013
+ for bad_junit in \
2014
+ '<testsuite tests="1" failures="-1" errors="0" skipped="0"/>' \
2015
+ '<testsuite tests="1" failures="0" errors="-1" skipped="0"/>'
2016
+ do
2017
+ printf '%s\n' "$bad_junit" > repo-pr-evidence/reports/junit.xml
2018
+ cp L-pr-evidence.json L-pr-evidence-before-negative-outcome.json
2019
+ chk "JUnit negative failure/error -> invalid evidence exit 2" 2 \
2020
+ run snapshot --ledger L-pr-evidence.json --repo pr-evidence
2021
+ cmp -s L-pr-evidence-before-negative-outcome.json L-pr-evidence.json \
2022
+ && { PASS=$((PASS+1)); echo " ok JUnit negative outcome leaves ledger unchanged"; } \
2023
+ || { FAIL=$((FAIL+1)); echo " FAIL JUnit negative outcome changed the ledger"; }
2024
+ done
2025
+ printf '<testsuite tests="1" failures="0" errors="0" skipped="2"/>\n' \
2026
+ > repo-pr-evidence/reports/junit.xml
2027
+ cp L-pr-evidence.json L-pr-evidence-before-excess-skipped.json
2028
+ chk "JUnit skipped exceeds tests -> invalid evidence exit 2" 2 \
2029
+ run snapshot --ledger L-pr-evidence.json --repo pr-evidence
2030
+ cmp -s L-pr-evidence-before-excess-skipped.json L-pr-evidence.json \
2031
+ && { PASS=$((PASS+1)); echo " ok JUnit excess skipped leaves ledger unchanged"; } \
2032
+ || { FAIL=$((FAIL+1)); echo " FAIL JUnit excess skipped changed the ledger"; }
2033
+
2034
+ printf '<testsuite tests="2" failures="1" errors="1" skipped="1"/>\n' \
2035
+ > repo-pr-evidence/reports/junit.xml
2036
+ cp L-pr-evidence.json L-pr-evidence-before-impossible-outcomes.json
2037
+ chk "JUnit outcomes exceed executed tests -> invalid evidence exit 2" 2 \
2038
+ run snapshot --ledger L-pr-evidence.json --repo pr-evidence
2039
+ cmp -s L-pr-evidence-before-impossible-outcomes.json L-pr-evidence.json \
2040
+ && { PASS=$((PASS+1)); echo " ok JUnit impossible outcomes leave ledger unchanged"; } \
2041
+ || { FAIL=$((FAIL+1)); echo " FAIL JUnit impossible outcomes changed the ledger"; }
2042
+
2043
+ printf '<testsuites tests="1" failures="1"><testsuite tests="1" errors="1"/></testsuites>\n' > repo-pr-evidence/reports/junit.xml
2044
+ cp L-pr-evidence.json L-pr-evidence-before-inconsistent-root.json
2045
+ chk "JUnit root/child counters inconsistent -> invalid evidence exit 2" 2 run snapshot --ledger L-pr-evidence.json --repo pr-evidence
2046
+ cmp -s L-pr-evidence-before-inconsistent-root.json L-pr-evidence.json \
2047
+ && { PASS=$((PASS+1)); echo " ok JUnit inconsistent root leaves ledger unchanged"; } \
2048
+ || { FAIL=$((FAIL+1)); echo " FAIL JUnit inconsistent root changed the ledger"; }
2049
+ chk "invalid JUnit cannot lead to pr-ready" 1 run phase --ledger L-pr-evidence.json --repo pr-evidence --require pr-ready
2050
+
2051
+ echo "== T69b (1.41.1): well-formed JUnit that LIES (failures=0 attr + real <failure>) reads RED =="
2052
+ "$PY" -c "
2053
+ import sys, os, tempfile
2054
+ sys.path.insert(0, os.path.dirname(sys.argv[1]))
2055
+ import qa_ledger as q
2056
+ def cnt(xml):
2057
+ d = tempfile.mkdtemp(); os.makedirs(os.path.join(d, 'reports'))
2058
+ open(os.path.join(d, 'reports', 'junit.xml'), 'w').write(xml)
2059
+ return q.junit_test_count(d)
2060
+ lie = cnt('<testsuite tests=\"2\" failures=\"0\" errors=\"0\" skipped=\"0\"><testcase name=\"t1\"/><testcase name=\"t2\"><failure message=\"x\"/></testcase></testsuite>')
2061
+ err = cnt('<testsuite tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\"><testcase name=\"t\"><error message=\"x\"/></testcase></testsuite>')
2062
+ adapter = cnt('<testsuites><testsuite name=\"pytest\" tests=\"5\" failures=\"0\" errors=\"0\" skipped=\"1\"/></testsuites>')
2063
+ legit = cnt('<testsuites><testsuite name=\"pytest\" tests=\"3\" failures=\"1\" errors=\"0\" skipped=\"0\"><testcase name=\"a\"/><testcase name=\"b\"><failure/></testcase><testcase name=\"c\"/></testsuite></testsuites>')
2064
+ ok = (lie['failures'] == 1 and err['errors'] == 1
2065
+ and adapter['total'] == 5 and adapter['failures'] == 0
2066
+ and legit['failures'] == 1)
2067
+ sys.exit(0 if ok else 1)" "$QL" \
2068
+ && { PASS=$((PASS+1)); echo " ok <failure>/<error> elements override a lying summary attribute; adapter/legit reports intact"; } \
2069
+ || { FAIL=$((FAIL+1)); echo " FAIL lying JUnit still reads green (element reconciliation broken)"; }
2070
+
2071
+ echo "== T69c (1.41.1): integration readiness does NOT trust the last event (green test cannot mask a failing gate) =="
2072
+ mkdir -p ri/reports
2073
+ printf '{ "defaults": { "acceptance_file": "acc-ri.md" }, "repos": [ {"name":"backend-api","path":"ri","type":"python"} ], "integration": {"enabled": true, "contract_tests_command": "x"} }\n' > ri.json
2074
+ printf -- "# A\n\n- [x] AC-01 x\n" > acc-ri.md
2075
+ run init --config ri.json --out L-ri.json >/dev/null 2>&1
2076
+ run log-step --ledger L-ri.json --repo integration --tool e2e-gate --iteration 1 --reported 3 --gated-reported 3 --tests-passed false >/dev/null 2>&1
2077
+ run log-step --ledger L-ri.json --repo integration --tool e2e-tests --iteration 2 --reported 0 --gated-reported 0 --tests-passed true >/dev/null 2>&1
2078
+ run readiness --ledger L-ri.json --json 2>/dev/null | "$PY" -c "import json,sys; sys.exit(0 if json.load(sys.stdin)['dimensions'].get('integration',{}).get('raw')==0.0 else 1)" \
2079
+ && { PASS=$((PASS+1)); echo " ok a failing integration gate is not masked by a trailing green test (dim=0.0)"; } \
2080
+ || { FAIL=$((FAIL+1)); echo " FAIL integration still trusts the last event (masking not closed)"; }
2081
+ run log-step --ledger L-ri.json --repo integration --tool e2e-gate --iteration 3 --reported 3 --gated-reported 0 >/dev/null 2>&1
2082
+ run readiness --ledger L-ri.json --json 2>/dev/null | "$PY" -c "import json,sys; sys.exit(0 if json.load(sys.stdin)['dimensions'].get('integration',{}).get('raw')==1.0 else 1)" \
2083
+ && { PASS=$((PASS+1)); echo " ok clearing the same-tool gate restores integration to green (no false negative)"; } \
2084
+ || { FAIL=$((FAIL+1)); echo " FAIL a fixed integration gate is not seen"; }
2085
+
2086
+ echo "== T70 static-gate silence does not invent clean evidence =="
2087
+ PHASE_NO_STATIC=$(run phase --ledger L-fsm.json --repo fsm --require pr-ready 2>&1)
2088
+ PHASE_NO_STATIC_RC=$?
2089
+ if [ "$PHASE_NO_STATIC_RC" -eq 0 ]; then
2090
+ PASS=$((PASS+1)); echo " ok measured green tests can reach pr-ready without static reports"
2091
+ else
2092
+ FAIL=$((FAIL+1)); echo " FAIL phase did not reach pr-ready without static reports ($PHASE_NO_STATIC)"
2093
+ fi
2094
+ if echo "$PHASE_NO_STATIC" | grep -q "static gates"; then
2095
+ FAIL=$((FAIL+1)); echo " FAIL phase invented a clean static-gate claim"
2096
+ else
2097
+ PASS=$((PASS+1)); echo " ok qa_tools_order covers agents; static silence is not evidence"
2098
+ fi
2099
+
2100
+ echo "== T71 pr-ready requires at least one executed test =="
2101
+ mkdir -p repo-zero-tests/reports
2102
+ cat > repo-zero-tests/reports/junit.xml <<'EOF'
2103
+ <testsuite tests="0" failures="0" errors="0" skipped="0"/>
2104
+ EOF
2105
+ printf '{ "defaults": {"qa_tools_order":["code-review"]},\n "repos": [ {"name":"zero-tests","path":"repo-zero-tests","type":"python"} ], "integration": {"enabled": false} }\n' > c-zero-tests.json
2106
+ run init --config c-zero-tests.json --out L-zero-tests.json >/dev/null 2>&1
2107
+ run snapshot --ledger L-zero-tests.json --repo zero-tests >/dev/null 2>&1
2108
+ run log-step --ledger L-zero-tests.json --repo zero-tests --tool code-review \
2109
+ --iteration 1 --gated-reported 0 --files-changed 0 >/dev/null 2>&1
2110
+ chk "zero executed tests cannot satisfy pr-ready" 1 \
2111
+ run phase --ledger L-zero-tests.json --repo zero-tests --require pr-ready
2112
+
2113
+ echo "== T72 installer safety =="
2114
+ SAFE_HOME="$SB/home-installer-safe"; SAFE_REPO="$SB/repo-installer-safe"
2115
+ mkdir -p "$SAFE_HOME" "$SAFE_REPO"
2116
+ printf 'keep-existing\n' > "$SAFE_REPO/CLAUDE.md"
2117
+ chk "init conflict exits nonzero" 1 "$PY" "$KIT/install-uscha.py" init --repo "$SAFE_REPO" --json
2118
+ grep -q '^keep-existing$' "$SAFE_REPO/CLAUDE.md" && { PASS=$((PASS+1)); echo " ok init preserves conflict"; } || { FAIL=$((FAIL+1)); echo " FAIL init replaced conflict"; }
2119
+ chk "init dry-run detects conflict" 1 "$PY" "$KIT/install-uscha.py" init --repo "$SAFE_REPO" --dry-run --json
2120
+ chk "init --force replaces conflict" 0 "$PY" "$KIT/install-uscha.py" init --repo "$SAFE_REPO" --force --json
2121
+ mkdir -p "$SAFE_HOME/plugins/uscha" "$SAFE_HOME/.agents/plugins"
2122
+ printf 'preserve-plugin\n' > "$SAFE_HOME/plugins/uscha/sentinel.txt"
2123
+ printf '{ malformed\n' > "$SAFE_HOME/.agents/plugins/marketplace.json"
2124
+ chk "dry-run rejects malformed marketplace" 1 "$PY" "$KIT/install-uscha.py" install --target codex --home "$SAFE_HOME" --dry-run --json
2125
+ test -f "$SAFE_HOME/plugins/uscha/sentinel.txt" && { PASS=$((PASS+1)); echo " ok malformed preflight preserves plugin"; } || { FAIL=$((FAIL+1)); echo " FAIL malformed preflight changed plugin"; }
2126
+ cat > "$SAFE_HOME/.agents/plugins/marketplace.json" <<'EOF'
2127
+ {"name":"personal","interface":{"displayName":"Personal"},"plugins":[{"name":"other"}]}
2128
+ EOF
2129
+ chk "install rejects structurally invalid marketplace" 1 "$PY" "$KIT/install-uscha.py" install --target codex --home "$SAFE_HOME" --json
2130
+ test -f "$SAFE_HOME/plugins/uscha/sentinel.txt" && { PASS=$((PASS+1)); echo " ok structural preflight preserves plugin"; } || { FAIL=$((FAIL+1)); echo " FAIL structural preflight changed plugin"; }
2131
+ rm -f "$SAFE_HOME/.agents/plugins/marketplace.json"
2132
+ chk "healthy Codex install" 0 "$PY" "$KIT/install-uscha.py" install --target codex --home "$SAFE_HOME" --json
2133
+ chk "healthy Codex doctor" 0 "$PY" "$KIT/install-uscha.py" doctor --target codex --home "$SAFE_HOME" --json
2134
+ EMPTY_HOME="$SB/home-installer-unhealthy"; mkdir -p "$EMPTY_HOME"
2135
+ chk "unhealthy doctor text exits 1" 1 "$PY" "$KIT/install-uscha.py" doctor --target codex --home "$EMPTY_HOME"
2136
+ chk "unhealthy doctor JSON exits 1" 1 "$PY" "$KIT/install-uscha.py" doctor --target codex --home "$EMPTY_HOME" --json
2137
+ CLAUDE_HOME="$SB/home-installer-claude"; mkdir -p "$CLAUDE_HOME/.claude"
2138
+ printf '{"theme":"dark","hooks":{"PostToolUse":[{"matcher":"*","hooks":[]}]}}\n' > "$CLAUDE_HOME/.claude/settings.json"
2139
+ chk "Claude install activates hook" 0 "$PY" "$KIT/install-uscha.py" install --target claude --home "$CLAUDE_HOME" --json
2140
+ chk "Claude reinstall is idempotent" 0 "$PY" "$KIT/install-uscha.py" install --target claude --home "$CLAUDE_HOME" --json
2141
+ CLAUDE_HOME="$CLAUDE_HOME" "$PY" -c "import json,os,pathlib,sys; d=json.load(open(pathlib.Path(os.environ['CLAUDE_HOME'])/'.claude/settings.json',encoding='utf-8')); p=d.get('hooks',{}).get('PreToolUse',[]); c=[h.get('command','') for r in p for h in r.get('hooks',[])]; sys.exit(0 if d.get('theme')=='dark' and 'PostToolUse' in d.get('hooks',{}) and sum('block-approved-writes.py' in x for x in c)==1 else 1)" && { PASS=$((PASS+1)); echo " ok settings preserved; hook registered once"; } || { FAIL=$((FAIL+1)); echo " FAIL Claude hook merge"; }
2142
+ chk "healthy Claude doctor" 0 "$PY" "$KIT/install-uscha.py" doctor --target claude --home "$CLAUDE_HOME" --json
2143
+ echo "== T73 mutation-safe input validation =="
2144
+ mkdir -p repo-valid repo-other repo-valid/reports
2145
+ printf '<testsuite tests="1" failures="0" errors="0" skipped="0"/>\n' > repo-valid/reports/junit.xml
2146
+ cat > c-validation.json <<'EOF'
2147
+ {"defaults":{"coverage_threshold":60,"max_iterations":5,"tools_per_cycle":3,"qa_tools_order":["code-review"]},"repos":[{"name":"valid","path":"repo-valid","type":"python"},{"name":"other","path":"repo-other","type":"node"}],"integration":{"enabled":false}}
2148
+ EOF
2149
+ run init --config c-validation.json --out L-validation.json >/dev/null 2>&1
2150
+
2151
+ reject_unchanged() {
2152
+ local desc="$1" ledger="$2"; shift 2
2153
+ cp "$ledger" "$ledger.before"
2154
+ "$@" >/dev/null 2>&1; local got=$?
2155
+ if [ "$got" -ne 0 ] && cmp -s "$ledger.before" "$ledger"; then
2156
+ PASS=$((PASS+1)); echo " ok $desc"
2157
+ else
2158
+ FAIL=$((FAIL+1)); echo " FAIL $desc (exit=$got or ledger changed)"
2159
+ fi
2160
+ }
2161
+
2162
+ reject_unchanged "escalate rejects unknown repo and preserves bytes" L-validation.json \
2163
+ run escalate --ledger L-validation.json --repo missing --reason "invalid target"
2164
+ run production-finding --ledger L-validation.json --repo valid --title "prod" --evidence "log:1" >/dev/null
2165
+ reject_unchanged "PF resolve requires nonempty note and preserves bytes" L-validation.json \
2166
+ run production-finding --ledger L-validation.json --id PF-001 --resolve --note " "
2167
+ chk "PF resolve accepts disposition note" 0 run production-finding --ledger L-validation.json --id PF-001 --resolve --note "triaged into fix"
2168
+
2169
+ run spec-doubt --ledger L-validation.json --repo valid --note "contract mismatch" >/dev/null
2170
+ reject_unchanged "SD resolve requires nonempty decision and preserves bytes" L-validation.json \
2171
+ run spec-doubt --ledger L-validation.json --id SD-001 --resolve --decision " "
2172
+ chk "SD resolve accepts decision" 0 run spec-doubt --ledger L-validation.json --id SD-001 --resolve --decision "SPEC amended"
2173
+
2174
+ run production-finding --ledger L-validation.json --repo valid --title "source" --evidence "log:2" >/dev/null
2175
+ run spec-doubt --ledger L-validation.json --repo other --note "other repo doubt" >/dev/null
2176
+ reject_unchanged "SCR rejects arbitrary source ID and preserves bytes" L-validation.json \
2177
+ run spec-change-request --ledger L-validation.json --repo valid --source BUG-9 --requested-change "change" --evidence "log:3"
2178
+ reject_unchanged "SCR rejects missing PF source and preserves bytes" L-validation.json \
2179
+ run spec-change-request --ledger L-validation.json --repo valid --source PF-999 --requested-change "change" --evidence "log:3"
2180
+ reject_unchanged "SCR rejects cross-repo SD source and preserves bytes" L-validation.json \
2181
+ run spec-change-request --ledger L-validation.json --repo valid --source SD-002 --requested-change "change" --evidence "log:3"
2182
+ chk "SCR accepts same-repo PF source" 0 run spec-change-request --ledger L-validation.json --repo valid --source PF-002 --requested-change "change" --evidence "log:3"
2183
+ for bad_args in "--decision accepted" "--decision rejected" "--decision superseded" "--decision rejected --note ' '"; do
2184
+ cp L-validation.json L-validation-before-scr.json
2185
+ eval "run spec-change-request --ledger L-validation.json --id SCR-001 --resolve $bad_args" >/dev/null 2>&1; rc=$?
2186
+ if [ "$rc" -ne 0 ] && cmp -s L-validation-before-scr.json L-validation.json; then
2187
+ PASS=$((PASS+1)); echo " ok invalid SCR closure rejected without mutation: $bad_args"
2188
+ else
2189
+ FAIL=$((FAIL+1)); echo " FAIL invalid SCR closure mutated ledger: $bad_args"
2190
+ fi
2191
+ done
2192
+ reject_unchanged "SCR resolve requires decision and preserves bytes" L-validation.json \
2193
+ run spec-change-request --ledger L-validation.json --id SCR-001 --resolve
2194
+ chk "SCR accepted requires amended artifact" 0 run spec-change-request --ledger L-validation.json --id SCR-001 --resolve --decision accepted --amended SPEC.md
2195
+
2196
+ run spec-change-request --ledger L-validation.json --repo valid --source PF-002 --requested-change "reject" --evidence "log:4" >/dev/null
2197
+ chk "SCR rejected accepts rationale" 0 run spec-change-request --ledger L-validation.json --id SCR-002 --resolve --decision rejected --note "not aligned"
2198
+ run spec-change-request --ledger L-validation.json --repo valid --source PF-002 --requested-change "replace" --evidence "log:5" >/dev/null
2199
+ chk "SCR superseded accepts replacement reference" 0 run spec-change-request --ledger L-validation.json --id SCR-003 --resolve --decision superseded --amended SPEC-v2.md
2200
+
2201
+ echo "== T73b P1 ledger integrity: readiness config and one-shot closures =="
2202
+ "$PY" "$KIT/tests/ledger-integrity-regressions.py" "$QL" >/dev/null 2>&1 \
2203
+ && { PASS=$((PASS+1)); echo " ok P1 ledger integrity regressions"; } \
2204
+ || { FAIL=$((FAIL+1)); echo " FAIL P1 ledger integrity regressions"; }
2205
+
2206
+ check_bad_config() {
2207
+ local desc="$1" json="$2"
2208
+ printf '%s\n' "$json" > c-bad-validation.json
2209
+ rm -f L-bad-validation.json
2210
+ run init --config c-bad-validation.json --out L-bad-validation.json >/dev/null 2>&1; local got=$?
2211
+ if [ "$got" -ne 0 ] && [ ! -e L-bad-validation.json ]; then
2212
+ PASS=$((PASS+1)); echo " ok $desc"
2213
+ else
2214
+ FAIL=$((FAIL+1)); echo " FAIL $desc (exit=$got or output created)"
2215
+ fi
2216
+ }
2217
+ check_bad_config "repos must be a list" '{"repos":{}}'
2218
+ check_bad_config "repo entries must be objects" '{"repos":["x"]}'
2219
+ check_bad_config "repo names must be nonempty" '{"repos":[{"name":" ","path":"x","type":"python"}]}'
2220
+ check_bad_config "repo names must be unique" '{"repos":[{"name":"x","path":"a","type":"python"},{"name":"x","path":"b","type":"node"}]}'
2221
+ check_bad_config "integration is a reserved repo name" '{"repos":[{"name":"integration","path":"x","type":"python"}]}'
2222
+ check_bad_config "repo paths must be nonempty" '{"repos":[{"name":"x","path":" ","type":"python"}]}'
2223
+ check_bad_config "repo types must be supported" '{"repos":[{"name":"x","path":"x","type":"ruby"}]}'
2224
+ check_bad_config "qa_tools_order must be nonempty" '{"defaults":{"qa_tools_order":[]},"repos":[]}'
2225
+ check_bad_config "qa_tools_order entries must be strings" '{"defaults":{"qa_tools_order":[1]},"repos":[]}'
2226
+ check_bad_config "qa_tools_order entries must be unique and nonempty" '{"defaults":{"qa_tools_order":["x","x"]},"repos":[]}'
2227
+ check_bad_config "qa_tools_order entries cannot be blank" '{"defaults":{"qa_tools_order":[" "]},"repos":[]}'
2228
+ for supported_type in maven flutter python node go rust dotnet cpp gradle swift; do
2229
+ printf '{"repos":[{"name":"x","path":"x","type":"%s"}]}\n' "$supported_type" > c-supported.json
2230
+ chk "supported repo type: $supported_type" 0 run init --config c-supported.json --out L-supported.json
2231
+ done
2232
+ check_bad_config "coverage threshold cannot be negative" '{"defaults":{"coverage_threshold":-1},"repos":[]}'
2233
+ check_bad_config "coverage threshold cannot exceed 100" '{"defaults":{"coverage_threshold":101},"repos":[]}'
2234
+ check_bad_config "max_iterations must be positive" '{"defaults":{"max_iterations":0},"repos":[]}'
2235
+ check_bad_config "tools_per_cycle must be positive" '{"defaults":{"tools_per_cycle":0},"repos":[]}'
2236
+
2237
+ for cmd in \
2238
+ "log-step --repo valid --tool code-review --iteration 0" \
2239
+ "ingest-gate --repo valid --iteration 0 --ruff repo-valid/reports/ruff.json" \
2240
+ "log-gate --repo valid --iteration 0 --kind regression --verdict pass" \
2241
+ "flag-blocker --repo valid --iteration 0 --note breach" \
2242
+ "rubric-ingest --repo valid --iteration 0 --report missing.json"
2243
+ do
2244
+ cp L-validation.json L-validation-before-iteration.json
2245
+ eval "run $cmd --ledger L-validation.json" >/dev/null 2>&1; rc=$?
2246
+ if [ "$rc" -ne 0 ] && cmp -s L-validation-before-iteration.json L-validation.json; then
2247
+ PASS=$((PASS+1)); echo " ok iteration <1 rejected without mutation: $cmd"
2248
+ else
2249
+ FAIL=$((FAIL+1)); echo " FAIL iteration <1 changed ledger: $cmd"
2250
+ fi
2251
+ done
2252
+
2253
+ printf '[{"code":"E501","filename":"x.py","location":{"row":1}}]\n' > repo-valid/reports/ruff.json
2254
+ chk "newer ingest-gate state accepted" 0 run ingest-gate --ledger L-validation.json --repo valid --iteration 3 --ruff repo-valid/reports/ruff.json
2255
+ reject_unchanged "older ingest-gate iteration cannot supersede newer state" L-validation.json \
2256
+ run ingest-gate --ledger L-validation.json --repo valid --iteration 2 --ruff repo-valid/reports/ruff.json
2257
+ chk "newer blocker state accepted" 0 run flag-blocker --ledger L-validation.json --repo valid --kind t73 --iteration 3 --note breach
2258
+ reject_unchanged "older blocker iteration cannot supersede newer state" L-validation.json \
2259
+ run flag-blocker --ledger L-validation.json --repo valid --kind t73 --iteration 2 --note older
2260
+ cat > T73-RUBRIC.md <<'EOF'
2261
+ # T73 rubric
2262
+ threshold: 0.50
2263
+ - [ ] RB-01 (peso 1) - evidence
2264
+ EOF
2265
+ cat > t73-grader.json <<'EOF'
2266
+ {"criteria":[{"id":"RB-01","verdict":"pass","evidence":"x.py:1","note":"ok"}]}
2267
+ EOF
2268
+ chk "newer rubric state accepted" 0 run rubric-ingest --ledger L-validation.json --repo valid --iteration 3 --rubric T73-RUBRIC.md --report t73-grader.json
2269
+ reject_unchanged "older rubric iteration cannot supersede newer state" L-validation.json \
2270
+ run rubric-ingest --ledger L-validation.json --repo valid --iteration 2 --rubric T73-RUBRIC.md --report t73-grader.json
2271
+
2272
+ for bad_counts in "--reported -1" "--gated-reported -1" "--fixed -1" "--deferred -1" \
2273
+ "--suppressed -1" "--files-changed -1" "--reported 1 --gated-reported 2"
2274
+ do
2275
+ cp L-validation.json L-validation-before-counts.json
2276
+ eval "run log-step --ledger L-validation.json --repo valid --tool code-review --iteration 1 $bad_counts" >/dev/null 2>&1; rc=$?
2277
+ if [ "$rc" -ne 0 ] && cmp -s L-validation-before-counts.json L-validation.json; then
2278
+ PASS=$((PASS+1)); echo " ok invalid log-step counters rejected: $bad_counts"
2279
+ else
2280
+ FAIL=$((FAIL+1)); echo " FAIL invalid log-step counters changed ledger: $bad_counts"
2281
+ fi
2282
+ done
2283
+ reject_unchanged "negative FACT-gate count rejected without mutation" L-validation.json \
2284
+ run log-gate --ledger L-validation.json --repo valid --iteration 1 --kind regression --verdict fail --count -1
2285
+ chk "log-step allows fixed above reported" 0 run log-step --ledger L-validation.json --repo valid --tool code-review --iteration 2 --reported 1 --fixed 2
2286
+ reject_unchanged "older log-step iteration cannot supersede newer state" L-validation.json \
2287
+ run log-step --ledger L-validation.json --repo valid --tool code-review --iteration 1
2288
+ chk "same-cycle retry remains valid" 0 run log-step --ledger L-validation.json --repo valid --tool code-review --iteration 2
2289
+ chk "newer FACT-gate state accepted" 0 run log-gate --ledger L-validation.json --repo valid --iteration 3 --kind regression --verdict fail --count 1
2290
+ reject_unchanged "older FACT-gate iteration cannot supersede newer state" L-validation.json \
2291
+ run log-gate --ledger L-validation.json --repo valid --iteration 2 --kind regression --verdict pass
2292
+ cat > c-integration-readiness.json <<'EOF'
2293
+ {"defaults":{"qa_tools_order":["code-review"]},"repos":[],"integration":{"enabled":true}}
2294
+ EOF
2295
+ run init --config c-integration-readiness.json --out L-integration-readiness.json >/dev/null
2296
+ run log-step --ledger L-integration-readiness.json --repo integration --tool integration-tests \
2297
+ --iteration 1 --tests-passed false >/dev/null
2298
+ run log-gate --ledger L-integration-readiness.json --repo integration --iteration 1 \
2299
+ --kind gate-check --verdict pass >/dev/null
2300
+ run readiness --ledger L-integration-readiness.json --json | "$PY" -c \
2301
+ "import json,sys; sys.exit(0 if json.load(sys.stdin)['dimensions']['integration']['raw'] == 0 else 1)" \
2302
+ && { PASS=$((PASS+1)); echo " ok integration failure survives later non-test event"; } \
2303
+ || { FAIL=$((FAIL+1)); echo " FAIL integration failure hidden by later non-test event"; }
2304
+ run log-step --ledger L-integration-readiness.json --repo integration --tool integration-tests \
2305
+ --iteration 2 --tests-passed true >/dev/null
2306
+ run readiness --ledger L-integration-readiness.json --json | "$PY" -c \
2307
+ "import json,sys; sys.exit(0 if json.load(sys.stdin)['dimensions']['integration']['raw'] == 1 else 1)" \
2308
+ && { PASS=$((PASS+1)); echo " ok newer measured integration success recovers readiness"; } \
2309
+ || { FAIL=$((FAIL+1)); echo " FAIL newer measured integration success did not recover readiness"; }
2310
+ echo "== T74 npm router: probes Python before one installer invocation =="
2311
+ if command -v node >/dev/null 2>&1; then
2312
+ ROUTER_BIN="$SB/router-bin"; ROUTER_LOG="$SB/router.log"
2313
+ ROUTER_FIXTURE_READY=1
2314
+ mkdir -p "$ROUTER_BIN"
2315
+ if [ "$(node -p 'process.platform')" = "win32" ]; then
2316
+ cat > "$ROUTER_BIN/fake-python.cs" <<'EOF'
2317
+ using System;
2318
+ using System.Diagnostics;
2319
+ using System.IO;
2320
+ class FakePython {
2321
+ static int Main(string[] args) {
2322
+ string role = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().MainModule.FileName).ToLowerInvariant();
2323
+ File.AppendAllText(Environment.GetEnvironmentVariable("USCHA_TEST_LOG"), role + " " + string.Join(" ", args) + "\r\n");
2324
+ if (Array.IndexOf(args, "--version") >= 0) {
2325
+ if (role == "python") return 1;
2326
+ Console.WriteLine("Python 3.8.0");
2327
+ return 0;
2328
+ }
2329
+ return role == "python" ? 9 : 0;
2330
+ }
2331
+ }
2332
+ EOF
2333
+ CSHARP_ROOT="${WINDIR:-C:/Windows}"
2334
+ command -v cygpath >/dev/null 2>&1 || ROUTER_FIXTURE_READY=0
2335
+ if [ "$ROUTER_FIXTURE_READY" -eq 1 ] && printf '%s' "$CSHARP_ROOT" | grep -Eq '^[A-Za-z]:'; then CSHARP_ROOT="$(cygpath -u "$CSHARP_ROOT")"; fi
2336
+ CSHARP_COMPILER="$CSHARP_ROOT/Microsoft.NET/Framework64/v4.0.30319/csc.exe"
2337
+ [ -x "$CSHARP_COMPILER" ] || CSHARP_COMPILER="$CSHARP_ROOT/Microsoft.NET/Framework/v4.0.30319/csc.exe"
2338
+ [ -x "$CSHARP_COMPILER" ] || ROUTER_FIXTURE_READY=0
2339
+ if [ "$ROUTER_FIXTURE_READY" -eq 1 ]; then
2340
+ CSHARP_OUTPUT="$(cygpath -w "$ROUTER_BIN/python.exe")"
2341
+ CSHARP_SOURCE="$(cygpath -w "$ROUTER_BIN/fake-python.cs")"
2342
+ MSYS_NO_PATHCONV=1 "$CSHARP_COMPILER" /nologo "/out:$CSHARP_OUTPUT" "$CSHARP_SOURCE" >/dev/null 2>&1 \
2343
+ && cp "$ROUTER_BIN/python.exe" "$ROUTER_BIN/py.exe" \
2344
+ || ROUTER_FIXTURE_READY=0
2345
+ fi
2346
+ ROUTER_PRIMARY="python"; ROUTER_FALLBACK="py -3"
2347
+ else
2348
+ cat > "$ROUTER_BIN/python3" <<'EOF'
2349
+ #!/usr/bin/env sh
2350
+ printf 'python3 %s\n' "$*" >> "$USCHA_TEST_LOG"
2351
+ [ "$1" = "--version" ] && exit 1
2352
+ exit 9
2353
+ EOF
2354
+ cat > "$ROUTER_BIN/python" <<'EOF'
2355
+ #!/usr/bin/env sh
2356
+ printf 'python %s\n' "$*" >> "$USCHA_TEST_LOG"
2357
+ if [ "$1" = "--version" ]; then echo "Python 3.8.0"; exit 0; fi
2358
+ exit 0
2359
+ EOF
2360
+ chmod +x "$ROUTER_BIN/python3" "$ROUTER_BIN/python"
2361
+ ROUTER_PRIMARY="python3"; ROUTER_FALLBACK="python"
2362
+ fi
2363
+ if [ "$ROUTER_FIXTURE_READY" -eq 1 ]; then
2364
+ PATH="$ROUTER_BIN:$PATH" USCHA_TEST_LOG="$ROUTER_LOG" node "$ROOT/bin/uscha.js" version >/dev/null 2>&1; ROUTER_RC=$?
2365
+ else
2366
+ ROUTER_RC=99
2367
+ fi
2368
+ if [ "$ROUTER_FIXTURE_READY" -eq 1 ] && [ "$ROUTER_RC" -eq 0 ] && grep -Fxq "$ROUTER_PRIMARY --version" "$ROUTER_LOG" \
2369
+ && grep -Fxq "$ROUTER_FALLBACK --version" "$ROUTER_LOG" \
2370
+ && [ "$(grep -F "$ROUTER_FALLBACK " "$ROUTER_LOG" | grep -c 'install-uscha.py version')" -eq 1 ] \
2371
+ && [ "$(grep -F "$ROUTER_PRIMARY " "$ROUTER_LOG" | grep -c 'install-uscha.py version')" -eq 0 ] \
2372
+ && [ "$(grep -c 'install-uscha.py version' "$ROUTER_LOG")" -eq 1 ]; then
2373
+ PASS=$((PASS+1)); echo " ok unusable first Python falls through to verified fallback once"
2374
+ else
2375
+ FAIL=$((FAIL+1)); echo " FAIL router did not probe/fallback exactly once"
2376
+ fi
2377
+ else
2378
+ FAIL=$((FAIL+1)); echo " FAIL node no esta disponible para probar fallback del router npm"
2379
+ fi
2380
+ echo "== T75 npm router: installer failure is not retried =="
2381
+ if command -v node >/dev/null 2>&1; then
2382
+ ROUTER_BIN="$SB/router-failure-bin"; ROUTER_LOG="$SB/router-failure.log"
2383
+ ROUTER_FIXTURE_READY=1
2384
+ mkdir -p "$ROUTER_BIN"
2385
+ if [ "$(node -p 'process.platform')" = "win32" ]; then
2386
+ cat > "$ROUTER_BIN/fake-python.cs" <<'EOF'
2387
+ using System;
2388
+ using System.Diagnostics;
2389
+ using System.IO;
2390
+ class FakePython {
2391
+ static int Main(string[] args) {
2392
+ string role = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().MainModule.FileName).ToLowerInvariant();
2393
+ File.AppendAllText(Environment.GetEnvironmentVariable("USCHA_TEST_LOG"), role + " " + string.Join(" ", args) + "\r\n");
2394
+ if (Array.IndexOf(args, "--version") >= 0) { Console.WriteLine("Python 3.8.0"); return 0; }
2395
+ return role == "python" ? 23 : 0;
2396
+ }
2397
+ }
2398
+ EOF
2399
+ CSHARP_ROOT="${WINDIR:-C:/Windows}"
2400
+ command -v cygpath >/dev/null 2>&1 || ROUTER_FIXTURE_READY=0
2401
+ if [ "$ROUTER_FIXTURE_READY" -eq 1 ] && printf '%s' "$CSHARP_ROOT" | grep -Eq '^[A-Za-z]:'; then CSHARP_ROOT="$(cygpath -u "$CSHARP_ROOT")"; fi
2402
+ CSHARP_COMPILER="$CSHARP_ROOT/Microsoft.NET/Framework64/v4.0.30319/csc.exe"
2403
+ [ -x "$CSHARP_COMPILER" ] || CSHARP_COMPILER="$CSHARP_ROOT/Microsoft.NET/Framework/v4.0.30319/csc.exe"
2404
+ [ -x "$CSHARP_COMPILER" ] || ROUTER_FIXTURE_READY=0
2405
+ if [ "$ROUTER_FIXTURE_READY" -eq 1 ]; then
2406
+ CSHARP_OUTPUT="$(cygpath -w "$ROUTER_BIN/python.exe")"
2407
+ CSHARP_SOURCE="$(cygpath -w "$ROUTER_BIN/fake-python.cs")"
2408
+ MSYS_NO_PATHCONV=1 "$CSHARP_COMPILER" /nologo "/out:$CSHARP_OUTPUT" "$CSHARP_SOURCE" >/dev/null 2>&1 \
2409
+ && cp "$ROUTER_BIN/python.exe" "$ROUTER_BIN/py.exe" \
2410
+ || ROUTER_FIXTURE_READY=0
2411
+ fi
2412
+ ROUTER_PRIMARY="python"; ROUTER_FALLBACK="py -3"
2413
+ else
2414
+ cat > "$ROUTER_BIN/python3" <<'EOF'
2415
+ #!/usr/bin/env sh
2416
+ printf 'python3 %s\n' "$*" >> "$USCHA_TEST_LOG"
2417
+ if [ "$1" = "--version" ]; then echo "Python 3.8.0"; exit 0; fi
2418
+ exit 23
2419
+ EOF
2420
+ cat > "$ROUTER_BIN/python" <<'EOF'
2421
+ #!/usr/bin/env sh
2422
+ printf 'python %s\n' "$*" >> "$USCHA_TEST_LOG"
2423
+ if [ "$1" = "--version" ]; then echo "Python 3.8.0"; exit 0; fi
2424
+ exit 0
2425
+ EOF
2426
+ chmod +x "$ROUTER_BIN/python3" "$ROUTER_BIN/python"
2427
+ ROUTER_PRIMARY="python3"; ROUTER_FALLBACK="python"
2428
+ fi
2429
+ if [ "$ROUTER_FIXTURE_READY" -eq 1 ]; then
2430
+ PATH="$ROUTER_BIN:$PATH" USCHA_TEST_LOG="$ROUTER_LOG" node "$ROOT/bin/uscha.js" version >/dev/null 2>&1; ROUTER_RC=$?
2431
+ else
2432
+ ROUTER_RC=99
2433
+ fi
2434
+ if [ "$ROUTER_FIXTURE_READY" -eq 1 ] && [ "$ROUTER_RC" -eq 23 ] && grep -Fxq "$ROUTER_PRIMARY --version" "$ROUTER_LOG" \
2435
+ && [ "$(grep -F "$ROUTER_PRIMARY " "$ROUTER_LOG" | grep -c 'install-uscha.py version')" -eq 1 ] \
2436
+ && ! grep -Fq "$ROUTER_FALLBACK" "$ROUTER_LOG"; then
2437
+ PASS=$((PASS+1)); echo " ok installer exit 23 is preserved without another interpreter"
2438
+ else
2439
+ FAIL=$((FAIL+1)); echo " FAIL router retried or changed installer failure status"
2440
+ fi
2441
+ else
2442
+ FAIL=$((FAIL+1)); echo " FAIL node no esta disponible para probar failure del router npm"
2443
+ fi
2444
+ echo "== T76 workbench doctor: usable Python fallback and source skill roster =="
2445
+ DOCTOR_BIN="$SB/doctor-bin"; DOCTOR_HOME="$SB/doctor-home"
2446
+ mkdir -p "$DOCTOR_BIN" "$DOCTOR_HOME/.claude/skills"
2447
+ cat > "$DOCTOR_BIN/python3" <<'EOF'
2448
+ #!/usr/bin/env sh
2449
+ echo "Python 2.7.18"
2450
+ EOF
2451
+ if case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) true;; *) false;; esac; then
2452
+ cat > "$DOCTOR_BIN/python" <<'EOF'
2453
+ #!/usr/bin/env sh
2454
+ echo "Python 2.7.18"
2455
+ EOF
2456
+ cat > "$DOCTOR_BIN/py" <<'EOF'
2457
+ #!/usr/bin/env sh
2458
+ if [ "$#" -eq 2 ] && [ "$1" = "-3" ] && [ "$2" = "--version" ]; then
2459
+ echo "Python 3.11.9"
2460
+ exit 0
2461
+ fi
2462
+ exit 1
2463
+ EOF
2464
+ chmod +x "$DOCTOR_BIN/python" "$DOCTOR_BIN/py"
2465
+ else
2466
+ cat > "$DOCTOR_BIN/python" <<'EOF'
2467
+ #!/usr/bin/env sh
2468
+ echo "Python 3.11.9"
2469
+ EOF
2470
+ chmod +x "$DOCTOR_BIN/python"
2471
+ fi
2472
+ chmod +x "$DOCTOR_BIN/python3"
2473
+ DOCTOR_SKILL_COUNT=0
2474
+ for source_skill in "$KIT/.claude/skills"/uscha-*; do
2475
+ [ -d "$source_skill" ] || continue
2476
+ skill_name="$(basename "$source_skill")"
2477
+ mkdir -p "$DOCTOR_HOME/.claude/skills/$skill_name"
2478
+ : > "$DOCTOR_HOME/.claude/skills/$skill_name/SKILL.md"
2479
+ DOCTOR_SKILL_COUNT=$((DOCTOR_SKILL_COUNT+1))
2480
+ done
2481
+ DOCTOR_OUT="$(HOME="$DOCTOR_HOME" PATH="$DOCTOR_BIN:$PATH" bash "$KIT/workbench-doctor.sh")"
2482
+ DOCTOR_SKILLS_OK=1
2483
+ for source_skill in "$KIT/.claude/skills"/uscha-*; do
2484
+ [ -d "$source_skill" ] || continue
2485
+ skill_name="$(basename "$source_skill")"
2486
+ printf '%s\n' "$DOCTOR_OUT" | grep -Fq "$skill_name" || DOCTOR_SKILLS_OK=0
2487
+ done
2488
+ if [ "$DOCTOR_SKILL_COUNT" -gt 0 ] && [ "$DOCTOR_SKILLS_OK" -eq 1 ] \
2489
+ && printf '%s\n' "$DOCTOR_OUT" | grep -Fq "Python 3.8+" \
2490
+ && printf '%s\n' "$DOCTOR_OUT" | grep -Fq "Python 3.11.9" \
2491
+ && ! printf '%s\n' "$DOCTOR_OUT" | grep -Fq "Python 2.7.18"; then
2492
+ PASS=$((PASS+1)); echo " ok doctor uses Python >=3.8 fallback and current uscha-* skill roster"
2493
+ else
2494
+ FAIL=$((FAIL+1)); echo " FAIL doctor did not use source roster or usable Python fallback"
2495
+ fi
2496
+
2497
+ echo "== T77 Claude install rolls back the complete managed target on a late failure =="
2498
+ ROLLBACK_HOME="$SB/home-installer-claude-rollback"
2499
+ ROLLBACK_ROOT="$ROLLBACK_HOME/.claude"
2500
+ ROLLBACK_FAULT="$SB/claude-rollback-fault"
2501
+ mkdir -p "$ROLLBACK_ROOT/skills/unrelated-skill" "$ROLLBACK_ROOT/hooks" "$ROLLBACK_FAULT"
2502
+ for source_skill in "$KIT/.claude/skills"/uscha-*; do
2503
+ [ -d "$source_skill" ] || continue
2504
+ skill_name="$(basename "$source_skill")"
2505
+ mkdir -p "$ROLLBACK_ROOT/skills/$skill_name"
2506
+ printf 'previous-%s\n' "$skill_name" > "$ROLLBACK_ROOT/skills/$skill_name/sentinel.bin"
2507
+ done
2508
+ printf 'unrelated-skill\n' > "$ROLLBACK_ROOT/skills/unrelated-skill/sentinel.bin"
2509
+ printf 'previous-hook\n' > "$ROLLBACK_ROOT/hooks/block-approved-writes.py"
2510
+ printf 'unrelated-file\n' > "$ROLLBACK_ROOT/unrelated.bin"
2511
+ printf '{"theme":"sentinel","permissions":{"allow":["Read"]},"hooks":{"PostToolUse":[{"matcher":"*","hooks":[]}]}}\n' > "$ROLLBACK_ROOT/settings.json"
2512
+ printf 'previous-marker\n' > "$ROLLBACK_ROOT/uscha-install.json"
2513
+ cat > "$ROLLBACK_FAULT/sitecustomize.py" <<'PY'
2514
+ import os
2515
+
2516
+ _replace = os.replace
2517
+ _failed = False
2518
+
2519
+
2520
+ def fail_at_claude_marker(source, target):
2521
+ global _failed
2522
+ normalized = os.path.normcase(os.path.normpath(os.fspath(target)))
2523
+ marker_suffix = os.path.normcase(os.path.normpath(os.path.join(".claude", "uscha-install.json")))
2524
+ if not _failed and normalized.endswith(marker_suffix):
2525
+ _failed = True
2526
+ root = os.path.dirname(normalized)
2527
+ replaced = os.path.isfile(os.path.join(root, "skills", "uscha-discovery", "SKILL.md"))
2528
+ old_gone = not os.path.exists(os.path.join(root, "skills", "uscha-discovery", "sentinel.bin"))
2529
+ with open(os.environ["USCHA_FAULT_WITNESS"], "w", encoding="utf-8", newline="\n") as handle:
2530
+ handle.write("late\n" if replaced and old_gone else "too-early\n")
2531
+ raise OSError("deterministic late Claude marker failure")
2532
+ return _replace(source, target)
2533
+
2534
+
2535
+ os.replace = fail_at_claude_marker
2536
+ PY
2537
+ cat > "$ROLLBACK_FAULT/snapshot.py" <<'PY'
2538
+ import base64
2539
+ import json
2540
+ import os
2541
+ import pathlib
2542
+ import stat
2543
+ import sys
2544
+
2545
+ root = pathlib.Path(sys.argv[1])
2546
+ snapshot = []
2547
+ for path in sorted(root.rglob("*"), key=lambda item: item.as_posix()):
2548
+ relative = path.relative_to(root).as_posix()
2549
+ info = path.lstat()
2550
+ if path.is_symlink():
2551
+ snapshot.append([relative, "symlink", os.readlink(path)])
2552
+ elif path.is_dir():
2553
+ snapshot.append([relative, "dir", stat.S_IMODE(info.st_mode)])
2554
+ else:
2555
+ snapshot.append([relative, "file", stat.S_IMODE(info.st_mode),
2556
+ base64.b64encode(path.read_bytes()).decode("ascii")])
2557
+ json.dump(snapshot, sys.stdout, separators=(",", ":"))
2558
+ sys.stdout.write("\n")
2559
+ PY
2560
+ "$PY" "$ROLLBACK_FAULT/snapshot.py" "$ROLLBACK_HOME" > "$ROLLBACK_FAULT/before.json"
2561
+ PYTHONPATH="$ROLLBACK_FAULT" USCHA_FAULT_WITNESS="$ROLLBACK_FAULT/witness.txt" \
2562
+ "$PY" "$KIT/install-uscha.py" install --target claude --home "$ROLLBACK_HOME" --json \
2563
+ > "$ROLLBACK_FAULT/stdout.txt" 2> "$ROLLBACK_FAULT/stderr.txt"
2564
+ ROLLBACK_RC=$?
2565
+ "$PY" "$ROLLBACK_FAULT/snapshot.py" "$ROLLBACK_HOME" > "$ROLLBACK_FAULT/after.json"
2566
+ if [ "$ROLLBACK_RC" -ne 0 ] \
2567
+ && grep -Fxq 'late' "$ROLLBACK_FAULT/witness.txt" \
2568
+ && cmp -s "$ROLLBACK_FAULT/before.json" "$ROLLBACK_FAULT/after.json"; then
2569
+ PASS=$((PASS+1)); echo " ok late Claude failure restores the full managed tree and unrelated state"
2570
+ else
2571
+ FAIL=$((FAIL+1)); echo " FAIL late Claude failure left a partial install or changed unrelated state"
2572
+ fi
2573
+
2574
+ rm -f "$ROLLBACK_ROOT/uscha-install.json" "$ROLLBACK_FAULT/witness.txt"
2575
+ "$PY" "$ROLLBACK_FAULT/snapshot.py" "$ROLLBACK_HOME" > "$ROLLBACK_FAULT/before-no-marker.json"
2576
+ PYTHONPATH="$ROLLBACK_FAULT" USCHA_FAULT_WITNESS="$ROLLBACK_FAULT/witness.txt" \
2577
+ "$PY" "$KIT/install-uscha.py" install --target claude --home "$ROLLBACK_HOME" --json \
2578
+ > "$ROLLBACK_FAULT/stdout-no-marker.txt" 2> "$ROLLBACK_FAULT/stderr-no-marker.txt"
2579
+ ROLLBACK_NO_MARKER_RC=$?
2580
+ "$PY" "$ROLLBACK_FAULT/snapshot.py" "$ROLLBACK_HOME" > "$ROLLBACK_FAULT/after-no-marker.json"
2581
+ if [ "$ROLLBACK_NO_MARKER_RC" -ne 0 ] \
2582
+ && grep -Fxq 'late' "$ROLLBACK_FAULT/witness.txt" \
2583
+ && [ ! -e "$ROLLBACK_ROOT/uscha-install.json" ] \
2584
+ && cmp -s "$ROLLBACK_FAULT/before-no-marker.json" "$ROLLBACK_FAULT/after-no-marker.json"; then
2585
+ PASS=$((PASS+1)); echo " ok late Claude failure does not leave a newly created marker"
2586
+ else
2587
+ FAIL=$((FAIL+1)); echo " FAIL late Claude failure left a marker that did not exist before"
2588
+ fi
2589
+
2590
+ echo "== T77b (1.41.1): Codex install rollback restores the pre-existing plugin (data-loss fix) =="
2591
+ "$PY" - "$KIT/install-uscha.py" <<'PY'
2592
+ import importlib.util, os, sys, shutil, tempfile, pathlib
2593
+ spec = importlib.util.spec_from_file_location("iu", sys.argv[1])
2594
+ iu = importlib.util.module_from_spec(spec); spec.loader.exec_module(iu)
2595
+ home = pathlib.Path(tempfile.mkdtemp())
2596
+ try:
2597
+ plugin = home / "plugins" / iu.PLUGIN_NAME
2598
+ plugin.mkdir(parents=True)
2599
+ (plugin / "SENTINEL.txt").write_text("ORIGINAL")
2600
+ real = os.replace
2601
+ def failing(src, dst, *a, **k):
2602
+ # only the stage->plugin swap fails (e.g. a Windows AV lock on the fresh stage
2603
+ # tree); the backup->plugin restore must still succeed
2604
+ if pathlib.Path(dst) == plugin and "staging" in str(src):
2605
+ raise PermissionError("simulated stage-swap failure")
2606
+ return real(src, dst, *a, **k)
2607
+ iu.os.replace = failing
2608
+ raised = False
2609
+ try:
2610
+ iu.install_codex(home, "copy", False, [])
2611
+ except BaseException:
2612
+ raised = True
2613
+ finally:
2614
+ iu.os.replace = real
2615
+ s = plugin / "SENTINEL.txt"
2616
+ ok = (raised and s.exists() and s.read_text() == "ORIGINAL"
2617
+ and not list((home / "plugins").glob(".*backup*")))
2618
+ sys.exit(0 if ok else 1)
2619
+ finally:
2620
+ shutil.rmtree(home, ignore_errors=True)
2621
+ PY
2622
+ if [ $? -eq 0 ]; then PASS=$((PASS+1)); echo " ok a failed Codex swap restores the user's pre-existing plugin (no data loss)"; \
2623
+ else FAIL=$((FAIL+1)); echo " FAIL Codex rollback lost or stranded the pre-existing plugin"; fi
2624
+
2625
+ echo "== T78 (1.41.2): NOT READY title is score-aware -- never says 'no arranca' once started =="
2626
+ # isolated subdir: dashboard scans the CWD for ADRs/specs, so run away from the shared sandbox
2627
+ mkdir -p title-sb && ( cd title-sb
2628
+ printf -- "# ACCEPTANCE\n\n- [ ] uno\n- [ ] dos\n" > title-acc.md
2629
+ printf '{ "defaults": { "acceptance_file": "title-acc.md" },\n "repos": [ {"name":"solo","path":"r","type":"python"} ], "integration": {"enabled": false} }\n' > title-cfg.json
2630
+ run init --config title-cfg.json --out L-title-a.json >/dev/null 2>&1
2631
+ # A: virgin ledger -> score 0 -> "sin evidencia medida", NOT "no arranca"
2632
+ run dashboard --ledger L-title-a.json --json 2>/dev/null | "$PY" -c "
2633
+ import json, sys
2634
+ r = json.load(sys.stdin)['readiness']
2635
+ t = r['title'] or ''
2636
+ sys.exit(0 if (r['band'] == 'NOT READY' and (r['score'] or 0) == 0
2637
+ and 'arranca' not in t and 'sin evidencia' in t) else 1)" \
2638
+ && { PASS=$((PASS+1)); echo " ok score 0 -> 'sin evidencia medida' (no 'no arranca')"; } \
2639
+ || { FAIL=$((FAIL+1)); echo " FAIL titulo virgen incorrecto"; }
2640
+ # B: one passing gate -> score >0 but still NOT READY -> "en construccion", never "no arranca"
2641
+ cp L-title-a.json L-title-b.json
2642
+ run log-gate --repo solo --iteration 1 --kind simplicity --verdict pass --ledger L-title-b.json >/dev/null 2>&1
2643
+ run dashboard --ledger L-title-b.json --json 2>/dev/null | "$PY" -c "
2644
+ import json, sys
2645
+ r = json.load(sys.stdin)['readiness']
2646
+ t = r['title'] or ''
2647
+ sys.exit(0 if (r['band'] == 'NOT READY' and 0 < (r['score'] or 0) < 50
2648
+ and 'arranca' not in t and 'construccion' in t) else 1)" \
2649
+ && { PASS=$((PASS+1)); echo " ok score >0 NOT READY -> 'en construccion' (no 'no arranca')"; } \
2650
+ || { FAIL=$((FAIL+1)); echo " FAIL titulo iniciado incorrecto"; }
2651
+ echo "$PASS $FAIL" > title-counts.txt )
2652
+ read PASS FAIL < title-sb/title-counts.txt
2653
+
2654
+ echo "== T79 (1.41.3): --refresh (live) never auto-opens -- one open tab self-reloads, no tab spam =="
2655
+ "$PY" - "$KIT/.claude/skills/uscha-mirador/mirador-render.py" "$KIT/.claude/skills/uscha-mirador/mirador.template.html" <<'PY'
2656
+ import importlib.util, os, pathlib, shutil, sys, tempfile
2657
+ render_path, tpl = sys.argv[1], sys.argv[2]
2658
+ spec = importlib.util.spec_from_file_location("mr", render_path)
2659
+ mr = importlib.util.module_from_spec(spec); spec.loader.exec_module(mr)
2660
+ tmp = pathlib.Path(tempfile.mkdtemp())
2661
+ eng = tmp / "eng.py"
2662
+ eng.write_text('import sys; sys.stdout.write(\'{"readiness":{"score":22,"band":"NOT READY","title":"x"}}\')\n', encoding="utf-8")
2663
+ opens = {"n": 0}
2664
+ mr._open_best_effort = lambda p: opens.__setitem__("n", opens["n"] + 1) # count instead of launching a browser
2665
+ def run(extra):
2666
+ sys.argv = ["mirador-render.py", "--engine", str(eng), "--ledger", str(tmp / "L.json"),
2667
+ "--template", tpl, "--out", str(tmp / "m.html"), "--sidecar", str(tmp / "none.jsonl")] + extra
2668
+ return mr.main()
2669
+ b = opens["n"]; run(["--refresh", "30"]); live = opens["n"] - b # live view -> 0 (page self-reloads)
2670
+ b = opens["n"]; run([]); oneshot = opens["n"] - b # one-shot -> 1 (open once)
2671
+ b = opens["n"]; run(["--no-open"]); noopen = opens["n"] - b # explicit suppress -> 0
2672
+ shutil.rmtree(tmp, ignore_errors=True)
2673
+ sys.exit(0 if (live == 0 and oneshot == 1 and noopen == 0) else 1)
2674
+ PY
2675
+ if [ $? -eq 0 ]; then PASS=$((PASS+1)); echo " ok live=0 opens, one-shot=1, --no-open=0 (no browser-tab spam under watch)"; \
2676
+ else FAIL=$((FAIL+1)); echo " FAIL auto-open policy wrong -- a live/watch render would spam browser tabs"; fi
2677
+
2678
+ echo "== T80 (1.42.0): mirador status story (como viene/que lo traba/que sigue) present + fed by the ledger =="
2679
+ printf '{ "defaults": { "acceptance_file": "ACCEPTANCE.md" }, "repos": [ {"name":"solo","path":"repo-c","type":"python"} ], "integration": {"enabled": false} }\n' > status-cfg.json
2680
+ run init --config status-cfg.json --out L-status.json >/dev/null 2>&1
2681
+ run log-gate --repo solo --iteration 1 --kind simplicity --verdict fail --count 3 --ledger L-status.json >/dev/null 2>&1
2682
+ "$PY" "$KIT/.claude/skills/uscha-mirador/mirador-render.py" --engine "$QL" --ledger L-status.json \
2683
+ --template "$KIT/.claude/skills/uscha-mirador/mirador.template.html" --out status-mir.html --no-open >/dev/null 2>&1
2684
+ "$PY" - status-mir.html <<'PYIN'
2685
+ import json, re, sys
2686
+ html = open(sys.argv[1], encoding="utf-8").read()
2687
+ scaffold = all(x in html for x in ('id="status"', 'id="s-how"', 'id="s-block"', 'id="s-next"',
2688
+ 'function renderStatus', 'renderStatus();', 'id="card-heat"'))
2689
+ data = json.loads(re.search(r"const DATA = (\{.*?\});\n/\*MIRADOR_DATA_END", html, re.S).group(1))
2690
+ subs = data.get("subscores") or []
2691
+ fed = any("FAIL" in str(s.get("bd") or "").upper() for s in subs) # a measured blocker feeds "que lo traba"
2692
+ sys.exit(0 if (scaffold and fed) else 1)
2693
+ PYIN
2694
+ if [ $? -eq 0 ]; then PASS=$((PASS+1)); echo " ok status scaffold present and 'que lo traba' fed by a measured sub-score"; \
2695
+ else FAIL=$((FAIL+1)); echo " FAIL status story missing or not fed by the ledger"; fi
2696
+
2697
+ echo "== T81 (1.43.0): 'uscha mirador' verb renders the dashboard from the ledger (no python/paths for the user) =="
2698
+ "$PY" "$KIT/install-uscha.py" mirador --ledger L-status.json --out uscha-mir.html --no-open >/dev/null 2>&1
2699
+ if [ $? -eq 0 ] && [ -f uscha-mir.html ] && grep -q 'id="status"' uscha-mir.html; then
2700
+ PASS=$((PASS+1)); echo " ok uscha mirador renders mirador.html with the status story"; \
2701
+ else FAIL=$((FAIL+1)); echo " FAIL uscha mirador did not render the dashboard"; fi
2702
+ "$PY" "$KIT/install-uscha.py" mirador --ledger NOPE-missing.json --no-open >/dev/null 2>&1
2703
+ if [ $? -ne 0 ]; then PASS=$((PASS+1)); echo " ok uscha mirador fails clearly when the ledger is missing"; \
2704
+ else FAIL=$((FAIL+1)); echo " FAIL uscha mirador did not fail on a missing ledger"; fi
2705
+
1736
2706
  echo ""
1737
- echo "RESULTADO: $PASS ok · $FAIL fail"
2707
+ echo "RESULTADO BASE: $PASS ok · $FAIL fail"
1738
2708
  cd / && rm -rf "$SB"
1739
2709
  [ "$FAIL" -eq 0 ]
2710
+ SMOKE_STATUS=$?
2711
+ # P0-B-START: targeted fail-closed static-analysis evidence regression.
2712
+ if [ "${USCHA_P0_B_SKIP:-0}" != "1" ]; then
2713
+ P0_B_ROOT="${P0_ROOT:-${ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}}"
2714
+ P0_B_QL="$P0_B_ROOT/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py"
2715
+ P0_B_PY="${PYTHON:-${PY:-python}}"
2716
+ "$P0_B_PY" - "$P0_B_QL" <<'PY'
2717
+ import json
2718
+ import pathlib
2719
+ import subprocess
2720
+ import sys
2721
+ import tempfile
2722
+
2723
+ engine = pathlib.Path(sys.argv[1])
2724
+
2725
+
2726
+ def run(*args):
2727
+ return subprocess.run(
2728
+ [sys.executable, str(engine), *map(str, args)],
2729
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8")
2730
+
2731
+
2732
+ cases = [
2733
+ ("checkstyle", "maven", "--checkstyle", "checkstyle.xml",
2734
+ '<checkstyle><file name="src/A.java"><error line="1" severity="error" source="Rule"/></file></checkstyle>',
2735
+ '<checkstyle/>', '<pmd/>', '<checkstyle>',
2736
+ '<checkstyle><file name="src/A"><error line="oops" severity="error" source="Rule"/></file></checkstyle>'),
2737
+ ("golangci", "go", "--golangci", "golangci.xml",
2738
+ '<checkstyle><file name="pkg/a.go"><error line="1" severity="error" source="Rule"/></file></checkstyle>',
2739
+ '<checkstyle/>', '<pmd/>', '<checkstyle>',
2740
+ '<checkstyle><file name="src/A"><error line="oops" severity="error" source="Rule"/></file></checkstyle>'),
2741
+ ("detekt", "gradle", "--detekt", "detekt.xml",
2742
+ '<checkstyle><file name="src/A.kt"><error line="1" severity="error" source="Rule"/></file></checkstyle>',
2743
+ '<checkstyle/>', '<pmd/>', '<checkstyle>',
2744
+ '<checkstyle><file name="src/A"><error line="oops" severity="error" source="Rule"/></file></checkstyle>'),
2745
+ ("swiftlint", "swift", "--swiftlint", "swiftlint.xml",
2746
+ '<checkstyle><file name="Sources/A.swift"><error line="1" severity="error" source="Rule"/></file></checkstyle>',
2747
+ '<checkstyle/>', '<pmd/>', '<checkstyle>',
2748
+ '<checkstyle><file name="src/A"><error line="oops" severity="error" source="Rule"/></file></checkstyle>'),
2749
+ ("pmd", "maven", "--pmd", "pmd.xml",
2750
+ '<pmd><file name="src/A.java"><violation beginline="1" priority="1" rule="Rule"/></file></pmd>',
2751
+ '<pmd/>', '<checkstyle/>', '<pmd>',
2752
+ '<pmd><file name="src/A.java"><violation beginline="1" priority="oops" rule="Rule"/></file></pmd>'),
2753
+ ("spotbugs", "maven", "--spotbugs", "spotbugs.xml",
2754
+ '<BugCollection><BugInstance type="BUG" priority="1" category="CORRECTNESS"><SourceLine sourcepath="A.java" start="1"/></BugInstance></BugCollection>',
2755
+ '<BugCollection/>', '<checkstyle/>', '<BugCollection>',
2756
+ '<BugCollection><BugInstance type="BUG" priority="oops" category="CORRECTNESS"/></BugCollection>'),
2757
+ ("eslint", "node", "--eslint", "eslint.json",
2758
+ '[{"filePath":"src/a.js","messages":[{"ruleId":"rule","severity":2,"line":1}]}]',
2759
+ '[]', '{}', '{', '[{"filePath":"src/a.js","messages":{}}]'),
2760
+ ("sarif", "dotnet", "--sarif", "analysis.sarif",
2761
+ '{"version":"2.1.0","runs":[{"results":[{"ruleId":"CA1","level":"error"}]}]}',
2762
+ '{"version":"2.1.0","runs":[]}', '[]', '{',
2763
+ '{"version":"2.1.0","runs":[{"results":{}}]}'),
2764
+ ("clippy", "rust", "--clippy", "clippy.json",
2765
+ '{"reason":"compiler-message","message":{"message":"needless return","level":"warning","code":{"code":"clippy::needless_return"},"spans":[{"file_name":"src/lib.rs","line_start":1,"is_primary":true}]}}',
2766
+ '', '[]', '{',
2767
+ '{"reason":"compiler-message","message":{"message":"bad spans","level":"warning","code":null,"spans":"not-an-array"}}'),
2768
+
2769
+ ]
2770
+
2771
+ with tempfile.TemporaryDirectory(prefix="uscha-p0-b-") as tmp:
2772
+ root = pathlib.Path(tmp)
2773
+ for family, repo_type, flag, report_name, finding, empty, wrong, malformed, nested in cases:
2774
+ repo = root / family
2775
+ repo.mkdir()
2776
+ report = repo / report_name
2777
+ config = root / f"{family}.config.json"
2778
+ ledger = root / f"{family}.ledger.json"
2779
+ config.write_text(json.dumps({
2780
+ "defaults": {"severity_gate": ["BLOCKER", "CRITICAL", "HIGH"]},
2781
+ "repos": [{"name": family, "path": str(repo), "type": repo_type}],
2782
+ "integration": {"enabled": False},
2783
+ }), encoding="utf-8")
2784
+ result = run("init", "--config", config, "--out", ledger)
2785
+ assert result.returncode == 0, (family, "init", result.stderr)
2786
+ report.write_text(finding, encoding="utf-8")
2787
+ result = run("ingest-gate", "--ledger", ledger, "--repo", family,
2788
+ "--iteration", "1", flag, report)
2789
+ assert result.returncode == 0, (family, "seed red", result.stdout, result.stderr)
2790
+ before = ledger.read_bytes()
2791
+ invalid_cases = [("malformed", malformed), ("wrong shape", wrong),
2792
+ ("invalid fields", nested)]
2793
+ if family == "clippy":
2794
+ invalid_cases.extend([
2795
+ ("missing diagnostic code", '{"reason":"compiler-message","message":{"message":"bad code","level":"warning","spans":[]}}'),
2796
+ ("invalid primary span", '{"reason":"compiler-message","message":{"message":"bad location","level":"warning","code":null,"spans":[{"file_name":"src/lib.rs","line_start":false,"is_primary":true}]}}'),
2797
+ ])
2798
+ for label, invalid in invalid_cases:
2799
+ report.write_text(invalid, encoding="utf-8")
2800
+ result = run("ingest-gate", "--ledger", ledger, "--repo", family,
2801
+ "--iteration", "2", flag, report)
2802
+ assert result.returncode == 2, (family, label, result.returncode,
2803
+ result.stdout, result.stderr)
2804
+ assert "invalid" in result.stderr.lower(), (family, label, result.stderr)
2805
+ assert ledger.read_bytes() == before, (family, label, "ledger mutated")
2806
+ valid_clean = [("valid empty", empty)]
2807
+ if family == "clippy":
2808
+ valid_clean.extend([
2809
+ ("valid no-span summary", '{"reason":"compiler-message","message":{"message":"1 warning emitted","level":"warning","code":null,"spans":[]}}'),
2810
+ ("valid Cargo summary", '{"reason":"build-finished","success":true}'),
2811
+ ])
2812
+ for label, clean in valid_clean:
2813
+ report.write_text(clean, encoding="utf-8")
2814
+ result = run("ingest-gate", "--ledger", ledger, "--repo", family,
2815
+ "--iteration", "2", flag, report)
2816
+ assert result.returncode == 0, (family, label, result.stdout, result.stderr)
2817
+ print(f"P0-B ok: {family} malformed/wrong-shape/invalid-field fail closed; valid empty/noise accepted")
2818
+ PY
2819
+ P0_B_STATUS=$?
2820
+ if [ "$P0_B_STATUS" -ne 0 ]; then SMOKE_STATUS="$P0_B_STATUS"; fi
2821
+ fi
2822
+ # P0-B-END
2823
+ # P0-C-START: targeted stale-JUnit pr-ready regression.
2824
+ if [ "${USCHA_P0_C_SKIP:-0}" != "1" ]; then
2825
+ P0_C_ROOT="${P0_ROOT:-${ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}}"
2826
+ P0_C_QL="$P0_C_ROOT/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py"
2827
+ P0_C_PY="${PYTHON:-${PY:-python}}"
2828
+ "$P0_C_PY" - "$P0_C_QL" <<'PY'
2829
+ import json
2830
+ import os
2831
+ import pathlib
2832
+ import subprocess
2833
+ import sys
2834
+ import tempfile
2835
+
2836
+ engine = pathlib.Path(sys.argv[1])
2837
+
2838
+
2839
+ def run(*args):
2840
+ return subprocess.run(
2841
+ [sys.executable, str(engine), *map(str, args)],
2842
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8")
2843
+
2844
+
2845
+ def check_layout(root, name, repo_type, source_rel, report_rel):
2846
+ repo = root / name
2847
+ source = repo / source_rel
2848
+ report = repo / report_rel
2849
+ source.parent.mkdir(parents=True, exist_ok=True)
2850
+ report.parent.mkdir(parents=True, exist_ok=True)
2851
+ source.write_text("source\n", encoding="utf-8")
2852
+ report.write_text(
2853
+ '<testsuite tests="1" failures="0" errors="0" skipped="0"/>\n',
2854
+ encoding="utf-8")
2855
+ base_ns = 1_700_000_000_000_000_000
2856
+ os.utime(source, ns=(base_ns, base_ns))
2857
+ os.utime(report, ns=(base_ns + 5_000_000_000,
2858
+ base_ns + 5_000_000_000))
2859
+
2860
+ config = root / f"{name}.config.json"
2861
+ ledger = root / f"{name}.ledger.json"
2862
+ config.write_text(json.dumps({
2863
+ "defaults": {"qa_tools_order": ["code-review"]},
2864
+ "repos": [{"name": name, "path": str(repo), "type": repo_type}],
2865
+ "integration": {"enabled": False},
2866
+ }), encoding="utf-8")
2867
+
2868
+ result = run("init", "--config", config, "--out", ledger)
2869
+ assert result.returncode == 0, (name, "init", result.stdout, result.stderr)
2870
+ result = run("snapshot", "--ledger", ledger, "--repo", name)
2871
+ assert result.returncode == 0, (name, "fresh snapshot", result.stdout, result.stderr)
2872
+ result = run("log-step", "--ledger", ledger, "--repo", name,
2873
+ "--tool", "code-review", "--iteration", "1",
2874
+ "--gated-reported", "0", "--files-changed", "0")
2875
+ assert result.returncode == 0, (name, "log-step", result.stdout, result.stderr)
2876
+ result = run("phase", "--ledger", ledger, "--repo", name,
2877
+ "--require", "pr-ready")
2878
+ assert result.returncode == 0, (name, "fresh pr-ready", result.stdout, result.stderr)
2879
+
2880
+ os.utime(source, ns=(base_ns + 10_000_000_000,
2881
+ base_ns + 10_000_000_000))
2882
+ result = run("snapshot", "--ledger", ledger, "--repo", name)
2883
+ assert result.returncode == 0, (name, "stale snapshot", result.stdout, result.stderr)
2884
+ assert "stale" in result.stdout.lower(), (name, "stale snapshot diagnostic", result.stdout)
2885
+ latest = json.loads(ledger.read_text(encoding="utf-8"))["repos"][name]["snapshots"][-1]["tests"]
2886
+ assert latest["report_found"] is True and latest["freshness"]["status"] == "stale", latest
2887
+ assert latest["reports"] and latest["reports"][0]["path"], latest
2888
+ result = run("phase", "--ledger", ledger, "--repo", name,
2889
+ "--require", "pr-ready")
2890
+ assert result.returncode == 1, (name, "stale pr-ready veto", result.stdout, result.stderr)
2891
+ assert "stale" in (result.stdout + result.stderr).lower(), (name, "stale phase diagnostic", result.stdout, result.stderr)
2892
+
2893
+ os.utime(report, ns=(base_ns + 15_000_000_000,
2894
+ base_ns + 15_000_000_000))
2895
+ result = run("snapshot", "--ledger", ledger, "--repo", name)
2896
+ assert result.returncode == 0, (name, "recovered snapshot", result.stdout, result.stderr)
2897
+ result = run("phase", "--ledger", ledger, "--repo", name,
2898
+ "--require", "pr-ready")
2899
+ assert result.returncode == 0, (name, "recovered pr-ready", result.stdout, result.stderr)
2900
+ print(f"P0-C ok: {repo_type} fresh -> stale veto -> regenerated recovery")
2901
+
2902
+
2903
+ def check_report_only_compatibility(root):
2904
+ name = "report-only"
2905
+ repo = root / name
2906
+ report = repo / "reports" / "junit.xml"
2907
+ ignored = repo / "vendor" / "ignored.py"
2908
+ report.parent.mkdir(parents=True)
2909
+ ignored.parent.mkdir(parents=True)
2910
+ report.write_text(
2911
+ '<testsuite tests="1" failures="0" errors="0" skipped="0"/>\n',
2912
+ encoding="utf-8")
2913
+ ignored.write_text("generated dependency\n", encoding="utf-8")
2914
+ config = root / f"{name}.config.json"
2915
+ ledger = root / f"{name}.ledger.json"
2916
+ config.write_text(json.dumps({
2917
+ "defaults": {"qa_tools_order": ["code-review"]},
2918
+ "repos": [{"name": name, "path": str(repo), "type": "python"}],
2919
+ "integration": {"enabled": False},
2920
+ }), encoding="utf-8")
2921
+ for args in (
2922
+ ("init", "--config", config, "--out", ledger),
2923
+ ("snapshot", "--ledger", ledger, "--repo", name),
2924
+ ("log-step", "--ledger", ledger, "--repo", name, "--tool", "code-review",
2925
+ "--iteration", "1", "--gated-reported", "0", "--files-changed", "0"),
2926
+ ):
2927
+ result = run(*args)
2928
+ assert result.returncode == 0, (name, args[0], result.stdout, result.stderr)
2929
+ latest = json.loads(ledger.read_text(encoding="utf-8"))["repos"][name]["snapshots"][-1]["tests"]
2930
+ assert latest["freshness"]["status"] == "unknown-no-sources", latest
2931
+ result = run("phase", "--ledger", ledger, "--repo", name,
2932
+ "--require", "pr-ready")
2933
+ assert result.returncode == 0, (name, "compatibility", result.stdout, result.stderr)
2934
+ print("P0-C ok: report-only repo stays usable with explicit unknown-no-sources provenance")
2935
+
2936
+
2937
+ with tempfile.TemporaryDirectory(prefix="uscha-p0-c-") as tmp:
2938
+ root = pathlib.Path(tmp)
2939
+ check_layout(root, "python-layout", "python", "src/app.py", "reports/junit.xml")
2940
+ check_layout(root, "maven-layout", "maven", "src/test/java/AppTest.java",
2941
+ "target/surefire-reports/TEST-AppTest.xml")
2942
+ check_report_only_compatibility(root)
2943
+ PY
2944
+ P0_C_STATUS=$?
2945
+ if [ "$P0_C_STATUS" -ne 0 ]; then SMOKE_STATUS="$P0_C_STATUS"; fi
2946
+ fi
2947
+ # P0-C-END
2948
+ # P0-A-START: targeted Mirador script/DOM injection regression.
2949
+ if [ "${USCHA_P0_A_SKIP:-0}" != "1" ]; then
2950
+ set -eu
2951
+ P0_PREVIOUS_STATUS="${SMOKE_STATUS:-0}"
2952
+ P0_ROOT="${P0_ROOT:-${ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}}"
2953
+ P0_KIT="$P0_ROOT/uscha-kit"
2954
+ P0_TMP="$(mktemp -d)"
2955
+ trap 'rm -rf "$P0_TMP"' EXIT
2956
+ P0_PY="${PYTHON:-${PY:-python}}"
2957
+ cat > "$P0_TMP/dashboard-engine.py" <<'PY'
2958
+ #!/usr/bin/env python3
2959
+ import json
2960
+ import sys
2961
+ sys.stdout.reconfigure(encoding="utf-8")
2962
+ attack = '</script><script>globalThis.MIRADOR_PWNED=1</script><b>&"\'\u2028\u2029'
2963
+ data = {
2964
+ "project": attack, "generated": attack,
2965
+ "readiness": {"score": 7, "band": "NOT READY", "title": attack, "sub": attack},
2966
+ "phases": [{"key": attack, "phase": attack, "label": attack, "status": "todo", "risk": 0, "count": attack,
2967
+ "execution": {"model": attack, "tier": attack, "effort": attack, "method": attack, "uncorrelated": True}}],
2968
+ "specs": [{"id": attack, "t": attack, "status": "todo", "acc": attack}],
2969
+ "adrs": [{"id": attack, "t": attack, "status": "todo", "adr_status": attack}],
2970
+ "inv": [{"name": attack, "status": "todo"}], "capas": [{"name": attack, "v": attack, "status": "warn"}],
2971
+ "loops": [{"mod": attack, "state": attack, "max": 1, "iters": 1}],
2972
+ "subscores": [{"k": attack, "val": None, "bd": attack}],
2973
+ "execution_policy": {"source": attack, "phases": {}},
2974
+ "evidence": {attack: {"ey": attack, "title": attack, "desc": attack, "pre": attack}},
2975
+ "snapshots": [{"date": attack, "readiness": 7, "reached": 0}],
2976
+ }
2977
+ print(json.dumps(data, ensure_ascii=False))
2978
+ PY
2979
+ cat > "$P0_TMP/telemetry.jsonl" <<'JSON'
2980
+ {"tokens_in":1,"tokens_out":2,"ms":3,"effort":"</script><script>telemetry()</script><i>&\"\u2028\u2029","by_model":[{"model":"<img src=x onerror=telemetry()> & \" model","tokens_in":1,"tokens_out":2}]}
2981
+ JSON
2982
+ "$P0_PY" "$P0_KIT/.claude/skills/uscha-mirador/mirador-render.py" \
2983
+ --engine "$P0_TMP/dashboard-engine.py" --ledger "$P0_TMP/ledger.json" \
2984
+ --template "$P0_KIT/.claude/skills/uscha-mirador/mirador.template.html" \
2985
+ --out "$P0_TMP/mirador.html" --sidecar "$P0_TMP/telemetry.jsonl" --no-open >/dev/null
2986
+ "$P0_PY" - "$P0_TMP/mirador.html" "$P0_KIT/.claude/skills/uscha-mirador/mirador.template.html" <<'PY'
2987
+ import json, pathlib, re, sys
2988
+ attack = '</script><script>globalThis.MIRADOR_PWNED=1</script><b>&"\'\u2028\u2029'
2989
+ html = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
2990
+ template = pathlib.Path(sys.argv[2]).read_text(encoding="utf-8")
2991
+ match = re.search(r"const DATA = (\{.*?\});\n/\*MIRADOR_DATA_END", html, re.S)
2992
+ assert match, "rendered DATA payload missing"
2993
+ payload = match.group(1)
2994
+ decoded = json.loads(payload)
2995
+ assert decoded["project"] == attack and decoded["readiness"]["title"] == attack
2996
+ assert decoded["telemetry"]["effort"].startswith("</script><script>telemetry()")
2997
+ for raw in ("<", ">", "&", "\u2028", "\u2029"):
2998
+ assert raw not in payload, f"raw script-context delimiter leaked: {raw!r}"
2999
+ for escaped in (r"\u003c", r"\u003e", r"\u0026", r"\u2028", r"\u2029"):
3000
+ assert escaped in payload, f"missing HTML-safe JSON escape: {escaped}"
3001
+ assert "globalThis.MIRADOR_PWNED" not in html.replace(payload, "")
3002
+ assert ".innerHTML" not in template, "data-bearing innerHTML sink remains"
3003
+ assert "insertAdjacentHTML" not in template and "document.write" not in template
3004
+ print("P0-A ok: script-context JSON escaped and dynamic DOM rendering has no HTML sinks")
3005
+ PY
3006
+ P0_A_STATUS=0
3007
+ fi
3008
+ # P0-A-END
3009
+
3010
+ if [ "${USCHA_P0_B_SKIP:-0}" != "1" ]; then
3011
+ if [ "${P0_B_STATUS:-1}" -eq 0 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); fi
3012
+ fi
3013
+ if [ "${USCHA_P0_C_SKIP:-0}" != "1" ]; then
3014
+ if [ "${P0_C_STATUS:-1}" -eq 0 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); fi
3015
+ fi
3016
+ if [ "${USCHA_P0_A_SKIP:-0}" != "1" ]; then
3017
+ if [ "${P0_A_STATUS:-1}" -eq 0 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); fi
3018
+ fi
3019
+
3020
+ echo ""
3021
+ printf 'RESULTADO: %s ok · %s fail\n' "$PASS" "$FAIL"
3022
+ exit "${SMOKE_STATUS:-0}"