@andresmassello/uscha 1.61.0 → 1.63.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 +2 -2
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +189 -2
- package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +1 -1
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +32 -2
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/reports/junit/.cleanroom-cases.json +1 -0
- package/uscha-kit/reports/junit/.specdrift-cases.json +1 -1
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +189 -2
- package/uscha-kit/skills/uscha-mirador/SKILL.md +1 -1
- package/uscha-kit/uscha.config.json +5 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Requires **Python 3.8+** on the machine (the engine is Python stdlib — no pip
|
|
|
40
40
|
runtime dependencies). The npm package is a thin router; the canonical installer is
|
|
41
41
|
`uscha-kit/install-uscha.py`.
|
|
42
42
|
|
|
43
|
-
**Kit v1.
|
|
43
|
+
**Kit v1.63.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
|
|
44
44
|
[changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG.md)
|
|
45
45
|
(the per-release changelogs live in the repo, not in the npm tarball)
|
|
46
46
|
|
|
@@ -76,7 +76,7 @@ and see which file, which test, and when.
|
|
|
76
76
|
| `/uscha-mirador` | Bird's-eye HTML dashboard: readiness, trail, acceptance, loops |
|
|
77
77
|
| `/uscha-status` | One-line progress readout, in chat |
|
|
78
78
|
|
|
79
|
-
**A measurement engine** (`qa_ledger.py`,
|
|
79
|
+
**A measurement engine** (`qa_ledger.py`, 33 subcommands, Python stdlib) that ingests
|
|
80
80
|
evidence from **11 language stacks** — maven, gradle, ant, python, node, go, rust, dotnet,
|
|
81
81
|
cpp, swift, flutter — and computes a readiness score with hard caps and visible provenance.
|
|
82
82
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.63.0",
|
|
4
4
|
"description": "Spec-driven development for LLM coding agents: 9 skills + a stdlib evidence engine. Facts block, guesses advise; the human approves.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Andres Massello",
|
|
@@ -2629,6 +2629,35 @@ def _derive_phase(ledger, name, node, k, qa_order):
|
|
|
2629
2629
|
reasons.append("la evidencia medida no contiene tests ejecutados")
|
|
2630
2630
|
_go, sev = _gate_open_and_sev(node)
|
|
2631
2631
|
blk = sev.get("BLOCKER", 0) + sev.get("CRITICAL", 0)
|
|
2632
|
+
# clean-room gate (ADR-008). OPT-IN: no clean_room block, or mode "off", and this does not
|
|
2633
|
+
# exist -- behavior identical to earlier releases. Declared "final": pr-ready additionally
|
|
2634
|
+
# requires clean-room evidence that is GREEN and pinned to the CURRENT HEAD. A new commit
|
|
2635
|
+
# makes the previous run stale for the gate, the same staleness posture the rest of the
|
|
2636
|
+
# engine takes: evidence certifies the thing it was measured against, nothing later.
|
|
2637
|
+
_cr = _cr_cfg(ledger)
|
|
2638
|
+
if _cr and _cr.get("mode") == "final":
|
|
2639
|
+
_head = None
|
|
2640
|
+
_hr = None
|
|
2641
|
+
_crcfg = (_repo_cfg(ledger, name) if name != "integration"
|
|
2642
|
+
else {"path": "."}) # synthetic scope: never in config["repos"]
|
|
2643
|
+
try:
|
|
2644
|
+
_hr = subprocess.run(["git", "rev-parse", "HEAD"],
|
|
2645
|
+
cwd=_crcfg.get("path", "."),
|
|
2646
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
2647
|
+
errors="replace")
|
|
2648
|
+
except OSError:
|
|
2649
|
+
_hr = None
|
|
2650
|
+
if _hr is not None and _hr.returncode == 0:
|
|
2651
|
+
_head = _hr.stdout.strip()
|
|
2652
|
+
_run = _cr_latest(ledger, name, _head) if _head else None
|
|
2653
|
+
if not _head:
|
|
2654
|
+
reasons.append("clean-room declarado pero no se pudo resolver HEAD "
|
|
2655
|
+
"(no se mide, no se aprueba)")
|
|
2656
|
+
conv = False
|
|
2657
|
+
elif not _run or not _run.get("ok"):
|
|
2658
|
+
reasons.append("falta clean-room verde para %s (evidencia del arbol no "
|
|
2659
|
+
"certifica el commit)" % _head[:8])
|
|
2660
|
+
conv = False
|
|
2632
2661
|
if conv and tests_measured_green and not tests_red and blk == 0:
|
|
2633
2662
|
evidence = ["ciclo de agente limpio", "tests verdes (medidos)",
|
|
2634
2663
|
"0 BLOCKER/CRITICAL abiertos"]
|
|
@@ -3026,7 +3055,7 @@ def _sd_governs(path):
|
|
|
3026
3055
|
return None
|
|
3027
3056
|
if not lines or lines[0].strip() != "---":
|
|
3028
3057
|
return None
|
|
3029
|
-
globs, in_governs = None, False
|
|
3058
|
+
globs, in_governs, explicit_empty = None, False, False
|
|
3030
3059
|
# scan runs to the CLOSING fence, not an arbitrary window -- a governs: key late in a
|
|
3031
3060
|
# long frontmatter block must not silently read as UNMAPPED (fresh-review finding).
|
|
3032
3061
|
for ln in lines[1:]:
|
|
@@ -3038,6 +3067,8 @@ def _sd_governs(path):
|
|
|
3038
3067
|
if rest.startswith("[") and rest.endswith("]"):
|
|
3039
3068
|
globs = [x.strip().strip("\x27\x22")
|
|
3040
3069
|
for x in rest[1:-1].split(",") if x.strip()]
|
|
3070
|
+
# only an INLINE [] is a declaration of "nothing to govern"
|
|
3071
|
+
explicit_empty = not globs
|
|
3041
3072
|
in_governs = False
|
|
3042
3073
|
elif rest:
|
|
3043
3074
|
# bare scalar (`governs: src/**`) -- a plausible authoring shorthand;
|
|
@@ -3051,6 +3082,11 @@ def _sd_governs(path):
|
|
|
3051
3082
|
globs.append(s[2:].strip().strip("\x27\x22"))
|
|
3052
3083
|
elif s and not ln.startswith((" ", "\t")):
|
|
3053
3084
|
in_governs = False
|
|
3085
|
+
if globs == [] and not explicit_empty:
|
|
3086
|
+
# a `governs:` key with nothing usable under it (a placeholder, a comment, a typo) is
|
|
3087
|
+
# an UNFINISHED declaration, not a statement that this spec governs nothing. Report it
|
|
3088
|
+
# as UNMAPPED, which is what it is (fresh-review finding).
|
|
3089
|
+
return None
|
|
3054
3090
|
return globs
|
|
3055
3091
|
|
|
3056
3092
|
|
|
@@ -3111,6 +3147,16 @@ def cmd_spec_drift(args):
|
|
|
3111
3147
|
row.update({"verdict": "UNMAPPED", "reason": "no governs: frontmatter"})
|
|
3112
3148
|
results.append(row)
|
|
3113
3149
|
continue
|
|
3150
|
+
if not governs:
|
|
3151
|
+
# An EXPLICIT empty list is a declaration, not an omission: this decision governs
|
|
3152
|
+
# no code and never will. Negative ADRs ("we are NOT doing X, and why") are a
|
|
3153
|
+
# documented practice in this kit, and reporting them UNMAPPED forever turns a
|
|
3154
|
+
# correct state into permanent noise -- which is how an advisory gets ignored.
|
|
3155
|
+
# Found by running spec-drift on this repo's own ADR-004.
|
|
3156
|
+
row.update({"verdict": "NO-CODE",
|
|
3157
|
+
"reason": "declares governs: [] -- a decision that governs no code"})
|
|
3158
|
+
results.append(row)
|
|
3159
|
+
continue
|
|
3114
3160
|
matched = []
|
|
3115
3161
|
pats = [_fp_glob_re(g) for g in governs]
|
|
3116
3162
|
for f in tracked:
|
|
@@ -3171,7 +3217,8 @@ def cmd_spec_drift(args):
|
|
|
3171
3217
|
print("SPEC-DRIFT %s (advisory, lag > %dd):" % (args.repo, lag_days))
|
|
3172
3218
|
if not results:
|
|
3173
3219
|
print(" no spec documents found (SPEC.md / docs/adr/*.md)")
|
|
3174
|
-
mark = {"SPEC_STALE": "!!", "CLEAN": "ok", "UNMAPPED": "--", "UNTRACKED": "--"
|
|
3220
|
+
mark = {"SPEC_STALE": "!!", "CLEAN": "ok", "UNMAPPED": "--", "UNTRACKED": "--",
|
|
3221
|
+
"NO-CODE": "ok"}
|
|
3175
3222
|
for r_ in results:
|
|
3176
3223
|
line = " %s %s: %s" % (mark.get(r_["verdict"], "??"), r_["file"],
|
|
3177
3224
|
r_["verdict"])
|
|
@@ -3337,6 +3384,132 @@ def cmd_golden_coverage(args):
|
|
|
3337
3384
|
|
|
3338
3385
|
|
|
3339
3386
|
|
|
3387
|
+
# --------------------------------------------------------------------------- #
|
|
3388
|
+
# cleanroom (ADR-008: verify the COMMIT, not the tree -- opt-in, human-supplied command)
|
|
3389
|
+
# --------------------------------------------------------------------------- #
|
|
3390
|
+
|
|
3391
|
+
CLEAN_ROOM_KEY = "clean_room"
|
|
3392
|
+
|
|
3393
|
+
|
|
3394
|
+
def _cr_cfg(ledger):
|
|
3395
|
+
cfg = ledger["config"].get("defaults", {}).get(CLEAN_ROOM_KEY)
|
|
3396
|
+
return cfg if isinstance(cfg, dict) else None
|
|
3397
|
+
|
|
3398
|
+
|
|
3399
|
+
def _cr_latest(ledger, repo, ref=None):
|
|
3400
|
+
"""Latest clean-room record for a repo, optionally pinned to one ref."""
|
|
3401
|
+
runs = [e for e in ledger.get(CLEAN_ROOM_KEY, [])
|
|
3402
|
+
if e.get("repo") == repo and (ref is None or e.get("ref") == ref)]
|
|
3403
|
+
return runs[-1] if runs else None
|
|
3404
|
+
|
|
3405
|
+
|
|
3406
|
+
def cmd_cleanroom(args):
|
|
3407
|
+
"""Run a command against a CLEAN CHECKOUT of one commit, in a throwaway worktree.
|
|
3408
|
+
|
|
3409
|
+
Evidence produced in the maker's tree is true of the TREE; this produces evidence true
|
|
3410
|
+
of the COMMIT. As a side effect maker != checker becomes physical: the worktree cannot
|
|
3411
|
+
see uncommitted state.
|
|
3412
|
+
|
|
3413
|
+
The engine does NOT decide what to run. The command arrives explicitly via --run, the
|
|
3414
|
+
same contract golden-coverage uses for --harness: the engine owns what it can guarantee
|
|
3415
|
+
(isolation, the SHA binding, cleanup) and never guesses what a project's suite is.
|
|
3416
|
+
Reading test_command_* from config and executing it would make the engine an executor
|
|
3417
|
+
of config-supplied shell, which it is not (ADR-008)."""
|
|
3418
|
+
import time
|
|
3419
|
+
ledger = _load(args.ledger)
|
|
3420
|
+
_repo_node(ledger, args.repo)
|
|
3421
|
+
repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
|
|
3422
|
+
|
|
3423
|
+
def git(*a, **kw):
|
|
3424
|
+
cwd = kw.pop("cwd", repo_path)
|
|
3425
|
+
try:
|
|
3426
|
+
return subprocess.run(["git"] + list(a), cwd=cwd, capture_output=True,
|
|
3427
|
+
text=True, encoding="utf-8", errors="replace")
|
|
3428
|
+
except OSError:
|
|
3429
|
+
return None
|
|
3430
|
+
|
|
3431
|
+
rev = git("rev-parse", "--verify", (args.ref or "HEAD") + "^{commit}")
|
|
3432
|
+
if rev is None or rev.returncode != 0 or not rev.stdout.strip():
|
|
3433
|
+
print("[qa_ledger] cannot resolve ref %r in %s - nothing to verify."
|
|
3434
|
+
% (args.ref or "HEAD", repo_path), file=sys.stderr)
|
|
3435
|
+
sys.exit(2)
|
|
3436
|
+
sha = rev.stdout.strip()
|
|
3437
|
+
|
|
3438
|
+
wt = tempfile.mkdtemp(prefix="uscha-cleanroom-")
|
|
3439
|
+
target = os.path.join(wt, "tree")
|
|
3440
|
+
started = time.time()
|
|
3441
|
+
record = {"repo": args.repo, "ref": sha, "at": _now(), "ok": False,
|
|
3442
|
+
"status": None, "wall_ms": None, "worktree_sha": None}
|
|
3443
|
+
keep = bool((_cr_cfg(ledger) or {}).get("keep_worktree_on_failure"))
|
|
3444
|
+
try:
|
|
3445
|
+
add = git("worktree", "add", "--detach", target, sha)
|
|
3446
|
+
if add is None or add.returncode != 0:
|
|
3447
|
+
record["status"] = "WORKTREE_FAILED"
|
|
3448
|
+
print("[qa_ledger] git worktree add failed:\n%s"
|
|
3449
|
+
% ((add.stderr if add else "git unavailable") or "")[-800:], file=sys.stderr)
|
|
3450
|
+
else:
|
|
3451
|
+
# a worktree of a commit must be clean by construction; if it is not, something
|
|
3452
|
+
# (a smudge filter, a hook, a stale index) intervened and the isolation claim is
|
|
3453
|
+
# already false. Say so rather than measure in it.
|
|
3454
|
+
st = git("status", "--porcelain", cwd=target)
|
|
3455
|
+
if st is None or st.returncode != 0 or st.stdout.strip():
|
|
3456
|
+
record["status"] = "WORKTREE_DIRTY"
|
|
3457
|
+
else:
|
|
3458
|
+
record["worktree_sha"] = sha
|
|
3459
|
+
if args.setup:
|
|
3460
|
+
r = subprocess.run(args.setup, cwd=target, shell=True,
|
|
3461
|
+
capture_output=True, text=True,
|
|
3462
|
+
encoding="utf-8", errors="replace")
|
|
3463
|
+
if r.returncode != 0:
|
|
3464
|
+
record["status"] = "SETUP_FAILED"
|
|
3465
|
+
print((r.stderr or "")[-800:], file=sys.stderr)
|
|
3466
|
+
if record["status"] is None:
|
|
3467
|
+
r = subprocess.run(args.run, cwd=target, shell=True,
|
|
3468
|
+
capture_output=True, text=True,
|
|
3469
|
+
encoding="utf-8", errors="replace")
|
|
3470
|
+
record["exit_code"] = r.returncode
|
|
3471
|
+
record["status"] = "GREEN" if r.returncode == 0 else "RED"
|
|
3472
|
+
record["ok"] = r.returncode == 0
|
|
3473
|
+
if r.returncode != 0:
|
|
3474
|
+
print((r.stdout or "")[-1500:], file=sys.stderr)
|
|
3475
|
+
finally:
|
|
3476
|
+
record["wall_ms"] = int((time.time() - started) * 1000)
|
|
3477
|
+
# Cleanup is unconditional unless the human asked to inspect a FAILURE: a zombie
|
|
3478
|
+
# worktree is a defect, and `git worktree remove` alone leaves the admin entry behind.
|
|
3479
|
+
if record["ok"] or not keep:
|
|
3480
|
+
git("worktree", "remove", "--force", target)
|
|
3481
|
+
git("worktree", "prune")
|
|
3482
|
+
shutil.rmtree(wt, ignore_errors=True)
|
|
3483
|
+
# VERIFY the removal instead of assuming it. rmtree(ignore_errors=True) and a
|
|
3484
|
+
# discarded git return code can both fail in silence -- typically on Windows,
|
|
3485
|
+
# where a handle the caller's command left open blocks removal.
|
|
3486
|
+
if os.path.exists(target):
|
|
3487
|
+
record["cleanup_failed"] = True
|
|
3488
|
+
record["worktree_kept_at"] = target
|
|
3489
|
+
print("[qa_ledger] WARNING: the clean-room worktree could not be removed and "
|
|
3490
|
+
"is still at %s (a process may still hold a handle). Remove it with: "
|
|
3491
|
+
"git worktree remove --force %s && git worktree prune"
|
|
3492
|
+
% (target, target), file=sys.stderr)
|
|
3493
|
+
else:
|
|
3494
|
+
record["worktree_kept_at"] = target
|
|
3495
|
+
|
|
3496
|
+
ledger.setdefault(CLEAN_ROOM_KEY, []).append(record)
|
|
3497
|
+
ledger["step_counter"] += 1
|
|
3498
|
+
ledger["steps"].append({"n": ledger["step_counter"], "at": _now(),
|
|
3499
|
+
"kind": "cleanroom", "repo": args.repo})
|
|
3500
|
+
_save(args.ledger, ledger)
|
|
3501
|
+
|
|
3502
|
+
if args.json:
|
|
3503
|
+
print(json.dumps(record, indent=2, ensure_ascii=False))
|
|
3504
|
+
else:
|
|
3505
|
+
print("CLEANROOM %s @ %s: %s (%.1fs)"
|
|
3506
|
+
% (args.repo, sha[:8], record["status"], (record["wall_ms"] or 0) / 1000.0))
|
|
3507
|
+
if record.get("worktree_kept_at"):
|
|
3508
|
+
print(" worktree kept for inspection: " + record["worktree_kept_at"])
|
|
3509
|
+
sys.exit(0 if record["ok"] else 1)
|
|
3510
|
+
|
|
3511
|
+
|
|
3512
|
+
|
|
3340
3513
|
def cmd_escalate(args):
|
|
3341
3514
|
ledger = _load(args.ledger)
|
|
3342
3515
|
_repo_node(ledger, args.repo)
|
|
@@ -4383,6 +4556,9 @@ def cmd_dashboard(args):
|
|
|
4383
4556
|
_org[_rn] = _snaps[-1]["origin"]
|
|
4384
4557
|
if _org:
|
|
4385
4558
|
out["evidence_origin"] = _org
|
|
4559
|
+
if ledger.get(CLEAN_ROOM_KEY):
|
|
4560
|
+
out["clean_room"] = {r: [e for e in ledger[CLEAN_ROOM_KEY] if e.get("repo") == r][-1]
|
|
4561
|
+
for r in {e.get("repo") for e in ledger[CLEAN_ROOM_KEY]}}
|
|
4386
4562
|
if getattr(args, "json", False):
|
|
4387
4563
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
4388
4564
|
return
|
|
@@ -7087,6 +7263,17 @@ def build_parser():
|
|
|
7087
7263
|
pfp.add_argument("--json", action="store_true")
|
|
7088
7264
|
pfp.set_defaults(func=cmd_fastpath_eval)
|
|
7089
7265
|
|
|
7266
|
+
pcr = sub.add_parser("cleanroom",
|
|
7267
|
+
help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
|
|
7268
|
+
pcr.add_argument("--ledger", default="QA-LEDGER.json")
|
|
7269
|
+
pcr.add_argument("--repo", required=True)
|
|
7270
|
+
pcr.add_argument("--ref", default=None, help="commit to verify; default HEAD")
|
|
7271
|
+
pcr.add_argument("--run", required=True,
|
|
7272
|
+
help="the command to run inside the worktree; the engine never guesses it")
|
|
7273
|
+
pcr.add_argument("--setup", default=None, help="optional bootstrap before --run (e.g. npm ci)")
|
|
7274
|
+
pcr.add_argument("--json", action="store_true")
|
|
7275
|
+
pcr.set_defaults(func=cmd_cleanroom)
|
|
7276
|
+
|
|
7090
7277
|
pgc = sub.add_parser("golden-coverage",
|
|
7091
7278
|
help="record the MEASURED source files a golden's harness exercises (ADR-006)")
|
|
7092
7279
|
pgc.add_argument("--harness", required=True, help="script that drives the subject")
|
|
@@ -71,7 +71,7 @@ than inventing a step. Keep the CONTENT in the conversation's language and the l
|
|
|
71
71
|
repo straight from the ledger, or null when none was requested. The template degrades when
|
|
72
72
|
absent, like every other field.
|
|
73
73
|
- **Spec-drift (ADR-005):** `dashboard --json` carries `spec_drift` — the latest advisory
|
|
74
|
-
run (per-document verdicts: SPEC_STALE / CLEAN / UNMAPPED / UNTRACKED) — only when a run
|
|
74
|
+
run (per-document verdicts: SPEC_STALE / CLEAN / UNMAPPED / UNTRACKED / NO-CODE) — only when a run
|
|
75
75
|
exists in the ledger; a virgin ledger keeps the exact prior schema. Advisory visibility of
|
|
76
76
|
the spec-maintenance tax, never readiness input.
|
|
77
77
|
- **Modes card:** the template draws one card for both modes — fast-path verdict chips per
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "uscha",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.63.0",
|
|
5
5
|
"displayName": "Uscha",
|
|
6
|
-
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py,
|
|
6
|
+
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 33 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Andres Massello",
|
|
9
9
|
"url": "https://github.com/andresmassello"
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.
|
|
3
|
+
**Kit version:** v1.63.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
|
|
4
4
|
|
|
5
5
|
Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
|
|
6
6
|
**Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
|
|
@@ -123,7 +123,8 @@ Per document: **`SPEC_STALE`** when governed code outran the spec by more than
|
|
|
123
123
|
`defaults.spec_drift.max_lag_days` (default 30), listing the newer files; **`CLEAN`** when it
|
|
124
124
|
did not; **`UNMAPPED`** when there is no `governs:` frontmatter *or its globs match nothing*
|
|
125
125
|
— absence of a mapping is absence of measurement, not "no drift"; **`UNTRACKED`** when the
|
|
126
|
-
spec has no commit date to compare
|
|
126
|
+
spec has no commit date to compare; **`NO-CODE`** when it declares `governs: []`, i.e. a
|
|
127
|
+
decision that governs no source (negative ADRs) — a declaration, not an omission. The latest run lands in the ledger (`spec_drift`) so the
|
|
127
128
|
mirador can surface it. No readiness impact, no exit-code gate: a stale spec is a prompt for
|
|
128
129
|
a human conversation, not a blocked pipeline.
|
|
129
130
|
|
|
@@ -148,6 +149,35 @@ knowing a tree was dirty does not tell you the evidence is wrong, only that it w
|
|
|
148
149
|
produced from a commit alone. The git-worktree clean-room that would answer the stronger
|
|
149
150
|
question is deliberately deferred; ADR-007 records why.
|
|
150
151
|
|
|
152
|
+
## Clean-room (ADR-008) - verify the commit, not your tree
|
|
153
|
+
|
|
154
|
+
A suite can pass in your working tree *because of uncommitted state* and fail against the
|
|
155
|
+
commit alone. The ledger records that as measured, with provenance true of the TREE and not of
|
|
156
|
+
the COMMIT that will merge.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
python qa_ledger.py cleanroom --repo <name> --run "<your test command>" [--setup "npm ci"]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
It creates a detached `git worktree` of the commit, verifies it is clean by construction, runs
|
|
163
|
+
what you gave it, records `ref` / `worktree_sha` / `status` / wall-clock, and removes the
|
|
164
|
+
worktree unconditionally (a zombie worktree is a defect). Status is specific:
|
|
165
|
+
`GREEN`, `RED`, `SETUP_FAILED` (a bootstrap failure is not a failing suite),
|
|
166
|
+
`WORKTREE_FAILED`, `WORKTREE_DIRTY`.
|
|
167
|
+
|
|
168
|
+
**The engine never decides what to run.** The command arrives via `--run`, the same contract
|
|
169
|
+
`golden-coverage` uses for `--harness`: the engine owns isolation, the SHA binding and
|
|
170
|
+
cleanup, and never turns a config file into a code-execution surface. That is also why it
|
|
171
|
+
works for any stack - it never had to know the stack.
|
|
172
|
+
|
|
173
|
+
**Opt-in gate:** `defaults.clean_room.mode` absent or `"off"` and nothing changes. `"final"`
|
|
174
|
+
makes `pr-ready` require a GREEN run pinned to the current HEAD; a new commit makes the
|
|
175
|
+
previous run stale for the gate.
|
|
176
|
+
|
|
177
|
+
Honest limit: this is **not** a substitute for CI. A local worktree runs on your machine, your
|
|
178
|
+
OS, your shell - environment variance is invisible to it. Different failure class, different
|
|
179
|
+
instrument.
|
|
180
|
+
|
|
151
181
|
## End-to-end flow
|
|
152
182
|
|
|
153
183
|
`uscha-discovery` is the front for something new (you only have the idea); `uscha-adr-refine` is the front
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit 1.
|
|
1
|
+
uscha-kit 1.63.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"AC-CR-06": true, "AC-CR-02": true, "AC-CR-04": true, "AC-CR-05": true, "AC-CR-03": true, "AC-CR-01": true, "AC-CR-07": true, "AC-CR-08": true}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"AC-SD-01": true, "AC-SD-03": true, "AC-SD-02": true, "AC-SD-04": true}
|
|
1
|
+
{"AC-SD-01": true, "AC-SD-03": true, "AC-SD-05": true, "AC-SD-02": true, "AC-SD-04": true}
|
|
@@ -2629,6 +2629,35 @@ def _derive_phase(ledger, name, node, k, qa_order):
|
|
|
2629
2629
|
reasons.append("la evidencia medida no contiene tests ejecutados")
|
|
2630
2630
|
_go, sev = _gate_open_and_sev(node)
|
|
2631
2631
|
blk = sev.get("BLOCKER", 0) + sev.get("CRITICAL", 0)
|
|
2632
|
+
# clean-room gate (ADR-008). OPT-IN: no clean_room block, or mode "off", and this does not
|
|
2633
|
+
# exist -- behavior identical to earlier releases. Declared "final": pr-ready additionally
|
|
2634
|
+
# requires clean-room evidence that is GREEN and pinned to the CURRENT HEAD. A new commit
|
|
2635
|
+
# makes the previous run stale for the gate, the same staleness posture the rest of the
|
|
2636
|
+
# engine takes: evidence certifies the thing it was measured against, nothing later.
|
|
2637
|
+
_cr = _cr_cfg(ledger)
|
|
2638
|
+
if _cr and _cr.get("mode") == "final":
|
|
2639
|
+
_head = None
|
|
2640
|
+
_hr = None
|
|
2641
|
+
_crcfg = (_repo_cfg(ledger, name) if name != "integration"
|
|
2642
|
+
else {"path": "."}) # synthetic scope: never in config["repos"]
|
|
2643
|
+
try:
|
|
2644
|
+
_hr = subprocess.run(["git", "rev-parse", "HEAD"],
|
|
2645
|
+
cwd=_crcfg.get("path", "."),
|
|
2646
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
2647
|
+
errors="replace")
|
|
2648
|
+
except OSError:
|
|
2649
|
+
_hr = None
|
|
2650
|
+
if _hr is not None and _hr.returncode == 0:
|
|
2651
|
+
_head = _hr.stdout.strip()
|
|
2652
|
+
_run = _cr_latest(ledger, name, _head) if _head else None
|
|
2653
|
+
if not _head:
|
|
2654
|
+
reasons.append("clean-room declarado pero no se pudo resolver HEAD "
|
|
2655
|
+
"(no se mide, no se aprueba)")
|
|
2656
|
+
conv = False
|
|
2657
|
+
elif not _run or not _run.get("ok"):
|
|
2658
|
+
reasons.append("falta clean-room verde para %s (evidencia del arbol no "
|
|
2659
|
+
"certifica el commit)" % _head[:8])
|
|
2660
|
+
conv = False
|
|
2632
2661
|
if conv and tests_measured_green and not tests_red and blk == 0:
|
|
2633
2662
|
evidence = ["ciclo de agente limpio", "tests verdes (medidos)",
|
|
2634
2663
|
"0 BLOCKER/CRITICAL abiertos"]
|
|
@@ -3026,7 +3055,7 @@ def _sd_governs(path):
|
|
|
3026
3055
|
return None
|
|
3027
3056
|
if not lines or lines[0].strip() != "---":
|
|
3028
3057
|
return None
|
|
3029
|
-
globs, in_governs = None, False
|
|
3058
|
+
globs, in_governs, explicit_empty = None, False, False
|
|
3030
3059
|
# scan runs to the CLOSING fence, not an arbitrary window -- a governs: key late in a
|
|
3031
3060
|
# long frontmatter block must not silently read as UNMAPPED (fresh-review finding).
|
|
3032
3061
|
for ln in lines[1:]:
|
|
@@ -3038,6 +3067,8 @@ def _sd_governs(path):
|
|
|
3038
3067
|
if rest.startswith("[") and rest.endswith("]"):
|
|
3039
3068
|
globs = [x.strip().strip("\x27\x22")
|
|
3040
3069
|
for x in rest[1:-1].split(",") if x.strip()]
|
|
3070
|
+
# only an INLINE [] is a declaration of "nothing to govern"
|
|
3071
|
+
explicit_empty = not globs
|
|
3041
3072
|
in_governs = False
|
|
3042
3073
|
elif rest:
|
|
3043
3074
|
# bare scalar (`governs: src/**`) -- a plausible authoring shorthand;
|
|
@@ -3051,6 +3082,11 @@ def _sd_governs(path):
|
|
|
3051
3082
|
globs.append(s[2:].strip().strip("\x27\x22"))
|
|
3052
3083
|
elif s and not ln.startswith((" ", "\t")):
|
|
3053
3084
|
in_governs = False
|
|
3085
|
+
if globs == [] and not explicit_empty:
|
|
3086
|
+
# a `governs:` key with nothing usable under it (a placeholder, a comment, a typo) is
|
|
3087
|
+
# an UNFINISHED declaration, not a statement that this spec governs nothing. Report it
|
|
3088
|
+
# as UNMAPPED, which is what it is (fresh-review finding).
|
|
3089
|
+
return None
|
|
3054
3090
|
return globs
|
|
3055
3091
|
|
|
3056
3092
|
|
|
@@ -3111,6 +3147,16 @@ def cmd_spec_drift(args):
|
|
|
3111
3147
|
row.update({"verdict": "UNMAPPED", "reason": "no governs: frontmatter"})
|
|
3112
3148
|
results.append(row)
|
|
3113
3149
|
continue
|
|
3150
|
+
if not governs:
|
|
3151
|
+
# An EXPLICIT empty list is a declaration, not an omission: this decision governs
|
|
3152
|
+
# no code and never will. Negative ADRs ("we are NOT doing X, and why") are a
|
|
3153
|
+
# documented practice in this kit, and reporting them UNMAPPED forever turns a
|
|
3154
|
+
# correct state into permanent noise -- which is how an advisory gets ignored.
|
|
3155
|
+
# Found by running spec-drift on this repo's own ADR-004.
|
|
3156
|
+
row.update({"verdict": "NO-CODE",
|
|
3157
|
+
"reason": "declares governs: [] -- a decision that governs no code"})
|
|
3158
|
+
results.append(row)
|
|
3159
|
+
continue
|
|
3114
3160
|
matched = []
|
|
3115
3161
|
pats = [_fp_glob_re(g) for g in governs]
|
|
3116
3162
|
for f in tracked:
|
|
@@ -3171,7 +3217,8 @@ def cmd_spec_drift(args):
|
|
|
3171
3217
|
print("SPEC-DRIFT %s (advisory, lag > %dd):" % (args.repo, lag_days))
|
|
3172
3218
|
if not results:
|
|
3173
3219
|
print(" no spec documents found (SPEC.md / docs/adr/*.md)")
|
|
3174
|
-
mark = {"SPEC_STALE": "!!", "CLEAN": "ok", "UNMAPPED": "--", "UNTRACKED": "--"
|
|
3220
|
+
mark = {"SPEC_STALE": "!!", "CLEAN": "ok", "UNMAPPED": "--", "UNTRACKED": "--",
|
|
3221
|
+
"NO-CODE": "ok"}
|
|
3175
3222
|
for r_ in results:
|
|
3176
3223
|
line = " %s %s: %s" % (mark.get(r_["verdict"], "??"), r_["file"],
|
|
3177
3224
|
r_["verdict"])
|
|
@@ -3337,6 +3384,132 @@ def cmd_golden_coverage(args):
|
|
|
3337
3384
|
|
|
3338
3385
|
|
|
3339
3386
|
|
|
3387
|
+
# --------------------------------------------------------------------------- #
|
|
3388
|
+
# cleanroom (ADR-008: verify the COMMIT, not the tree -- opt-in, human-supplied command)
|
|
3389
|
+
# --------------------------------------------------------------------------- #
|
|
3390
|
+
|
|
3391
|
+
CLEAN_ROOM_KEY = "clean_room"
|
|
3392
|
+
|
|
3393
|
+
|
|
3394
|
+
def _cr_cfg(ledger):
|
|
3395
|
+
cfg = ledger["config"].get("defaults", {}).get(CLEAN_ROOM_KEY)
|
|
3396
|
+
return cfg if isinstance(cfg, dict) else None
|
|
3397
|
+
|
|
3398
|
+
|
|
3399
|
+
def _cr_latest(ledger, repo, ref=None):
|
|
3400
|
+
"""Latest clean-room record for a repo, optionally pinned to one ref."""
|
|
3401
|
+
runs = [e for e in ledger.get(CLEAN_ROOM_KEY, [])
|
|
3402
|
+
if e.get("repo") == repo and (ref is None or e.get("ref") == ref)]
|
|
3403
|
+
return runs[-1] if runs else None
|
|
3404
|
+
|
|
3405
|
+
|
|
3406
|
+
def cmd_cleanroom(args):
|
|
3407
|
+
"""Run a command against a CLEAN CHECKOUT of one commit, in a throwaway worktree.
|
|
3408
|
+
|
|
3409
|
+
Evidence produced in the maker's tree is true of the TREE; this produces evidence true
|
|
3410
|
+
of the COMMIT. As a side effect maker != checker becomes physical: the worktree cannot
|
|
3411
|
+
see uncommitted state.
|
|
3412
|
+
|
|
3413
|
+
The engine does NOT decide what to run. The command arrives explicitly via --run, the
|
|
3414
|
+
same contract golden-coverage uses for --harness: the engine owns what it can guarantee
|
|
3415
|
+
(isolation, the SHA binding, cleanup) and never guesses what a project's suite is.
|
|
3416
|
+
Reading test_command_* from config and executing it would make the engine an executor
|
|
3417
|
+
of config-supplied shell, which it is not (ADR-008)."""
|
|
3418
|
+
import time
|
|
3419
|
+
ledger = _load(args.ledger)
|
|
3420
|
+
_repo_node(ledger, args.repo)
|
|
3421
|
+
repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
|
|
3422
|
+
|
|
3423
|
+
def git(*a, **kw):
|
|
3424
|
+
cwd = kw.pop("cwd", repo_path)
|
|
3425
|
+
try:
|
|
3426
|
+
return subprocess.run(["git"] + list(a), cwd=cwd, capture_output=True,
|
|
3427
|
+
text=True, encoding="utf-8", errors="replace")
|
|
3428
|
+
except OSError:
|
|
3429
|
+
return None
|
|
3430
|
+
|
|
3431
|
+
rev = git("rev-parse", "--verify", (args.ref or "HEAD") + "^{commit}")
|
|
3432
|
+
if rev is None or rev.returncode != 0 or not rev.stdout.strip():
|
|
3433
|
+
print("[qa_ledger] cannot resolve ref %r in %s - nothing to verify."
|
|
3434
|
+
% (args.ref or "HEAD", repo_path), file=sys.stderr)
|
|
3435
|
+
sys.exit(2)
|
|
3436
|
+
sha = rev.stdout.strip()
|
|
3437
|
+
|
|
3438
|
+
wt = tempfile.mkdtemp(prefix="uscha-cleanroom-")
|
|
3439
|
+
target = os.path.join(wt, "tree")
|
|
3440
|
+
started = time.time()
|
|
3441
|
+
record = {"repo": args.repo, "ref": sha, "at": _now(), "ok": False,
|
|
3442
|
+
"status": None, "wall_ms": None, "worktree_sha": None}
|
|
3443
|
+
keep = bool((_cr_cfg(ledger) or {}).get("keep_worktree_on_failure"))
|
|
3444
|
+
try:
|
|
3445
|
+
add = git("worktree", "add", "--detach", target, sha)
|
|
3446
|
+
if add is None or add.returncode != 0:
|
|
3447
|
+
record["status"] = "WORKTREE_FAILED"
|
|
3448
|
+
print("[qa_ledger] git worktree add failed:\n%s"
|
|
3449
|
+
% ((add.stderr if add else "git unavailable") or "")[-800:], file=sys.stderr)
|
|
3450
|
+
else:
|
|
3451
|
+
# a worktree of a commit must be clean by construction; if it is not, something
|
|
3452
|
+
# (a smudge filter, a hook, a stale index) intervened and the isolation claim is
|
|
3453
|
+
# already false. Say so rather than measure in it.
|
|
3454
|
+
st = git("status", "--porcelain", cwd=target)
|
|
3455
|
+
if st is None or st.returncode != 0 or st.stdout.strip():
|
|
3456
|
+
record["status"] = "WORKTREE_DIRTY"
|
|
3457
|
+
else:
|
|
3458
|
+
record["worktree_sha"] = sha
|
|
3459
|
+
if args.setup:
|
|
3460
|
+
r = subprocess.run(args.setup, cwd=target, shell=True,
|
|
3461
|
+
capture_output=True, text=True,
|
|
3462
|
+
encoding="utf-8", errors="replace")
|
|
3463
|
+
if r.returncode != 0:
|
|
3464
|
+
record["status"] = "SETUP_FAILED"
|
|
3465
|
+
print((r.stderr or "")[-800:], file=sys.stderr)
|
|
3466
|
+
if record["status"] is None:
|
|
3467
|
+
r = subprocess.run(args.run, cwd=target, shell=True,
|
|
3468
|
+
capture_output=True, text=True,
|
|
3469
|
+
encoding="utf-8", errors="replace")
|
|
3470
|
+
record["exit_code"] = r.returncode
|
|
3471
|
+
record["status"] = "GREEN" if r.returncode == 0 else "RED"
|
|
3472
|
+
record["ok"] = r.returncode == 0
|
|
3473
|
+
if r.returncode != 0:
|
|
3474
|
+
print((r.stdout or "")[-1500:], file=sys.stderr)
|
|
3475
|
+
finally:
|
|
3476
|
+
record["wall_ms"] = int((time.time() - started) * 1000)
|
|
3477
|
+
# Cleanup is unconditional unless the human asked to inspect a FAILURE: a zombie
|
|
3478
|
+
# worktree is a defect, and `git worktree remove` alone leaves the admin entry behind.
|
|
3479
|
+
if record["ok"] or not keep:
|
|
3480
|
+
git("worktree", "remove", "--force", target)
|
|
3481
|
+
git("worktree", "prune")
|
|
3482
|
+
shutil.rmtree(wt, ignore_errors=True)
|
|
3483
|
+
# VERIFY the removal instead of assuming it. rmtree(ignore_errors=True) and a
|
|
3484
|
+
# discarded git return code can both fail in silence -- typically on Windows,
|
|
3485
|
+
# where a handle the caller's command left open blocks removal.
|
|
3486
|
+
if os.path.exists(target):
|
|
3487
|
+
record["cleanup_failed"] = True
|
|
3488
|
+
record["worktree_kept_at"] = target
|
|
3489
|
+
print("[qa_ledger] WARNING: the clean-room worktree could not be removed and "
|
|
3490
|
+
"is still at %s (a process may still hold a handle). Remove it with: "
|
|
3491
|
+
"git worktree remove --force %s && git worktree prune"
|
|
3492
|
+
% (target, target), file=sys.stderr)
|
|
3493
|
+
else:
|
|
3494
|
+
record["worktree_kept_at"] = target
|
|
3495
|
+
|
|
3496
|
+
ledger.setdefault(CLEAN_ROOM_KEY, []).append(record)
|
|
3497
|
+
ledger["step_counter"] += 1
|
|
3498
|
+
ledger["steps"].append({"n": ledger["step_counter"], "at": _now(),
|
|
3499
|
+
"kind": "cleanroom", "repo": args.repo})
|
|
3500
|
+
_save(args.ledger, ledger)
|
|
3501
|
+
|
|
3502
|
+
if args.json:
|
|
3503
|
+
print(json.dumps(record, indent=2, ensure_ascii=False))
|
|
3504
|
+
else:
|
|
3505
|
+
print("CLEANROOM %s @ %s: %s (%.1fs)"
|
|
3506
|
+
% (args.repo, sha[:8], record["status"], (record["wall_ms"] or 0) / 1000.0))
|
|
3507
|
+
if record.get("worktree_kept_at"):
|
|
3508
|
+
print(" worktree kept for inspection: " + record["worktree_kept_at"])
|
|
3509
|
+
sys.exit(0 if record["ok"] else 1)
|
|
3510
|
+
|
|
3511
|
+
|
|
3512
|
+
|
|
3340
3513
|
def cmd_escalate(args):
|
|
3341
3514
|
ledger = _load(args.ledger)
|
|
3342
3515
|
_repo_node(ledger, args.repo)
|
|
@@ -4383,6 +4556,9 @@ def cmd_dashboard(args):
|
|
|
4383
4556
|
_org[_rn] = _snaps[-1]["origin"]
|
|
4384
4557
|
if _org:
|
|
4385
4558
|
out["evidence_origin"] = _org
|
|
4559
|
+
if ledger.get(CLEAN_ROOM_KEY):
|
|
4560
|
+
out["clean_room"] = {r: [e for e in ledger[CLEAN_ROOM_KEY] if e.get("repo") == r][-1]
|
|
4561
|
+
for r in {e.get("repo") for e in ledger[CLEAN_ROOM_KEY]}}
|
|
4386
4562
|
if getattr(args, "json", False):
|
|
4387
4563
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
4388
4564
|
return
|
|
@@ -7087,6 +7263,17 @@ def build_parser():
|
|
|
7087
7263
|
pfp.add_argument("--json", action="store_true")
|
|
7088
7264
|
pfp.set_defaults(func=cmd_fastpath_eval)
|
|
7089
7265
|
|
|
7266
|
+
pcr = sub.add_parser("cleanroom",
|
|
7267
|
+
help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
|
|
7268
|
+
pcr.add_argument("--ledger", default="QA-LEDGER.json")
|
|
7269
|
+
pcr.add_argument("--repo", required=True)
|
|
7270
|
+
pcr.add_argument("--ref", default=None, help="commit to verify; default HEAD")
|
|
7271
|
+
pcr.add_argument("--run", required=True,
|
|
7272
|
+
help="the command to run inside the worktree; the engine never guesses it")
|
|
7273
|
+
pcr.add_argument("--setup", default=None, help="optional bootstrap before --run (e.g. npm ci)")
|
|
7274
|
+
pcr.add_argument("--json", action="store_true")
|
|
7275
|
+
pcr.set_defaults(func=cmd_cleanroom)
|
|
7276
|
+
|
|
7090
7277
|
pgc = sub.add_parser("golden-coverage",
|
|
7091
7278
|
help="record the MEASURED source files a golden's harness exercises (ADR-006)")
|
|
7092
7279
|
pgc.add_argument("--harness", required=True, help="script that drives the subject")
|
|
@@ -71,7 +71,7 @@ than inventing a step. Keep the CONTENT in the conversation's language and the l
|
|
|
71
71
|
repo straight from the ledger, or null when none was requested. The template degrades when
|
|
72
72
|
absent, like every other field.
|
|
73
73
|
- **Spec-drift (ADR-005):** `dashboard --json` carries `spec_drift` — the latest advisory
|
|
74
|
-
run (per-document verdicts: SPEC_STALE / CLEAN / UNMAPPED / UNTRACKED) — only when a run
|
|
74
|
+
run (per-document verdicts: SPEC_STALE / CLEAN / UNMAPPED / UNTRACKED / NO-CODE) — only when a run
|
|
75
75
|
exists in the ledger; a virgin ledger keeps the exact prior schema. Advisory visibility of
|
|
76
76
|
the spec-maintenance tax, never readiness input.
|
|
77
77
|
- **Modes card:** the template draws one card for both modes — fast-path verdict chips per
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.63.0",
|
|
3
3
|
"project": null,
|
|
4
4
|
"defaults": {
|
|
5
5
|
"coverage_threshold": 60,
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"spec_drift": {
|
|
56
56
|
"max_lag_days": 30
|
|
57
57
|
},
|
|
58
|
+
"clean_room": {
|
|
59
|
+
"mode": "off",
|
|
60
|
+
"keep_worktree_on_failure": false
|
|
61
|
+
},
|
|
58
62
|
"static_gate_zero_at": 10,
|
|
59
63
|
"constitution_file": "CONSTITUTION.md",
|
|
60
64
|
"rebuild": {
|