@andresmassello/uscha 1.91.0 → 1.92.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 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.91.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
43
+ **Kit v1.92.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
 
@@ -85,7 +85,7 @@ automatic tool can perform: a human verdict.
85
85
  from the compiled code: 0.828 measured (12 archetypes) — names AND behaviour
86
86
  ```
87
87
 
88
- **What each arrow is, in the engine (kit 1.91.0, 52 subcommands, all measured):**
88
+ **What each arrow is, in the engine (kit 1.92.0, 53 subcommands, all measured):**
89
89
 
90
90
  | Leg | Subcommands | What it establishes |
91
91
  |---|---|---|
@@ -145,7 +145,7 @@ and see which file, which test, and when.
145
145
  | `/uscha-mirador` | Bird's-eye HTML dashboard: readiness, trail, acceptance, loops |
146
146
  | `/uscha-status` | One-line progress readout, in chat |
147
147
 
148
- **A measurement engine** (`qa_ledger.py`, 52 subcommands, Python stdlib) that ingests
148
+ **A measurement engine** (`qa_ledger.py`, 53 subcommands, Python stdlib) that ingests
149
149
  evidence from **11 language stacks** — maven, gradle, ant, python, node, go, rust, dotnet,
150
150
  cpp, swift, flutter — and computes a readiness score with hard caps and visible provenance.
151
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresmassello/uscha",
3
- "version": "1.91.0",
3
+ "version": "1.92.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",
@@ -459,6 +459,18 @@ BLOCKER/CRITICAL + no open escalation), never self-declared — if it exits 1, t
459
459
  output lists exactly which facts are missing; do NOT open the PR, close the gap.
460
460
  A `spike/*` branch NEVER passes this gate (kit 1.19.0): spike code is disposable
461
461
  by contract — its only legitimate output is an ADR with lessons, never a merge.
462
+ - **Before declaring TERMINADO, run the seal (kit 1.92.0, INV-T1 / ADR-038):**
463
+
464
+ ```bash
465
+ python3 $QL check-terminado # 0 = sealed · 1 = broken · 2 = UNMEASURED
466
+ ```
467
+
468
+ It recomputes, from the ledger and the tree, whether the recorded evidence still belongs to
469
+ the code on disk: the repo subtree clean, `HEAD` equal to the last snapshot's commit, every
470
+ ingested report still hashing to what was recorded. **Exit 1** — do not declare TERMINADO:
471
+ re-snapshot on the CURRENT state (`snapshot --repo <REPO> --phase post`) and record why the
472
+ seal broke. **Exit 2** — the seal is UNMEASURED (no git, or no snapshot recorded): say so
473
+ plainly; an answer nobody could measure is not a TERMINADO either.
462
474
  - Ensure conventional-commit history is clean.
463
475
  - Open the PR(s). Confirm CI is green.
464
476
  - **STOP.** Present the PR link(s) and wait for the human to merge.
@@ -704,7 +704,14 @@ def _source_newest_mtime(repo_path):
704
704
  def _test_evidence_provenance(repo_path, repo_type):
705
705
  """Explain which JUnit reports back a snapshot and whether they are newer
706
706
  than relevant source/test files. No discoverable source is explicitly
707
- uncorrelated-but-usable to preserve synthetic/report-only workflows."""
707
+ uncorrelated-but-usable to preserve synthetic/report-only workflows.
708
+
709
+ Since 1.92.0 (ADR-038) each report also carries its CONTENT hash. Path and mtime
710
+ answer "which file, and was it written after the source"; they cannot answer "is
711
+ this still the file that was ingested" -- a log swapped or edited after the run
712
+ keeps its name and can keep its date. The hash is taken over the same files this
713
+ function already selects and the parser already reads: no new file is opened, and
714
+ `None` (unreadable) is recorded as absence, never as a match."""
708
715
  files = _junit_files_for(repo_path, repo_type)
709
716
  reports = []
710
717
  for path in files:
@@ -717,6 +724,7 @@ def _test_evidence_provenance(repo_path, repo_type):
717
724
  "mtime_ns": mtime_ns,
718
725
  "mtime": datetime.fromtimestamp(
719
726
  mtime_ns / 1_000_000_000, timezone.utc).isoformat(),
727
+ "sha256": _sha256_file(path),
720
728
  })
721
729
  if not reports:
722
730
  status = "not-applicable" if repo_type == "flutter" else "missing"
@@ -8639,6 +8647,205 @@ def _top_spec_diff(ledger):
8639
8647
  "source": "spec-drift"}
8640
8648
 
8641
8649
 
8650
+ SEAL_NO_GIT = "no git work tree — seal UNMEASURED"
8651
+ SEAL_NO_COMMIT = "git repo without commits — seal UNMEASURED"
8652
+
8653
+
8654
+ def _seal_git(repo_path, *argv):
8655
+ """One git read for the seal, or None. Same OSError posture as `_evidence_origin`:
8656
+ git absent, or a repo path that does not exist, is an ordinary state of the world and
8657
+ must degrade to UNMEASURED, never take down the command it only annotates."""
8658
+ try:
8659
+ r = subprocess.run(["git"] + list(argv), cwd=repo_path, capture_output=True,
8660
+ text=True, encoding="utf-8", errors="replace")
8661
+ except OSError:
8662
+ return None
8663
+ return r if r.returncode == 0 else None
8664
+
8665
+
8666
+ def _seal_rel(work_tree, path):
8667
+ """`path` as git names it: relative to the work tree root, forward slashes, or None
8668
+ when it falls outside the tree.
8669
+
8670
+ BOTH sides go through `realpath` first. On Windows one API answers with an 8.3 short
8671
+ name (`RUNNER~1`) and another with the long one, and `relpath` between the two yields
8672
+ `..\\..` for a file plainly inside the tree — the CI-only failure paid for on
8673
+ 2026-08-02. A None here can only WITHHOLD an exemption, so the seal fails closed."""
8674
+ try:
8675
+ rel = os.path.relpath(os.path.realpath(path), os.path.realpath(work_tree))
8676
+ except (OSError, ValueError):
8677
+ return None
8678
+ rel = rel.replace("\\", "/")
8679
+ return None if rel == ".." or rel.startswith("../") else rel
8680
+
8681
+
8682
+ def _porcelain_paths(text):
8683
+ """Every path named by `git status --porcelain -uall`, rename destinations included.
8684
+
8685
+ A path with special characters comes back C-quoted; the quotes are stripped and any
8686
+ escape inside is left as-is. That can only fail to MATCH an exemption, which leaves the
8687
+ seal broken — the safe direction: an unrecognized change is dirt, never a pass."""
8688
+ out = []
8689
+ for line in (text or "").splitlines():
8690
+ if len(line) < 4:
8691
+ continue
8692
+ for part in line[3:].split(" -> "):
8693
+ part = part.strip()
8694
+ if len(part) >= 2 and part[0] == '"' and part[-1] == '"':
8695
+ part = part[1:-1]
8696
+ if part:
8697
+ out.append(part.replace("\\", "/"))
8698
+ return out
8699
+
8700
+
8701
+ def _sealed_state(ledger, ledger_path):
8702
+ """INV-T1 (ADR-038): is the recorded evidence bound to the code state on disk RIGHT NOW?
8703
+
8704
+ Derived at read time, never written: nothing here creates a file, and re-deriving it is
8705
+ the only way it can be trusted — a stored verdict is a claim about a tree that has moved
8706
+ on since. Three questions, all answerable from what the ledger already carries:
8707
+
8708
+ 1. is the TRACKED REPO'S SUBTREE clean -- `git status ... -- .` inside the configured
8709
+ repo path, the same per-path scoping `_evidence_origin` uses (ADR-007), so a
8710
+ monorepo sibling's edit is not this repo's dirt -- ignoring the ledger itself and
8711
+ the report files the last snapshot names (those two are the seal's own footprint,
8712
+ exactly as the reference `sh` package exempts `EVIDENCIA.md` and the logs it hashes);
8713
+ 2. is `HEAD` the commit that snapshot was taken at (`origin.commit`, ADR-007);
8714
+ 3. does every report the snapshot names still exist and still hash to what was
8715
+ recorded at ingest (`sha256`, added in 1.92.0).
8716
+
8717
+ Three verdicts, never two: `True` sealed, `False` a MEASURED break (the reasons say
8718
+ which), `None` UNMEASURED — no git work tree, or a snapshot old enough to predate the
8719
+ content hash. A measured break outranks an unmeasured check (fail-closed); an unmeasured
8720
+ check never reads as a pass (INV-TOP-05). The repo is the FIRST configured one, the same
8721
+ choice `_top_spec_pin` and `_top_repos` make, and it is named in the `repo` member so
8722
+ every reason below is read against it.
8723
+
8724
+ The block carries NO timestamp of its own. It is recomputed on every read, so a
8725
+ "checked at" would be a second wall clock inside a payload whose only other one is
8726
+ `generated_at` -- and two consecutive `top --json` runs must differ in nothing else
8727
+ (AC-T-24 measures exactly that, and caught this before it shipped)."""
8728
+ out = {"ok": None, "reasons": [], "commit": None, "repo": None}
8729
+ repos = (ledger.get("config", {}) or {}).get("repos") or []
8730
+ if not repos:
8731
+ out["reasons"].append("no repo configured — seal UNMEASURED")
8732
+ return out
8733
+ name = repos[0].get("name")
8734
+ path = repos[0].get("path", ".")
8735
+ out["repo"] = _top_clean(name) if name else None
8736
+
8737
+ top = _seal_git(path, "rev-parse", "--show-toplevel")
8738
+ if top is None or not top.stdout.strip():
8739
+ out["reasons"].append(SEAL_NO_GIT)
8740
+ return out
8741
+ head = _seal_git(path, "rev-parse", "HEAD")
8742
+ if head is None or not head.stdout.strip():
8743
+ # a git tree with no commit yet: `rev-parse HEAD` fails on an unborn branch. It is a
8744
+ # DIFFERENT absence from "not a work tree" and the reason says so -- the verdict is
8745
+ # the same UNMEASURED, but a reason that misnames the cause sends the reader to the
8746
+ # wrong fix.
8747
+ out["reasons"].append(SEAL_NO_COMMIT)
8748
+ return out
8749
+ head_sha = head.stdout.strip()
8750
+ work_tree = top.stdout.strip()
8751
+ out["commit"] = head_sha
8752
+
8753
+ snaps = ((ledger.get("repos") or {}).get(name) or {}).get("snapshots") or []
8754
+ if not snaps:
8755
+ # UNMEASURED, not broken. The reference `sh` package calls a missing EVIDENCIA.md a
8756
+ # rejection, and that is right for a file whose only job is to be the seal -- but a
8757
+ # snapshot is the INGEST record, and with none recorded there is nothing to compare
8758
+ # the tree against: not "the evidence is stale", not "the evidence was altered",
8759
+ # simply no anchor. Calling that a break would also make the seal non-deterministic
8760
+ # for a board whose evidence is read live from reports (the `top` fixtures are
8761
+ # exactly that), and INV-TOP-05 already fixes the posture: absence renders as
8762
+ # absence. The teeth stay where they bite -- `check-terminado` exits 2, so a hook or
8763
+ # a human gating on exit 0 still refuses. The LIMIT this leaves is stated out loud in
8764
+ # SPEC §4: a board at 100% with no snapshot at all carries no seal marker.
8765
+ out["reasons"].append("no snapshot recorded yet")
8766
+ return out
8767
+ snap = snaps[-1]
8768
+
8769
+ failures, unmeasured = [], []
8770
+ snap_commit = (snap.get("origin") or {}).get("commit")
8771
+ if not snap_commit:
8772
+ unmeasured.append("snapshot recorded no commit — seal UNMEASURED")
8773
+ elif snap_commit != head_sha:
8774
+ failures.append("stale seal: snapshot at %s, HEAD is %s"
8775
+ % (snap_commit[:8], head_sha[:8]))
8776
+
8777
+ reports = [r for r in ((snap.get("tests") or {}).get("reports") or [])
8778
+ if isinstance(r, dict) and r.get("path")]
8779
+ exempt = set()
8780
+ for candidate in [ledger_path] + [os.path.join(path, r["path"]) for r in reports]:
8781
+ rel = _seal_rel(work_tree, candidate)
8782
+ if rel:
8783
+ exempt.add(rel)
8784
+ # `core.quotepath=false`: without it git C-quotes any non-ASCII path, so a report named
8785
+ # `junit-acción.xml` would appear in the reason as `junit-acción.xml` -- a reason
8786
+ # nobody can act on, and an exemption that cannot match. Set on the command, never in the
8787
+ # user's config: the engine reads git, it does not configure it.
8788
+ st = _seal_git(path, "-c", "core.quotepath=false",
8789
+ "status", "--porcelain", "-uall", "--", ".")
8790
+ if st is None:
8791
+ unmeasured.append("repo subtree state unreadable — seal UNMEASURED")
8792
+ else:
8793
+ dirty = sorted(set(_porcelain_paths(st.stdout)) - exempt)
8794
+ if dirty:
8795
+ failures.append("repo subtree dirty: changes no snapshot covers (%s)" % dirty[0])
8796
+
8797
+ for r in reports:
8798
+ rel, full = r["path"], os.path.join(path, r["path"])
8799
+ if not os.path.isfile(full):
8800
+ failures.append("evidence missing: %s" % rel)
8801
+ elif not r.get("sha256"):
8802
+ unmeasured.append("evidence hash unmeasured: %s — no hash recorded at ingest "
8803
+ "(older snapshot, or the file was unreadable)" % rel)
8804
+ elif _sha256_file(full) != r["sha256"]:
8805
+ failures.append("evidence altered after ingest: %s" % rel)
8806
+
8807
+ out["reasons"] = failures + unmeasured
8808
+ out["ok"] = False if failures else (None if unmeasured else True)
8809
+ return out
8810
+
8811
+
8812
+ def cmd_check_terminado(args):
8813
+ """The enforcement side of INV-T1: the SAME `_sealed_state` derivation `top --json`
8814
+ publishes, with an exit code a hook or a human can act on. It measures the tree and
8815
+ prints; it writes nothing and it decides nothing else.
8816
+
8817
+ 0 = sealed · 1 = a measured break · 2 = UNMEASURED (no git work tree, no configured
8818
+ repo, a snapshot with no recorded hash -- or no readable ledger at all). 2 is the
8819
+ reference script's error class: "I could not answer" is not "yes".
8820
+
8821
+ A missing or corrupt ledger is UNMEASURED, not a break: `_load` exits 1 by design, and 1
8822
+ here means "I checked and the seal is broken". Reporting "not sealed" for a file the
8823
+ command never managed to read would be a verdict on evidence nobody looked at -- the
8824
+ exact failure this command exists to catch."""
8825
+ try:
8826
+ ledger = _load(args.ledger)
8827
+ except SystemExit as exc:
8828
+ message = exc.code if isinstance(exc.code, str) else None
8829
+ print(message or "[qa_ledger] check-terminado: ledger '%s' unreadable" % args.ledger)
8830
+ print("[qa_ledger] check-terminado: UNMEASURED — no readable ledger, no seal.")
8831
+ sys.exit(2)
8832
+ sealed = _sealed_state(ledger, args.ledger)
8833
+ ok = sealed.get("ok")
8834
+ if getattr(args, "json", False):
8835
+ print(json.dumps(sealed, indent=2, ensure_ascii=False))
8836
+ else:
8837
+ verdict = "SEALED" if ok is True else ("UNSEALED" if ok is False else "UNMEASURED")
8838
+ where = " at %s" % sealed["commit"][:8] if sealed.get("commit") else ""
8839
+ print("[qa_ledger] check-terminado: %s%s (repo %s)"
8840
+ % (verdict, where, sealed.get("repo") or "?"))
8841
+ for reason in sealed.get("reasons") or []:
8842
+ print(" - %s" % reason)
8843
+ if ok is not True:
8844
+ print(" TERMINADO is not enabled: re-run the evidence and `snapshot` "
8845
+ "on the current state.")
8846
+ sys.exit(0 if ok is True else (1 if ok is False else 2))
8847
+
8848
+
8642
8849
  def cmd_top(args):
8643
8850
  """`uscha top` — the WHOLE projection of the ledger as one read-only JSON (ADR-032).
8644
8851
 
@@ -8744,6 +8951,15 @@ def cmd_top(args):
8744
8951
  done, fail, quar = _n("MEASURED_PASS"), _n("MEASURED_FAIL"), _n("QUARANTINE")
8745
8952
  unmeasured = _n("UNMEASURED") + _n("TRACED")
8746
8953
  pct = _top_pct(done, total)
8954
+ # INV-TOP-06 (ADR-038): DONE never publishes 100% while the seal is MEASURED broken --
8955
+ # every criterion green against evidence that no longer belongs to this code state is
8956
+ # the same lie INV-TOP-01 forbids one row earlier. The cap lives HERE, beside the
8957
+ # rounding cap, so no renderer is the place it happens. An UNMEASURED seal (`ok is
8958
+ # None` -- no git) does NOT cap: absence of measurement is not evidence of a break, and
8959
+ # capping on it would put an unearned 99 on every non-git tree.
8960
+ sealed = _sealed_state(ledger, args.ledger)
8961
+ if sealed.get("ok") is False and pct >= 100:
8962
+ pct = 99
8747
8963
  measured = done + fail
8748
8964
  out = {
8749
8965
  "schema": TOP_SCHEMA,
@@ -8760,7 +8976,10 @@ def cmd_top(args):
8760
8976
  "events_tail": _top_events(ledger),
8761
8977
  "counts": {"measured_pass": done, "measured_fail": fail, "quarantine": quar,
8762
8978
  "unmeasured": _n("UNMEASURED"), "traced": 0, "tagged": 0, "total": total},
8763
- "terminado": {"done": done, "total": total, "pct": pct, "unmeasured": unmeasured},
8979
+ # `sealed` is DERIVED at read time from the ledger plus the tree (ADR-038); it is
8980
+ # never stored, so it cannot go stale the way the claim it guards can.
8981
+ "terminado": {"done": done, "total": total, "pct": pct, "unmeasured": unmeasured,
8982
+ "sealed": sealed},
8764
8983
  "debtors": {"machine": fail, "you": quar, "untagged": unmeasured},
8765
8984
  "honesty": {"measured": measured, "total": total,
8766
8985
  "pct": _top_pct(measured, total)},
@@ -11952,6 +12171,14 @@ def build_parser():
11952
12171
  ptop.add_argument("--json", action="store_true")
11953
12172
  ptop.set_defaults(func=cmd_top)
11954
12173
 
12174
+ pct = sub.add_parser("check-terminado",
12175
+ help="INV-T1 (ADR-038): is TERMINADO sealed to the code state on "
12176
+ "disk? Same derivation `top --json` publishes. Exit 0 sealed, "
12177
+ "1 broken, 2 UNMEASURED")
12178
+ add_ledger(pct)
12179
+ pct.add_argument("--json", action="store_true")
12180
+ pct.set_defaults(func=cmd_check_terminado)
12181
+
11955
12182
  pb = sub.add_parser("rebuild",
11956
12183
  help="rebuild test: is the SPEC complete enough to "
11957
12184
  "regenerate the system? (completeness, not correctness)")
@@ -131,6 +131,17 @@ ACTIONS = {
131
131
  "TAGGED": "machine: run the case",
132
132
  }
133
133
 
134
+ # INV-TOP-06 (ADR-038): what a row that is green ON PAPER is actually waiting on when the
135
+ # seal is broken. Presentation, like ACTIONS above: the engine says WHAT broke (the reason),
136
+ # this says what the reader does about it, and the mapping is by reason prefix so a new
137
+ # reason class degrades to the generic line instead of to silence.
138
+ SEAL_ACTIONS = (
139
+ ("stale seal", "seal: snapshot at HEAD"),
140
+ ("repo subtree dirty", "seal: commit or discard, then snapshot"),
141
+ ("evidence altered", "seal: re-run the suite, then snapshot"),
142
+ ("evidence missing", "seal: re-run the suite, then snapshot"),
143
+ )
144
+
134
145
 
135
146
  # --------------------------------------------------------------------------- #
136
147
  # pure rendering #
@@ -216,7 +227,15 @@ def _pct_line(terminado):
216
227
  """INV-TOP-01: the DONE bar carries an explicit `N unmeasured` suffix whenever anything
217
228
  is unmeasured, and the engine has already capped the percentage below 100 while any
218
229
  obligation sits outside MEASURED_PASS -- the renderer republishes that fact, it never
219
- recomputes it (AC-T-01, AC-T-04, AC-T-23)."""
230
+ recomputes it (AC-T-01, AC-T-04, AC-T-23).
231
+
232
+ INV-TOP-06 (ADR-038) rides on the same line: when the engine's seal is MEASURED broken
233
+ (`terminado.sealed.ok is False`) the bar says so and names the first reason -- and the
234
+ percentage beside it is already capped below 100, in the engine, for the same reason the
235
+ unmeasured cap is (single derivation, AC-T-24). An UNMEASURED seal (`ok is null`: no git
236
+ work tree, the state of every frozen fixture) adds NOTHING here: the seal is shown only
237
+ when it is measured, and decorating a header with the absence of a measurement would
238
+ turn INV-TOP-05's `—` into noise on every board."""
220
239
  done = terminado.get("done")
221
240
  total = terminado.get("total")
222
241
  pct = terminado.get("pct")
@@ -224,9 +243,37 @@ def _pct_line(terminado):
224
243
  line = "DONE %s/%s (%s%%)" % (_num(done), _num(total), _num(pct))
225
244
  if unm:
226
245
  line += " %s %d unmeasured" % (MID, unm)
246
+ # the state is a FILE a human can hand us (`--state`), so `sealed` is guarded by TYPE and
247
+ # not merely by truthiness: a string there would answer `.get` with an AttributeError, and a
248
+ # `reasons` that is a string is iterable -- the frame would name its first CHARACTER as the
249
+ # reason. Same guards `_top_spec_diff` applies on the engine side, for the same reason.
250
+ seal = terminado.get("sealed")
251
+ seal = seal if isinstance(seal, dict) else {}
252
+ if seal.get("ok") is False:
253
+ raw = seal.get("reasons")
254
+ reasons = [r for r in raw if isinstance(r, str) and r] if isinstance(raw, list) else []
255
+ line += " %s unsealed (%s)" % (MID, _safe(reasons[0]) if reasons
256
+ else "no reason recorded")
227
257
  return line
228
258
 
229
259
 
260
+ def _seal_action(sealed):
261
+ """The ACTION cell of a row that is green on paper while the seal is broken. Empty
262
+ whenever the seal is not MEASURED broken -- an unmeasured seal changes no row, and a
263
+ `sealed` of the wrong TYPE reads as no seal at all rather than raising mid-frame."""
264
+ seal = sealed if isinstance(sealed, dict) else {}
265
+ if seal.get("ok") is not False:
266
+ return ""
267
+ raw = seal.get("reasons")
268
+ for reason in (raw if isinstance(raw, list) else []):
269
+ if not isinstance(reason, str):
270
+ continue
271
+ for prefix, action in SEAL_ACTIONS:
272
+ if reason.startswith(prefix):
273
+ return action
274
+ return "seal: re-snapshot the current state"
275
+
276
+
230
277
  def _burnup_line(burnup, cols):
231
278
  """The score trend, labelled as a score trend. v0.1 has no obligation-count history
232
279
  (ADR-035/2), so calling this a burn-up of closed obligations would be a lie the label
@@ -264,15 +311,21 @@ def _cases_text(ob):
264
311
  return "%s/%s" % (_num(ob.get("cases_pass")), total)
265
312
 
266
313
 
267
- def _row(ob, selected):
314
+ def _row(ob, selected, seal_action=""):
268
315
  # the three left columns are cut and padded in COLUMNS: an id or state carrying wide
269
316
  # characters used to eat its neighbour's field and walk every column after it.
270
317
  gutter = "> " if selected else " "
318
+ action = ACTIONS.get(ob.get("state"), DASH)
319
+ # INV-TOP-06: only the rows that CLAIM to be done change, and only while the seal is
320
+ # measured broken. A failing or unmeasured row already names its own debtor; telling it
321
+ # about the seal too would bury the thing it is actually waiting for.
322
+ if seal_action and ob.get("state") == "MEASURED_PASS":
323
+ action = seal_action
271
324
  return "%s%s%s%s%7s%5s %s" % (
272
325
  gutter, _pad(_cut(_safe(ob.get("id") or "?"), 8), 8),
273
326
  _pad(_cut(_safe(ob.get("gate") or DASH), 8), 9),
274
327
  _pad(_cut(_safe(ob.get("state") or "?"), 14), 15), _cases_text(ob),
275
- _num(ob.get("age_hours")), ACTIONS.get(ob.get("state"), DASH))
328
+ _num(ob.get("age_hours")), action)
276
329
 
277
330
 
278
331
  def _safe(text):
@@ -382,8 +435,9 @@ def _render_board(state, size, sel, plain, status=""):
382
435
  top = max(0, top)
383
436
 
384
437
  table = []
438
+ seal_action = _seal_action(terminado.get("sealed"))
385
439
  for i, ob in enumerate(obligations[top:top + body], start=top):
386
- line = _fit(_row(ob, i == sel), cols)
440
+ line = _fit(_row(ob, i == sel, seal_action), cols)
387
441
  table.append(line if plain else _colorize(line, ob.get("state")))
388
442
  hidden = len(obligations) - len(table)
389
443
  if hidden > 0:
@@ -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.91.0",
4
+ "version": "1.92.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, 52 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
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, 53 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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "1.91.0",
3
+ "version": "1.92.0",
4
4
  "description": "Uscha spec-driven development methodology for coding agents. Includes npm/npx router.",
5
5
  "author": {
6
6
  "name": "Andres Massello",
@@ -1,6 +1,6 @@
1
1
  # uscha-kit
2
2
 
3
- **Kit version:** v1.91.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.92.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`,
@@ -591,7 +591,7 @@ python3 $QL simplicity-check --diff changes.diff --json # consumed by usc
591
591
 
592
592
  ## Ledger subcommands
593
593
 
594
- `bench - bench-curate - bench-r2 - bench-roundtrip - bootstrap-oracle - bootstrap-variance - check-coverage - cleanroom - compile-ingest - compile-validate - converged - curate - curation-check - dashboard - discover - doctor - escalate - execution-policy - facts - fastpath-eval - fidelity - flag-blocker - gate-check - golden-coverage - golden-diff - ingest-gate - init - ir-extract - ir-render - lang-compare - log-gate - log-step - oscillation - phase - pit-check - production-finding - promote - readiness - rebuild - regression-check - resolve-escalation - roundtrip - rubric-ingest - simplicity-check - snapshot - spec-change-request - spec-check - spec-doubt - spec-drift - summary - top - waste-check` - the exact current `qa_ledger.py` parser surface (52 subcommands, derived from `SYSTEM-FACTS.json`, itself introspected from `build_parser()`); each supports `--help`.
594
+ `bench - bench-curate - bench-r2 - bench-roundtrip - bootstrap-oracle - bootstrap-variance - check-coverage - check-terminado - cleanroom - compile-ingest - compile-validate - converged - curate - curation-check - dashboard - discover - doctor - escalate - execution-policy - facts - fastpath-eval - fidelity - flag-blocker - gate-check - golden-coverage - golden-diff - ingest-gate - init - ir-extract - ir-render - lang-compare - log-gate - log-step - oscillation - phase - pit-check - production-finding - promote - readiness - rebuild - regression-check - resolve-escalation - roundtrip - rubric-ingest - simplicity-check - snapshot - spec-change-request - spec-check - spec-doubt - spec-drift - summary - top - waste-check` - the exact current `qa_ledger.py` parser surface (53 subcommands, derived from `SYSTEM-FACTS.json`, itself introspected from `build_parser()`); each supports `--help`.
595
595
 
596
596
  The **fact gates** (golden-diff, gate-check, pit-check, simplicity) are PERSISTED with
597
597
  `log-gate`: a fail blocks convergence and caps readiness ≤65 via the ledger. A CONSTITUTION
package/uscha-kit/VERSION CHANGED
@@ -1 +1 @@
1
- uscha-kit 1.91.0
1
+ uscha-kit 1.92.0
@@ -459,6 +459,18 @@ BLOCKER/CRITICAL + no open escalation), never self-declared — if it exits 1, t
459
459
  output lists exactly which facts are missing; do NOT open the PR, close the gap.
460
460
  A `spike/*` branch NEVER passes this gate (kit 1.19.0): spike code is disposable
461
461
  by contract — its only legitimate output is an ADR with lessons, never a merge.
462
+ - **Before declaring TERMINADO, run the seal (kit 1.92.0, INV-T1 / ADR-038):**
463
+
464
+ ```bash
465
+ python3 $QL check-terminado # 0 = sealed · 1 = broken · 2 = UNMEASURED
466
+ ```
467
+
468
+ It recomputes, from the ledger and the tree, whether the recorded evidence still belongs to
469
+ the code on disk: the repo subtree clean, `HEAD` equal to the last snapshot's commit, every
470
+ ingested report still hashing to what was recorded. **Exit 1** — do not declare TERMINADO:
471
+ re-snapshot on the CURRENT state (`snapshot --repo <REPO> --phase post`) and record why the
472
+ seal broke. **Exit 2** — the seal is UNMEASURED (no git, or no snapshot recorded): say so
473
+ plainly; an answer nobody could measure is not a TERMINADO either.
462
474
  - Ensure conventional-commit history is clean.
463
475
  - Open the PR(s). Confirm CI is green.
464
476
  - **STOP.** Present the PR link(s) and wait for the human to merge.
@@ -704,7 +704,14 @@ def _source_newest_mtime(repo_path):
704
704
  def _test_evidence_provenance(repo_path, repo_type):
705
705
  """Explain which JUnit reports back a snapshot and whether they are newer
706
706
  than relevant source/test files. No discoverable source is explicitly
707
- uncorrelated-but-usable to preserve synthetic/report-only workflows."""
707
+ uncorrelated-but-usable to preserve synthetic/report-only workflows.
708
+
709
+ Since 1.92.0 (ADR-038) each report also carries its CONTENT hash. Path and mtime
710
+ answer "which file, and was it written after the source"; they cannot answer "is
711
+ this still the file that was ingested" -- a log swapped or edited after the run
712
+ keeps its name and can keep its date. The hash is taken over the same files this
713
+ function already selects and the parser already reads: no new file is opened, and
714
+ `None` (unreadable) is recorded as absence, never as a match."""
708
715
  files = _junit_files_for(repo_path, repo_type)
709
716
  reports = []
710
717
  for path in files:
@@ -717,6 +724,7 @@ def _test_evidence_provenance(repo_path, repo_type):
717
724
  "mtime_ns": mtime_ns,
718
725
  "mtime": datetime.fromtimestamp(
719
726
  mtime_ns / 1_000_000_000, timezone.utc).isoformat(),
727
+ "sha256": _sha256_file(path),
720
728
  })
721
729
  if not reports:
722
730
  status = "not-applicable" if repo_type == "flutter" else "missing"
@@ -8639,6 +8647,205 @@ def _top_spec_diff(ledger):
8639
8647
  "source": "spec-drift"}
8640
8648
 
8641
8649
 
8650
+ SEAL_NO_GIT = "no git work tree — seal UNMEASURED"
8651
+ SEAL_NO_COMMIT = "git repo without commits — seal UNMEASURED"
8652
+
8653
+
8654
+ def _seal_git(repo_path, *argv):
8655
+ """One git read for the seal, or None. Same OSError posture as `_evidence_origin`:
8656
+ git absent, or a repo path that does not exist, is an ordinary state of the world and
8657
+ must degrade to UNMEASURED, never take down the command it only annotates."""
8658
+ try:
8659
+ r = subprocess.run(["git"] + list(argv), cwd=repo_path, capture_output=True,
8660
+ text=True, encoding="utf-8", errors="replace")
8661
+ except OSError:
8662
+ return None
8663
+ return r if r.returncode == 0 else None
8664
+
8665
+
8666
+ def _seal_rel(work_tree, path):
8667
+ """`path` as git names it: relative to the work tree root, forward slashes, or None
8668
+ when it falls outside the tree.
8669
+
8670
+ BOTH sides go through `realpath` first. On Windows one API answers with an 8.3 short
8671
+ name (`RUNNER~1`) and another with the long one, and `relpath` between the two yields
8672
+ `..\\..` for a file plainly inside the tree — the CI-only failure paid for on
8673
+ 2026-08-02. A None here can only WITHHOLD an exemption, so the seal fails closed."""
8674
+ try:
8675
+ rel = os.path.relpath(os.path.realpath(path), os.path.realpath(work_tree))
8676
+ except (OSError, ValueError):
8677
+ return None
8678
+ rel = rel.replace("\\", "/")
8679
+ return None if rel == ".." or rel.startswith("../") else rel
8680
+
8681
+
8682
+ def _porcelain_paths(text):
8683
+ """Every path named by `git status --porcelain -uall`, rename destinations included.
8684
+
8685
+ A path with special characters comes back C-quoted; the quotes are stripped and any
8686
+ escape inside is left as-is. That can only fail to MATCH an exemption, which leaves the
8687
+ seal broken — the safe direction: an unrecognized change is dirt, never a pass."""
8688
+ out = []
8689
+ for line in (text or "").splitlines():
8690
+ if len(line) < 4:
8691
+ continue
8692
+ for part in line[3:].split(" -> "):
8693
+ part = part.strip()
8694
+ if len(part) >= 2 and part[0] == '"' and part[-1] == '"':
8695
+ part = part[1:-1]
8696
+ if part:
8697
+ out.append(part.replace("\\", "/"))
8698
+ return out
8699
+
8700
+
8701
+ def _sealed_state(ledger, ledger_path):
8702
+ """INV-T1 (ADR-038): is the recorded evidence bound to the code state on disk RIGHT NOW?
8703
+
8704
+ Derived at read time, never written: nothing here creates a file, and re-deriving it is
8705
+ the only way it can be trusted — a stored verdict is a claim about a tree that has moved
8706
+ on since. Three questions, all answerable from what the ledger already carries:
8707
+
8708
+ 1. is the TRACKED REPO'S SUBTREE clean -- `git status ... -- .` inside the configured
8709
+ repo path, the same per-path scoping `_evidence_origin` uses (ADR-007), so a
8710
+ monorepo sibling's edit is not this repo's dirt -- ignoring the ledger itself and
8711
+ the report files the last snapshot names (those two are the seal's own footprint,
8712
+ exactly as the reference `sh` package exempts `EVIDENCIA.md` and the logs it hashes);
8713
+ 2. is `HEAD` the commit that snapshot was taken at (`origin.commit`, ADR-007);
8714
+ 3. does every report the snapshot names still exist and still hash to what was
8715
+ recorded at ingest (`sha256`, added in 1.92.0).
8716
+
8717
+ Three verdicts, never two: `True` sealed, `False` a MEASURED break (the reasons say
8718
+ which), `None` UNMEASURED — no git work tree, or a snapshot old enough to predate the
8719
+ content hash. A measured break outranks an unmeasured check (fail-closed); an unmeasured
8720
+ check never reads as a pass (INV-TOP-05). The repo is the FIRST configured one, the same
8721
+ choice `_top_spec_pin` and `_top_repos` make, and it is named in the `repo` member so
8722
+ every reason below is read against it.
8723
+
8724
+ The block carries NO timestamp of its own. It is recomputed on every read, so a
8725
+ "checked at" would be a second wall clock inside a payload whose only other one is
8726
+ `generated_at` -- and two consecutive `top --json` runs must differ in nothing else
8727
+ (AC-T-24 measures exactly that, and caught this before it shipped)."""
8728
+ out = {"ok": None, "reasons": [], "commit": None, "repo": None}
8729
+ repos = (ledger.get("config", {}) or {}).get("repos") or []
8730
+ if not repos:
8731
+ out["reasons"].append("no repo configured — seal UNMEASURED")
8732
+ return out
8733
+ name = repos[0].get("name")
8734
+ path = repos[0].get("path", ".")
8735
+ out["repo"] = _top_clean(name) if name else None
8736
+
8737
+ top = _seal_git(path, "rev-parse", "--show-toplevel")
8738
+ if top is None or not top.stdout.strip():
8739
+ out["reasons"].append(SEAL_NO_GIT)
8740
+ return out
8741
+ head = _seal_git(path, "rev-parse", "HEAD")
8742
+ if head is None or not head.stdout.strip():
8743
+ # a git tree with no commit yet: `rev-parse HEAD` fails on an unborn branch. It is a
8744
+ # DIFFERENT absence from "not a work tree" and the reason says so -- the verdict is
8745
+ # the same UNMEASURED, but a reason that misnames the cause sends the reader to the
8746
+ # wrong fix.
8747
+ out["reasons"].append(SEAL_NO_COMMIT)
8748
+ return out
8749
+ head_sha = head.stdout.strip()
8750
+ work_tree = top.stdout.strip()
8751
+ out["commit"] = head_sha
8752
+
8753
+ snaps = ((ledger.get("repos") or {}).get(name) or {}).get("snapshots") or []
8754
+ if not snaps:
8755
+ # UNMEASURED, not broken. The reference `sh` package calls a missing EVIDENCIA.md a
8756
+ # rejection, and that is right for a file whose only job is to be the seal -- but a
8757
+ # snapshot is the INGEST record, and with none recorded there is nothing to compare
8758
+ # the tree against: not "the evidence is stale", not "the evidence was altered",
8759
+ # simply no anchor. Calling that a break would also make the seal non-deterministic
8760
+ # for a board whose evidence is read live from reports (the `top` fixtures are
8761
+ # exactly that), and INV-TOP-05 already fixes the posture: absence renders as
8762
+ # absence. The teeth stay where they bite -- `check-terminado` exits 2, so a hook or
8763
+ # a human gating on exit 0 still refuses. The LIMIT this leaves is stated out loud in
8764
+ # SPEC §4: a board at 100% with no snapshot at all carries no seal marker.
8765
+ out["reasons"].append("no snapshot recorded yet")
8766
+ return out
8767
+ snap = snaps[-1]
8768
+
8769
+ failures, unmeasured = [], []
8770
+ snap_commit = (snap.get("origin") or {}).get("commit")
8771
+ if not snap_commit:
8772
+ unmeasured.append("snapshot recorded no commit — seal UNMEASURED")
8773
+ elif snap_commit != head_sha:
8774
+ failures.append("stale seal: snapshot at %s, HEAD is %s"
8775
+ % (snap_commit[:8], head_sha[:8]))
8776
+
8777
+ reports = [r for r in ((snap.get("tests") or {}).get("reports") or [])
8778
+ if isinstance(r, dict) and r.get("path")]
8779
+ exempt = set()
8780
+ for candidate in [ledger_path] + [os.path.join(path, r["path"]) for r in reports]:
8781
+ rel = _seal_rel(work_tree, candidate)
8782
+ if rel:
8783
+ exempt.add(rel)
8784
+ # `core.quotepath=false`: without it git C-quotes any non-ASCII path, so a report named
8785
+ # `junit-acción.xml` would appear in the reason as `junit-acción.xml` -- a reason
8786
+ # nobody can act on, and an exemption that cannot match. Set on the command, never in the
8787
+ # user's config: the engine reads git, it does not configure it.
8788
+ st = _seal_git(path, "-c", "core.quotepath=false",
8789
+ "status", "--porcelain", "-uall", "--", ".")
8790
+ if st is None:
8791
+ unmeasured.append("repo subtree state unreadable — seal UNMEASURED")
8792
+ else:
8793
+ dirty = sorted(set(_porcelain_paths(st.stdout)) - exempt)
8794
+ if dirty:
8795
+ failures.append("repo subtree dirty: changes no snapshot covers (%s)" % dirty[0])
8796
+
8797
+ for r in reports:
8798
+ rel, full = r["path"], os.path.join(path, r["path"])
8799
+ if not os.path.isfile(full):
8800
+ failures.append("evidence missing: %s" % rel)
8801
+ elif not r.get("sha256"):
8802
+ unmeasured.append("evidence hash unmeasured: %s — no hash recorded at ingest "
8803
+ "(older snapshot, or the file was unreadable)" % rel)
8804
+ elif _sha256_file(full) != r["sha256"]:
8805
+ failures.append("evidence altered after ingest: %s" % rel)
8806
+
8807
+ out["reasons"] = failures + unmeasured
8808
+ out["ok"] = False if failures else (None if unmeasured else True)
8809
+ return out
8810
+
8811
+
8812
+ def cmd_check_terminado(args):
8813
+ """The enforcement side of INV-T1: the SAME `_sealed_state` derivation `top --json`
8814
+ publishes, with an exit code a hook or a human can act on. It measures the tree and
8815
+ prints; it writes nothing and it decides nothing else.
8816
+
8817
+ 0 = sealed · 1 = a measured break · 2 = UNMEASURED (no git work tree, no configured
8818
+ repo, a snapshot with no recorded hash -- or no readable ledger at all). 2 is the
8819
+ reference script's error class: "I could not answer" is not "yes".
8820
+
8821
+ A missing or corrupt ledger is UNMEASURED, not a break: `_load` exits 1 by design, and 1
8822
+ here means "I checked and the seal is broken". Reporting "not sealed" for a file the
8823
+ command never managed to read would be a verdict on evidence nobody looked at -- the
8824
+ exact failure this command exists to catch."""
8825
+ try:
8826
+ ledger = _load(args.ledger)
8827
+ except SystemExit as exc:
8828
+ message = exc.code if isinstance(exc.code, str) else None
8829
+ print(message or "[qa_ledger] check-terminado: ledger '%s' unreadable" % args.ledger)
8830
+ print("[qa_ledger] check-terminado: UNMEASURED — no readable ledger, no seal.")
8831
+ sys.exit(2)
8832
+ sealed = _sealed_state(ledger, args.ledger)
8833
+ ok = sealed.get("ok")
8834
+ if getattr(args, "json", False):
8835
+ print(json.dumps(sealed, indent=2, ensure_ascii=False))
8836
+ else:
8837
+ verdict = "SEALED" if ok is True else ("UNSEALED" if ok is False else "UNMEASURED")
8838
+ where = " at %s" % sealed["commit"][:8] if sealed.get("commit") else ""
8839
+ print("[qa_ledger] check-terminado: %s%s (repo %s)"
8840
+ % (verdict, where, sealed.get("repo") or "?"))
8841
+ for reason in sealed.get("reasons") or []:
8842
+ print(" - %s" % reason)
8843
+ if ok is not True:
8844
+ print(" TERMINADO is not enabled: re-run the evidence and `snapshot` "
8845
+ "on the current state.")
8846
+ sys.exit(0 if ok is True else (1 if ok is False else 2))
8847
+
8848
+
8642
8849
  def cmd_top(args):
8643
8850
  """`uscha top` — the WHOLE projection of the ledger as one read-only JSON (ADR-032).
8644
8851
 
@@ -8744,6 +8951,15 @@ def cmd_top(args):
8744
8951
  done, fail, quar = _n("MEASURED_PASS"), _n("MEASURED_FAIL"), _n("QUARANTINE")
8745
8952
  unmeasured = _n("UNMEASURED") + _n("TRACED")
8746
8953
  pct = _top_pct(done, total)
8954
+ # INV-TOP-06 (ADR-038): DONE never publishes 100% while the seal is MEASURED broken --
8955
+ # every criterion green against evidence that no longer belongs to this code state is
8956
+ # the same lie INV-TOP-01 forbids one row earlier. The cap lives HERE, beside the
8957
+ # rounding cap, so no renderer is the place it happens. An UNMEASURED seal (`ok is
8958
+ # None` -- no git) does NOT cap: absence of measurement is not evidence of a break, and
8959
+ # capping on it would put an unearned 99 on every non-git tree.
8960
+ sealed = _sealed_state(ledger, args.ledger)
8961
+ if sealed.get("ok") is False and pct >= 100:
8962
+ pct = 99
8747
8963
  measured = done + fail
8748
8964
  out = {
8749
8965
  "schema": TOP_SCHEMA,
@@ -8760,7 +8976,10 @@ def cmd_top(args):
8760
8976
  "events_tail": _top_events(ledger),
8761
8977
  "counts": {"measured_pass": done, "measured_fail": fail, "quarantine": quar,
8762
8978
  "unmeasured": _n("UNMEASURED"), "traced": 0, "tagged": 0, "total": total},
8763
- "terminado": {"done": done, "total": total, "pct": pct, "unmeasured": unmeasured},
8979
+ # `sealed` is DERIVED at read time from the ledger plus the tree (ADR-038); it is
8980
+ # never stored, so it cannot go stale the way the claim it guards can.
8981
+ "terminado": {"done": done, "total": total, "pct": pct, "unmeasured": unmeasured,
8982
+ "sealed": sealed},
8764
8983
  "debtors": {"machine": fail, "you": quar, "untagged": unmeasured},
8765
8984
  "honesty": {"measured": measured, "total": total,
8766
8985
  "pct": _top_pct(measured, total)},
@@ -11952,6 +12171,14 @@ def build_parser():
11952
12171
  ptop.add_argument("--json", action="store_true")
11953
12172
  ptop.set_defaults(func=cmd_top)
11954
12173
 
12174
+ pct = sub.add_parser("check-terminado",
12175
+ help="INV-T1 (ADR-038): is TERMINADO sealed to the code state on "
12176
+ "disk? Same derivation `top --json` publishes. Exit 0 sealed, "
12177
+ "1 broken, 2 UNMEASURED")
12178
+ add_ledger(pct)
12179
+ pct.add_argument("--json", action="store_true")
12180
+ pct.set_defaults(func=cmd_check_terminado)
12181
+
11955
12182
  pb = sub.add_parser("rebuild",
11956
12183
  help="rebuild test: is the SPEC complete enough to "
11957
12184
  "regenerate the system? (completeness, not correctness)")
@@ -131,6 +131,17 @@ ACTIONS = {
131
131
  "TAGGED": "machine: run the case",
132
132
  }
133
133
 
134
+ # INV-TOP-06 (ADR-038): what a row that is green ON PAPER is actually waiting on when the
135
+ # seal is broken. Presentation, like ACTIONS above: the engine says WHAT broke (the reason),
136
+ # this says what the reader does about it, and the mapping is by reason prefix so a new
137
+ # reason class degrades to the generic line instead of to silence.
138
+ SEAL_ACTIONS = (
139
+ ("stale seal", "seal: snapshot at HEAD"),
140
+ ("repo subtree dirty", "seal: commit or discard, then snapshot"),
141
+ ("evidence altered", "seal: re-run the suite, then snapshot"),
142
+ ("evidence missing", "seal: re-run the suite, then snapshot"),
143
+ )
144
+
134
145
 
135
146
  # --------------------------------------------------------------------------- #
136
147
  # pure rendering #
@@ -216,7 +227,15 @@ def _pct_line(terminado):
216
227
  """INV-TOP-01: the DONE bar carries an explicit `N unmeasured` suffix whenever anything
217
228
  is unmeasured, and the engine has already capped the percentage below 100 while any
218
229
  obligation sits outside MEASURED_PASS -- the renderer republishes that fact, it never
219
- recomputes it (AC-T-01, AC-T-04, AC-T-23)."""
230
+ recomputes it (AC-T-01, AC-T-04, AC-T-23).
231
+
232
+ INV-TOP-06 (ADR-038) rides on the same line: when the engine's seal is MEASURED broken
233
+ (`terminado.sealed.ok is False`) the bar says so and names the first reason -- and the
234
+ percentage beside it is already capped below 100, in the engine, for the same reason the
235
+ unmeasured cap is (single derivation, AC-T-24). An UNMEASURED seal (`ok is null`: no git
236
+ work tree, the state of every frozen fixture) adds NOTHING here: the seal is shown only
237
+ when it is measured, and decorating a header with the absence of a measurement would
238
+ turn INV-TOP-05's `—` into noise on every board."""
220
239
  done = terminado.get("done")
221
240
  total = terminado.get("total")
222
241
  pct = terminado.get("pct")
@@ -224,9 +243,37 @@ def _pct_line(terminado):
224
243
  line = "DONE %s/%s (%s%%)" % (_num(done), _num(total), _num(pct))
225
244
  if unm:
226
245
  line += " %s %d unmeasured" % (MID, unm)
246
+ # the state is a FILE a human can hand us (`--state`), so `sealed` is guarded by TYPE and
247
+ # not merely by truthiness: a string there would answer `.get` with an AttributeError, and a
248
+ # `reasons` that is a string is iterable -- the frame would name its first CHARACTER as the
249
+ # reason. Same guards `_top_spec_diff` applies on the engine side, for the same reason.
250
+ seal = terminado.get("sealed")
251
+ seal = seal if isinstance(seal, dict) else {}
252
+ if seal.get("ok") is False:
253
+ raw = seal.get("reasons")
254
+ reasons = [r for r in raw if isinstance(r, str) and r] if isinstance(raw, list) else []
255
+ line += " %s unsealed (%s)" % (MID, _safe(reasons[0]) if reasons
256
+ else "no reason recorded")
227
257
  return line
228
258
 
229
259
 
260
+ def _seal_action(sealed):
261
+ """The ACTION cell of a row that is green on paper while the seal is broken. Empty
262
+ whenever the seal is not MEASURED broken -- an unmeasured seal changes no row, and a
263
+ `sealed` of the wrong TYPE reads as no seal at all rather than raising mid-frame."""
264
+ seal = sealed if isinstance(sealed, dict) else {}
265
+ if seal.get("ok") is not False:
266
+ return ""
267
+ raw = seal.get("reasons")
268
+ for reason in (raw if isinstance(raw, list) else []):
269
+ if not isinstance(reason, str):
270
+ continue
271
+ for prefix, action in SEAL_ACTIONS:
272
+ if reason.startswith(prefix):
273
+ return action
274
+ return "seal: re-snapshot the current state"
275
+
276
+
230
277
  def _burnup_line(burnup, cols):
231
278
  """The score trend, labelled as a score trend. v0.1 has no obligation-count history
232
279
  (ADR-035/2), so calling this a burn-up of closed obligations would be a lie the label
@@ -264,15 +311,21 @@ def _cases_text(ob):
264
311
  return "%s/%s" % (_num(ob.get("cases_pass")), total)
265
312
 
266
313
 
267
- def _row(ob, selected):
314
+ def _row(ob, selected, seal_action=""):
268
315
  # the three left columns are cut and padded in COLUMNS: an id or state carrying wide
269
316
  # characters used to eat its neighbour's field and walk every column after it.
270
317
  gutter = "> " if selected else " "
318
+ action = ACTIONS.get(ob.get("state"), DASH)
319
+ # INV-TOP-06: only the rows that CLAIM to be done change, and only while the seal is
320
+ # measured broken. A failing or unmeasured row already names its own debtor; telling it
321
+ # about the seal too would bury the thing it is actually waiting for.
322
+ if seal_action and ob.get("state") == "MEASURED_PASS":
323
+ action = seal_action
271
324
  return "%s%s%s%s%7s%5s %s" % (
272
325
  gutter, _pad(_cut(_safe(ob.get("id") or "?"), 8), 8),
273
326
  _pad(_cut(_safe(ob.get("gate") or DASH), 8), 9),
274
327
  _pad(_cut(_safe(ob.get("state") or "?"), 14), 15), _cases_text(ob),
275
- _num(ob.get("age_hours")), ACTIONS.get(ob.get("state"), DASH))
328
+ _num(ob.get("age_hours")), action)
276
329
 
277
330
 
278
331
  def _safe(text):
@@ -382,8 +435,9 @@ def _render_board(state, size, sel, plain, status=""):
382
435
  top = max(0, top)
383
436
 
384
437
  table = []
438
+ seal_action = _seal_action(terminado.get("sealed"))
385
439
  for i, ob in enumerate(obligations[top:top + body], start=top):
386
- line = _fit(_row(ob, i == sel), cols)
440
+ line = _fit(_row(ob, i == sel, seal_action), cols)
387
441
  table.append(line if plain else _colorize(line, ob.get("state")))
388
442
  hidden = len(obligations) - len(table)
389
443
  if hidden > 0:
@@ -0,0 +1,65 @@
1
+ # The Sceptic — claims audit before TERMINADO (optional, one call)
2
+
3
+ > This is a PORTABLE prompt, like the rubric grader's: instructions for ANY runner —
4
+ > Claude Code, Codex, Gemini CLI, Cursor, a `curl` to any API, or a human with the
5
+ > diff open. Nothing about it is vendor-specific.
6
+ >
7
+ > **It writes nothing and it gates nothing.** There is no ingest command for its output,
8
+ > no ledger field it fills and no exit code anyone checks: it produces a short markdown
9
+ > table a human reads before deciding. Unlike `check-terminado` (ADR-038), which is a
10
+ > mechanical recomputation over recorded evidence, this is a JUDGEMENT — and it is a
11
+ > **hypothesis until it has been used against real runs**. Treat its verdict as an
12
+ > opinion with citations, never as a measurement.
13
+
14
+ You are uscha's closing Sceptic. Your only job is to audit **claims**, not code. You treat
15
+ every claim of completeness as false until you have seen the evidence. You are not hunting
16
+ for new bugs and you are not reviewing the design: you are auditing the bookkeeping of a
17
+ delivery.
18
+
19
+ ## Inputs
20
+
21
+ 1. `CLAIMS`: the handoff, PR description, changelog, or whatever was declared about the
22
+ state of the work.
23
+ 2. The **evidence** the claims rest on: the ingested reports the ledger names (the paths in
24
+ the last snapshot's `tests.reports`), gate logs, test runs.
25
+ 3. `DIFF`: the diff of the delivery.
26
+
27
+ ## What to attack
28
+
29
+ 1. **Claims with no artifact**: every past-tense verb ("tested", "verified", "works on X")
30
+ requires a file among the evidence above that backs it.
31
+ 2. **Evidence that does not say what the claim says**: a log is attached, but skipped tests
32
+ are counted as passed, warnings are omitted, a partial run is presented as a full one.
33
+ 3. **Residue of incompleteness**: new TODO/FIXME/XXX in the diff, stubs, unticked
34
+ checkboxes, hardcoded values where the claim says "configurable".
35
+ 4. **Inflated scope**: "migrated all of X" — enumerate which parts of X the diff really
36
+ touches and which it does not.
37
+ 5. **Silences**: files in the diff no claim mentions; limits that were never declared.
38
+
39
+ ## Rules
40
+
41
+ - Do not punish honesty: a declared limit ("not tested on macOS") is NOT a finding; the
42
+ finding is the limit that was NOT declared.
43
+ - Every finding quotes the claim verbatim plus the absent or contradictory artifact (with
44
+ `file:line` where it applies). No exact citation, no finding.
45
+ - If you find nothing: your output MUST list, claim by claim, the evidence that backs it
46
+ (claim -> artifact -> verified). "All OK" without that table is an invalid output.
47
+
48
+ ## Output (markdown, short)
49
+
50
+ ```
51
+ ## Claims audit — <date>
52
+
53
+ | Claim (quoted) | Evidence | Status |
54
+ |---|---|---|
55
+ | "..." | reports/junit.xml | BACKED |
56
+ | "..." | (none) | UNBACKED |
57
+
58
+ ### Blocking findings
59
+ - <quoted claim>: <what is missing, or what contradicts it>
60
+
61
+ ### Verdict: BACKED / HAS GAPS
62
+ ```
63
+
64
+ HAS GAPS = at least one central claim has no backing. The decision to proceed anyway
65
+ belongs to the human, but it is now written down.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.91.0",
2
+ "version": "1.92.0",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,