@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.
- package/README.md +6 -6
- package/bin/uscha.js +19 -7
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-adr-refine/SKILL.md +2 -2
- package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +3 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +729 -155
- package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +22 -7
- package/uscha-kit/.claude/skills/uscha-mirador/mirador-render.py +44 -5
- package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.ps1 +1 -1
- package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.sh +1 -1
- package/uscha-kit/.claude/skills/uscha-mirador/mirador.template.html +666 -586
- package/uscha-kit/.claude-plugin/plugin.json +1 -1
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/CHANGELOG-1.41.0.md +18 -0
- package/uscha-kit/CHANGELOG-1.41.1.md +53 -0
- package/uscha-kit/CHANGELOG-1.41.2.md +34 -0
- package/uscha-kit/CHANGELOG-1.41.3.md +30 -0
- package/uscha-kit/CHANGELOG-1.42.0.md +41 -0
- package/uscha-kit/CHANGELOG-1.43.0.md +37 -0
- package/uscha-kit/INSTALL.md +120 -101
- package/uscha-kit/README.md +24 -13
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/WORKBENCH.md +19 -5
- package/uscha-kit/hooks/block-approved-writes.py +25 -0
- package/uscha-kit/install-uscha.py +534 -267
- package/uscha-kit/skills/uscha-adr-refine/SKILL.md +2 -2
- package/uscha-kit/skills/uscha-devloop/SKILL.md +3 -1
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +729 -155
- package/uscha-kit/skills/uscha-mirador/SKILL.md +22 -7
- package/uscha-kit/skills/uscha-mirador/mirador-render.py +44 -5
- package/uscha-kit/skills/uscha-mirador/mirador-watch.ps1 +1 -1
- package/uscha-kit/skills/uscha-mirador/mirador-watch.sh +1 -1
- package/uscha-kit/skills/uscha-mirador/mirador.template.html +666 -586
- package/uscha-kit/templates/CONSTITUTION.md +4 -4
- package/uscha-kit/templates/docs/adr/README.md +19 -19
- package/uscha-kit/tests/ledger-integrity-regressions.py +136 -0
- package/uscha-kit/tests/smoke-engine.sh +1312 -29
- package/uscha-kit/uscha.config.json +1 -1
- 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":[]
|
|
309
|
-
{"reason":"compiler-message","message":{"level":"error","code":null,"spans":[]
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
-
##
|
|
1716
|
+
## HIPÓTESIS
|
|
1526
1717
|
El checkout nuevo reduce abandonos sin subir errores.
|
|
1527
|
-
##
|
|
1718
|
+
## SEÑAL DE FEEDBACK
|
|
1528
1719
|
Conversion rate y errores de pago en produccion.
|
|
1529
1720
|
## Review By: 2099-01-01
|
|
1530
|
-
##
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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}"
|