@andresmassello/uscha 1.90.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 +5 -5
- 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 +306 -2
- package/uscha-kit/.claude/skills/uscha-devloop/uscha_top.py +406 -11
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +2 -6
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/install-uscha.py +5 -0
- package/uscha-kit/skills/uscha-devloop/SKILL.md +12 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +306 -2
- package/uscha-kit/skills/uscha-devloop/uscha_top.py +406 -11
- package/uscha-kit/templates/esceptico-prompt.md +65 -0
- package/uscha-kit/uscha.config.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ the rest.* Uscha gives a coding agent a spec to build against, a QA loop that co
|
|
|
5
5
|
instead of looping forever, and a deterministic ledger that records what was **measured** —
|
|
6
6
|
never what was claimed.
|
|
7
7
|
|
|
8
|
-
> The
|
|
8
|
+
> The agent executes · the method governs · evidence decides · the human approves.
|
|
9
9
|
|
|
10
10
|
**[uscha.dev](https://uscha.dev)** — the method, the five rules, the skills, the library
|
|
11
11
|
(the diamond thesis, how-it-works diagrams, essay, 2-day dev course, reference, paper).
|
|
@@ -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,13 +85,13 @@ 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
|
|---|---|---|
|
|
92
92
|
| Asset → typed graph | `ir-extract`, `ir-render` | the whole package becomes one canonical IR (M2, ADR-015) — deterministic, `UNTYPED` is a measurement not an error |
|
|
93
93
|
| Forward, the compiler | `compile-validate`, `compile-ingest` | any model produces code; the engine validates the output contract and never compiles (M3, ADR-016) |
|
|
94
|
-
| Forward, is it the *same* system? | `bootstrap-oracle`, `bootstrap-variance`, `bench` | a withheld oracle judges blind compilations — **12 archetypes, 9 PASS · 3 PARTIAL**, three models, JS included (M4/M5, ADR-017/018/028/029) |
|
|
94
|
+
| Forward, is it the *same* system? | `bootstrap-oracle`, `bootstrap-variance`, `bench` | a withheld oracle judges blind compilations — **12 archetypes, 9 PASS · 3 PARTIAL**, three Claude-family models (Haiku · Sonnet · Opus — one vendor; cross-vendor not yet measured), JS included (M4/M5, ADR-017/018/028/029) |
|
|
95
95
|
| Reverse, facts | `discover`, `golden-diff` (+ the `/uscha-characterize` skill) | system map + mechanically captured golden; typed candidate observations with evidence class (M1, ADR-013) |
|
|
96
96
|
| Reverse, the human gate | `curate`, `promote`, `curation-check`, `bench-curate` | one verdict per candidate, append-only ledger verified against git; unjudged → `pr-ready` blocked naming it (ADR-009/010, INV-CURATION-01) |
|
|
97
97
|
| Fidelity, honestly | `fidelity`, `roundtrip`, `bench-roundtrip`, `bench-r2` | per-compiler fidelity vector, id-level round trip, recoverability **0.828**, and the **noise floor** under every variance claim (ADR-014/022/027/030) |
|
|
@@ -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"
|
|
@@ -8567,6 +8575,277 @@ def _top_events(ledger, limit=TOP_EVENTS_TAIL):
|
|
|
8567
8575
|
return events[:max(0, int(limit))]
|
|
8568
8576
|
|
|
8569
8577
|
|
|
8578
|
+
def _top_repos(ledger):
|
|
8579
|
+
"""The configured repos, name and configured path, in configuration order (phase 2).
|
|
8580
|
+
|
|
8581
|
+
Two things the TUI must not decide for itself now have a source: WHICH repo a rerun runs
|
|
8582
|
+
in and ingests for (ADR-037 picks the first configured one, exactly as `_top_spec_pin`
|
|
8583
|
+
picks the sha it labels), and WHICH repo the `d` pane names when it tells the reader how
|
|
8584
|
+
to produce the spec-drift run that is missing. `path` is the path as CONFIGURED --
|
|
8585
|
+
relative to the ledger, never resolved here: an absolute machine path in the contract is
|
|
8586
|
+
a frozen state nobody else can render (the golden frames are files in a repo)."""
|
|
8587
|
+
out = []
|
|
8588
|
+
for r in ((ledger.get("config", {}) or {}).get("repos") or []):
|
|
8589
|
+
if isinstance(r, dict) and r.get("name"):
|
|
8590
|
+
out.append({"name": _top_clean(r["name"]), "path": _top_clean(r.get("path", "."))})
|
|
8591
|
+
return out
|
|
8592
|
+
|
|
8593
|
+
|
|
8594
|
+
def _top_spec_diff(ledger):
|
|
8595
|
+
"""The advisory spec↔code drift the ledger ALREADY carries, projected for `d` (ADR-037).
|
|
8596
|
+
|
|
8597
|
+
It measures NOTHING: `spec-drift` (ADR-005) is the only command that walks git for this,
|
|
8598
|
+
and `top` is read-only by contract (ADR-032) -- so this reads `ledger["spec_drift"]`, the
|
|
8599
|
+
latest-state record that command leaves behind, and nothing else. **No recorded run ->
|
|
8600
|
+
`null`**, which the TUI renders as "no spec-drift run recorded", never as "no drift":
|
|
8601
|
+
"nobody measured" and "nothing is stale" are different statements and only one of them is
|
|
8602
|
+
ever free (INV-TOP-05).
|
|
8603
|
+
|
|
8604
|
+
Only `SPEC_STALE` rows travel: CLEAN/UNMAPPED/UNTRACKED/NO-CODE are the four ways a doc
|
|
8605
|
+
is NOT drifting, and `docs_total` keeps the denominator visible beside the count so the
|
|
8606
|
+
pane can say `3 of 21`. `code_ref` is ONE of the governed files that outran the doc --
|
|
8607
|
+
the record stores a capped, alphabetically sorted list and no per-file dates, so it is
|
|
8608
|
+
"a newer file", never "the newest one" (under-claim; `newer_files_total` carries the
|
|
8609
|
+
real cardinality)."""
|
|
8610
|
+
rec = ledger.get("spec_drift")
|
|
8611
|
+
if not isinstance(rec, dict):
|
|
8612
|
+
return None
|
|
8613
|
+
results = rec.get("results")
|
|
8614
|
+
results = results if isinstance(results, list) else []
|
|
8615
|
+
docs = [r for r in results if isinstance(r, dict)]
|
|
8616
|
+
stale = []
|
|
8617
|
+
for r in docs:
|
|
8618
|
+
if r.get("verdict") != "SPEC_STALE":
|
|
8619
|
+
continue
|
|
8620
|
+
# `newer_files` is a LIST in the record `spec-drift` writes, but the ledger is JSON on
|
|
8621
|
+
# disk: a hand edit can leave a string there, and a string is iterable -- the old
|
|
8622
|
+
# comprehension would have walked its characters and named `"n"` as the governed file
|
|
8623
|
+
# that outran the doc. A non-list is no evidence, so it yields no code_ref and 0.
|
|
8624
|
+
nf = r.get("newer_files")
|
|
8625
|
+
newer = [f for f in nf if isinstance(f, str)] if isinstance(nf, list) else []
|
|
8626
|
+
lag = r.get("lag_days_actual")
|
|
8627
|
+
total = r.get("newer_files_total")
|
|
8628
|
+
stale.append({
|
|
8629
|
+
"doc": _top_clean(r.get("file") or "?"),
|
|
8630
|
+
"lag_days": lag if isinstance(lag, (int, float)) else None,
|
|
8631
|
+
"code_ref": _top_clean(newer[0]) if newer else None,
|
|
8632
|
+
"newer_files_total": total if isinstance(total, int) else len(newer),
|
|
8633
|
+
"spec_committed_at": (_top_clean(r["spec_committed_at"])
|
|
8634
|
+
if r.get("spec_committed_at") else None),
|
|
8635
|
+
"newest_governed_at": (_top_clean(r["newest_governed_at"])
|
|
8636
|
+
if r.get("newest_governed_at") else None)})
|
|
8637
|
+
# worst lag first, the doc name as the tie-break: deterministic given the record, which
|
|
8638
|
+
# is what lets a golden frame be the oracle for this pane too (ADR-034).
|
|
8639
|
+
stale.sort(key=lambda s: (-(s["lag_days"] or 0), s["doc"]))
|
|
8640
|
+
lag_days = rec.get("max_lag_days")
|
|
8641
|
+
return {"measured_at": _top_clean(rec["at"]) if rec.get("at") else None,
|
|
8642
|
+
"repo": _top_clean(rec["repo"]) if rec.get("repo") else None,
|
|
8643
|
+
"max_lag_days": lag_days if isinstance(lag_days, int) else None,
|
|
8644
|
+
"docs_total": len(docs),
|
|
8645
|
+
"stale": stale,
|
|
8646
|
+
"advisory": True, # ADR-005: this never gates, here or anywhere
|
|
8647
|
+
"source": "spec-drift"}
|
|
8648
|
+
|
|
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
|
+
|
|
8570
8849
|
def cmd_top(args):
|
|
8571
8850
|
"""`uscha top` — the WHOLE projection of the ledger as one read-only JSON (ADR-032).
|
|
8572
8851
|
|
|
@@ -8672,6 +8951,15 @@ def cmd_top(args):
|
|
|
8672
8951
|
done, fail, quar = _n("MEASURED_PASS"), _n("MEASURED_FAIL"), _n("QUARANTINE")
|
|
8673
8952
|
unmeasured = _n("UNMEASURED") + _n("TRACED")
|
|
8674
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
|
|
8675
8963
|
measured = done + fail
|
|
8676
8964
|
out = {
|
|
8677
8965
|
"schema": TOP_SCHEMA,
|
|
@@ -8688,7 +8976,10 @@ def cmd_top(args):
|
|
|
8688
8976
|
"events_tail": _top_events(ledger),
|
|
8689
8977
|
"counts": {"measured_pass": done, "measured_fail": fail, "quarantine": quar,
|
|
8690
8978
|
"unmeasured": _n("UNMEASURED"), "traced": 0, "tagged": 0, "total": total},
|
|
8691
|
-
|
|
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},
|
|
8692
8983
|
"debtors": {"machine": fail, "you": quar, "untagged": unmeasured},
|
|
8693
8984
|
"honesty": {"measured": measured, "total": total,
|
|
8694
8985
|
"pct": _top_pct(measured, total)},
|
|
@@ -8699,6 +8990,11 @@ def cmd_top(args):
|
|
|
8699
8990
|
"medians": {"verdict_min": None, "loop_min": _top_loop_median_min(ledger)},
|
|
8700
8991
|
"checks": _top_checks(ledger),
|
|
8701
8992
|
"drift_pct": None, # spec_drift is per-file; an aggregate is ADR-035/3
|
|
8993
|
+
# phase 2 (ADR-037): the repos in configuration order -- the source for the ONE repo
|
|
8994
|
+
# `o` reruns in and `d` names -- and the advisory drift ALREADY recorded by
|
|
8995
|
+
# `spec-drift`. Both are reads of what the ledger holds; `top` still runs nothing.
|
|
8996
|
+
"repos": _top_repos(ledger),
|
|
8997
|
+
"spec_diff": _top_spec_diff(ledger),
|
|
8702
8998
|
# the ONLY real series is the readiness SCORE history; an obligation-count burn-up
|
|
8703
8999
|
# needs new persistence (ADR-035/2), so `kind` is emitted for the TUI to label it a
|
|
8704
9000
|
# score trend and never as a count of closed obligations.
|
|
@@ -11875,6 +12171,14 @@ def build_parser():
|
|
|
11875
12171
|
ptop.add_argument("--json", action="store_true")
|
|
11876
12172
|
ptop.set_defaults(func=cmd_top)
|
|
11877
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
|
+
|
|
11878
12182
|
pb = sub.add_parser("rebuild",
|
|
11879
12183
|
help="rebuild test: is the SPEC complete enough to "
|
|
11880
12184
|
"regenerate the system? (completeness, not correctness)")
|