@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 +3 -3
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +12 -0
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +229 -2
- package/uscha-kit/.claude/skills/uscha-devloop/uscha_top.py +58 -4
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +2 -2
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/skills/uscha-devloop/SKILL.md +12 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +229 -2
- package/uscha-kit/skills/uscha-devloop/uscha_top.py +58 -4
- package/uscha-kit/templates/esceptico-prompt.md +65 -0
- package/uscha-kit/uscha.config.json +1 -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.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.
|
|
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`,
|
|
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.
|
|
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
|
-
|
|
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")),
|
|
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.
|
|
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,
|
|
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"
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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")),
|
|
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.
|