@andresmassello/uscha 1.85.0 → 1.86.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 +69 -4
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +282 -3
- package/uscha-kit/.claude/skills/uscha-devloop/uscha_top.py +403 -0
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +1 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/install-uscha.py +58 -0
- package/uscha-kit/reports/junit/.top-cases.json +1 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +282 -3
- package/uscha-kit/skills/uscha-devloop/uscha_top.py +403 -0
- package/uscha-kit/uscha.config.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ never what was claimed.
|
|
|
8
8
|
> The tool 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
|
-
(essay, 2-day dev course, reference, paper).
|
|
11
|
+
(the diamond thesis, how-it-works diagrams, essay, 2-day dev course, reference, paper).
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
npx --yes @andresmassello/uscha@latest install --target claude # Claude Code
|
|
@@ -40,12 +40,77 @@ 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.86.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
|
|
|
47
47
|
---
|
|
48
48
|
|
|
49
|
+
## The diamond — specs are the source code, end to end
|
|
50
|
+
|
|
51
|
+
Uscha closes a cycle most spec-driven tools only walk halfway. The **spec package plus its
|
|
52
|
+
behavior ledger** is the canonical asset of a system; the code is a regenerable build artifact.
|
|
53
|
+
An LLM compiles the package into code under a validated contract; reverse discovery decompiles
|
|
54
|
+
existing code back into *curated* specs — passing, mandatorily, through the one step no
|
|
55
|
+
automatic tool can perform: a human verdict.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
THE ASSET (solid) ── it appreciates with every model generation
|
|
59
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
60
|
+
│ SPEC PACKAGE + BEHAVIOR LEDGER + IR │
|
|
61
|
+
│ SPEC · ADRs · ACCEPTANCE · CONSTITUTION │
|
|
62
|
+
│ verdicts: preserve · fix · undefined │
|
|
63
|
+
└──────────────┬─────────────────────────────▲──────────────────┘
|
|
64
|
+
│ │
|
|
65
|
+
FORWARD │ │ REVERSE
|
|
66
|
+
the LLM compiles │ │ reverse discovery
|
|
67
|
+
▼ │
|
|
68
|
+
┌─────────────────────────────────────┐ ┌─────────┴─────────────────────┐
|
|
69
|
+
│ compile-validate — output contract, │ │ CURATION · the human gate │
|
|
70
|
+
│ mechanical only, model-blind │ │ candidate ─▶ verdict ─▶ ledger │
|
|
71
|
+
│ withheld ORACLE — authored BEFORE │ │ no verdict → PR blocked, │
|
|
72
|
+
│ compiling, never in the prompt │ │ the candidate is NAMED │
|
|
73
|
+
└─────────────────┬───────────────────┘ └─────────▲─────────────────────┘
|
|
74
|
+
│ │
|
|
75
|
+
▼ │
|
|
76
|
+
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌─────────────┴─────────────────────┐
|
|
77
|
+
CODE (dashed) — build artifact, │ discover · golden capture │
|
|
78
|
+
regenerable, disposable ───▶│ candidates: typed evidence │
|
|
79
|
+
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ + confidence │
|
|
80
|
+
└───────────────────────────────────┘
|
|
81
|
+
▲ also enters here: any LEGACY
|
|
82
|
+
system (= 100% drift)
|
|
83
|
+
|
|
84
|
+
round trip · bench-roundtrip — how much of the asset the reverse organs re-anchor
|
|
85
|
+
from the compiled code: 0.062 measured (12 archetypes) — names, not yet semantics
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**What each arrow is, in the engine (kit 1.86.0, 52 subcommands, all measured):**
|
|
89
|
+
|
|
90
|
+
| Leg | Subcommands | What it establishes |
|
|
91
|
+
|---|---|---|
|
|
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
|
+
| 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) |
|
|
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
|
+
| 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
|
+
| Fidelity, honestly | `fidelity`, `roundtrip`, `bench-roundtrip`, `bench-r2` | per-compiler fidelity vector, id-level round trip, recoverability **0.062**, and the **noise floor** under every variance claim (ADR-014/022/027/030) |
|
|
98
|
+
|
|
99
|
+
**Read the numbers the way the repo does.** 9 of 12 archetypes regenerate to the same system
|
|
100
|
+
under an oracle the compilers never saw — that is the closed loop working. 0.062 is the mean
|
|
101
|
+
*recoverability* of the asset from compiled code counting only static and behavioural
|
|
102
|
+
footing, with the behaviour dimension still `UNMEASURED` — it says the reverse organs anchor
|
|
103
|
+
**names, not yet semantics**, and it is published rather than smoothed. And `bench-r2` measured
|
|
104
|
+
that same-model reruns differ structurally about as much as different models do (aggregate
|
|
105
|
+
`NOISY`) — so one earlier variance narrative was **retracted**. Every claim above is a subcommand
|
|
106
|
+
you can run; every unmeasured part is labeled. That honesty is the method applied to itself.
|
|
107
|
+
|
|
108
|
+
→ The full thesis, with before/after diagrams and the REAL vs VISION table:
|
|
109
|
+
**[uscha.dev/diamond](https://uscha.dev/diamond)** · the mechanism, in three diagrams:
|
|
110
|
+
**[uscha.dev/how](https://uscha.dev/how)**
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
49
114
|
## The problem it solves
|
|
50
115
|
|
|
51
116
|
An agent will tell you the tests pass. It will tell you the feature is done. It is often
|
|
@@ -68,7 +133,7 @@ and see which file, which test, and when.
|
|
|
68
133
|
|---|---|
|
|
69
134
|
| `/uscha-discovery` | Idea → spec package (CONTEXT, SPEC, ADRs, CONSTITUTION, ACCEPTANCE) |
|
|
70
135
|
| `/uscha-adr-refine` | Known feature → ADR + ACCEPTANCE, by interrogating you first |
|
|
71
|
-
| `/uscha-reverse-discovery` | Existing system →
|
|
136
|
+
| `/uscha-reverse-discovery` | Existing system → facts + typed candidates in quarantine; never promoted without a human verdict (brownfield) |
|
|
72
137
|
| `/uscha-characterize` | Capture a golden suite of current behavior before touching it |
|
|
73
138
|
| `/uscha-devloop` | Plan → build → severity-gated QA loop → PR (stops at the merge) |
|
|
74
139
|
| `/uscha-rubric` | Grade the non-testable (conventions, ergonomics) against a versioned rubric |
|
|
@@ -76,7 +141,7 @@ and see which file, which test, and when.
|
|
|
76
141
|
| `/uscha-mirador` | Bird's-eye HTML dashboard: readiness, trail, acceptance, loops |
|
|
77
142
|
| `/uscha-status` | One-line progress readout, in chat |
|
|
78
143
|
|
|
79
|
-
**A measurement engine** (`qa_ledger.py`,
|
|
144
|
+
**A measurement engine** (`qa_ledger.py`, 52 subcommands, Python stdlib) that ingests
|
|
80
145
|
evidence from **11 language stacks** — maven, gradle, ant, python, node, go, rust, dotnet,
|
|
81
146
|
cpp, swift, flutter — and computes a readiness score with hard caps and visible provenance.
|
|
82
147
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.86.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",
|
|
@@ -7616,6 +7616,14 @@ def _mirador_adrs(adr_dir):
|
|
|
7616
7616
|
return out
|
|
7617
7617
|
|
|
7618
7618
|
|
|
7619
|
+
def _project_name(cfg):
|
|
7620
|
+
"""Project label: DECLARED by the human in config (project/name); otherwise derived by
|
|
7621
|
+
joining the configured repo names; None when there is nothing to join. One derivation,
|
|
7622
|
+
shared by `dashboard` and `top` -- two readouts must never disagree about the name."""
|
|
7623
|
+
names = [r.get("name") for r in cfg.get("repos", []) if r.get("name")]
|
|
7624
|
+
return cfg.get("project") or cfg.get("name") or (" + ".join(names) if names else None)
|
|
7625
|
+
|
|
7626
|
+
|
|
7619
7627
|
def cmd_dashboard(args):
|
|
7620
7628
|
"""mirador — vista bird's-eye del estado. Agrega SOLO hechos que el ledger ya tiene
|
|
7621
7629
|
al contrato DATA del template. Read-only, determinista, cero narracion. Campos sin
|
|
@@ -7780,10 +7788,9 @@ def cmd_dashboard(args):
|
|
|
7780
7788
|
"reached": _reached_index(h.get("score"))}
|
|
7781
7789
|
for h in ledger.get("readiness_history", [])]
|
|
7782
7790
|
|
|
7783
|
-
names = [r.get("name") for r in cfg.get("repos", []) if r.get("name")]
|
|
7784
7791
|
# nombre de proyecto: lo declara el humano en config (project/name); si no,
|
|
7785
7792
|
# se deriva juntando los repos. Truth-pass: nombre puesto si existe, si no derivado.
|
|
7786
|
-
project =
|
|
7793
|
+
project = _project_name(cfg)
|
|
7787
7794
|
adrs = _mirador_adrs(getattr(args, "adr_dir", "docs/adr"))
|
|
7788
7795
|
|
|
7789
7796
|
# evidence (kit 1.50.0): RECEIPTS. The template shipped a click-a-milestone drawer
|
|
@@ -8003,6 +8010,260 @@ def cmd_dashboard(args):
|
|
|
8003
8010
|
f"{len(snapshots)} snapshot(s) en el time-lapse")
|
|
8004
8011
|
|
|
8005
8012
|
|
|
8013
|
+
TOP_SCHEMA = "uscha-top/v0.1"
|
|
8014
|
+
|
|
8015
|
+
# `uscha top` state ladder (ADR-032). TRACED and TAGGED are declared here and NEVER emitted
|
|
8016
|
+
# in v0.1: no general-project source exists for either (the only "does source name this AC"
|
|
8017
|
+
# scan is bench-wired, and JUnit has no "written but unexecuted" case). They keep their names
|
|
8018
|
+
# so the renderer can class them gray instead of inventing them into PASS (INV-TOP-02).
|
|
8019
|
+
TOP_STATES = ("UNMEASURED", "TRACED", "TAGGED", "MEASURED_PASS", "MEASURED_FAIL",
|
|
8020
|
+
"QUARANTINE")
|
|
8021
|
+
|
|
8022
|
+
|
|
8023
|
+
def _top_dt(iso):
|
|
8024
|
+
"""Tolerant ISO-8601 -> datetime, or None. Never raises: a malformed timestamp in one
|
|
8025
|
+
ledger record must degrade that record, not the read-only readout."""
|
|
8026
|
+
if not isinstance(iso, str) or not iso.strip():
|
|
8027
|
+
return None
|
|
8028
|
+
txt = iso.strip()
|
|
8029
|
+
if txt.endswith("Z"): # py3.8's fromisoformat does not take the Z suffix
|
|
8030
|
+
txt = txt[:-1] + "+00:00"
|
|
8031
|
+
try:
|
|
8032
|
+
return datetime.fromisoformat(txt)
|
|
8033
|
+
except ValueError:
|
|
8034
|
+
return None
|
|
8035
|
+
|
|
8036
|
+
|
|
8037
|
+
def _top_loop_median_min(ledger):
|
|
8038
|
+
"""medians.loop_min: the median gap in MINUTES between consecutive QA iterations, over
|
|
8039
|
+
the timestamps the ledger already carries (repos[r].iterations[*].at, grouped by
|
|
8040
|
+
iteration number). Fewer than two iterations -> None: an honest absence, never a zero
|
|
8041
|
+
(audit A/medians.loop_min)."""
|
|
8042
|
+
gaps = []
|
|
8043
|
+
for node in (ledger.get("repos") or {}).values():
|
|
8044
|
+
first = {}
|
|
8045
|
+
for s in node.get("iterations") or []:
|
|
8046
|
+
it, at = s.get("iteration"), _top_dt(s.get("at"))
|
|
8047
|
+
if it is None or at is None:
|
|
8048
|
+
continue
|
|
8049
|
+
if it not in first or at < first[it]:
|
|
8050
|
+
first[it] = at
|
|
8051
|
+
ordered = [first[k] for k in sorted(first)]
|
|
8052
|
+
for a, b in zip(ordered, ordered[1:]):
|
|
8053
|
+
gaps.append((b - a).total_seconds() / 60.0)
|
|
8054
|
+
if not gaps:
|
|
8055
|
+
return None
|
|
8056
|
+
gaps.sort()
|
|
8057
|
+
mid = len(gaps) // 2
|
|
8058
|
+
return int(round(gaps[mid] if len(gaps) % 2 else (gaps[mid - 1] + gaps[mid]) / 2.0))
|
|
8059
|
+
|
|
8060
|
+
|
|
8061
|
+
def _top_checks(ledger):
|
|
8062
|
+
"""checks{pass,fail,total} from the LATEST snapshot per repo -- the whole suite's run,
|
|
8063
|
+
NOT the AC-tagged subset (which travels per obligation as cases_pass/cases_total).
|
|
8064
|
+
None when no repo carries an ingested report: no evidence, no number."""
|
|
8065
|
+
got = False
|
|
8066
|
+
tot = {"pass": 0, "fail": 0, "total": 0}
|
|
8067
|
+
for node in (ledger.get("repos") or {}).values():
|
|
8068
|
+
snaps = node.get("snapshots") or []
|
|
8069
|
+
if not snaps:
|
|
8070
|
+
continue
|
|
8071
|
+
t = snaps[-1].get("tests") or {}
|
|
8072
|
+
if not t.get("report_found"):
|
|
8073
|
+
continue
|
|
8074
|
+
got = True
|
|
8075
|
+
tot["pass"] += t.get("passed") or 0
|
|
8076
|
+
tot["fail"] += (t.get("failures") or 0) + (t.get("errors") or 0)
|
|
8077
|
+
tot["total"] += t.get("executed") or 0
|
|
8078
|
+
return tot if got else None
|
|
8079
|
+
|
|
8080
|
+
|
|
8081
|
+
def _top_spec_pin(ledger):
|
|
8082
|
+
"""spec_pin (v0.1): git HEAD of the FIRST configured repo, labelled NOT clean-room
|
|
8083
|
+
verified unless a clean_room GREEN record exists at that exact sha. There is no pinned-
|
|
8084
|
+
spec concept in the engine yet (a designed pin is ADR-035); this is the honest interim
|
|
8085
|
+
proxy, and a non-git tree returns None so the TUI renders an em dash rather than a
|
|
8086
|
+
fabricated sha (INV-TOP-05)."""
|
|
8087
|
+
repos = (ledger.get("config", {}) or {}).get("repos") or []
|
|
8088
|
+
if not repos:
|
|
8089
|
+
return None
|
|
8090
|
+
name, path = repos[0].get("name"), repos[0].get("path", ".")
|
|
8091
|
+
sha = (_evidence_origin(path) or {}).get("commit")
|
|
8092
|
+
if not sha:
|
|
8093
|
+
return None
|
|
8094
|
+
cr = _cr_latest(ledger, name, sha) if name else None
|
|
8095
|
+
return {"sha": sha[:7],
|
|
8096
|
+
"clean_room_verified": bool(cr and cr.get("status") == "GREEN")}
|
|
8097
|
+
|
|
8098
|
+
|
|
8099
|
+
def _top_pct(done, total):
|
|
8100
|
+
"""A whole-number percentage with INV-TOP-01 enforced AT THE SOURCE: 999 of 1000 rounds
|
|
8101
|
+
to 100, and a board reading 100% while one obligation sits outside MEASURED_PASS is the
|
|
8102
|
+
exact lie the invariant forbids. Capped at 99 until the last one is really measured --
|
|
8103
|
+
in the engine, so no renderer can be the place the rounding happens. The same cap covers
|
|
8104
|
+
the honesty ratio: "100% measured" with one criterion unmeasured is the same lie."""
|
|
8105
|
+
if not total:
|
|
8106
|
+
return 0
|
|
8107
|
+
pct = int(round(done * 100.0 / total))
|
|
8108
|
+
return 99 if (done < total and pct >= 100) else pct
|
|
8109
|
+
|
|
8110
|
+
|
|
8111
|
+
def _top_ac_num(cid):
|
|
8112
|
+
"""Stable numeric order for the normalized 'AC-<n>' ids _parse_acceptance_items emits."""
|
|
8113
|
+
try:
|
|
8114
|
+
return int(str(cid).split("-")[1])
|
|
8115
|
+
except (IndexError, ValueError):
|
|
8116
|
+
return 0
|
|
8117
|
+
|
|
8118
|
+
|
|
8119
|
+
def cmd_top(args):
|
|
8120
|
+
"""`uscha top` — the WHOLE projection of the ledger as one read-only JSON (ADR-032).
|
|
8121
|
+
|
|
8122
|
+
Single derivation: every state, cardinality, median and percentage the TUI shows is
|
|
8123
|
+
computed HERE, from the same helpers readiness/dashboard use (_parse_acceptance_items,
|
|
8124
|
+
_ac_tags, _delta_state, _cr_latest, _evidence_origin). The renderer (uscha_top.py) is a
|
|
8125
|
+
pure function of this object and computes no KPI of its own (ADR-034, AC-T-24).
|
|
8126
|
+
|
|
8127
|
+
Read-only and truth-pass: it never writes, never runs tests, never calls a model, and a
|
|
8128
|
+
field with no honest source is null -- eta_min, medians.verdict_min, drift_pct, every
|
|
8129
|
+
age_hours, and every trace[] are null/empty in v0.1 BY DESIGN, each with its deferred
|
|
8130
|
+
wiring recorded in ADR-035. Under-claim, then wire, then re-claim."""
|
|
8131
|
+
ledger = _load(args.ledger)
|
|
8132
|
+
cfg = ledger.get("config", {}) or {}
|
|
8133
|
+
acc_path = args.acceptance or (cfg.get("defaults") or {}).get("acceptance_file")
|
|
8134
|
+
acc_items, _acc_found = _parse_acceptance_items(acc_path, args.section)
|
|
8135
|
+
|
|
8136
|
+
# measured evidence: the SAME helper readiness closes criteria with (_ac_tags), summed
|
|
8137
|
+
# across repos exactly as cmd_readiness sums it. Re-deriving it here would be the 1.48.1
|
|
8138
|
+
# mirador sin (two derivations of one number, free to disagree).
|
|
8139
|
+
ac_tags = {}
|
|
8140
|
+
for rcfg in cfg.get("repos", []):
|
|
8141
|
+
rtags, _stale = _ac_tags(rcfg.get("path", "."), rcfg.get("type", "maven"))
|
|
8142
|
+
for cid, v in rtags.items():
|
|
8143
|
+
d = ac_tags.setdefault(cid, {"green": 0, "red": 0})
|
|
8144
|
+
d["green"] += v.get("green", 0)
|
|
8145
|
+
d["red"] += v.get("red", 0)
|
|
8146
|
+
|
|
8147
|
+
# quarantine: UNCURATED observations whose statement literally names an AC id. The link
|
|
8148
|
+
# is `canonical_match`, a heuristic TEXT match (_match_canonical) -- not a designed
|
|
8149
|
+
# AC<->OBS field -- so an observation that matches nothing carries ac: null rather than
|
|
8150
|
+
# a guessed criterion (audit A/quarantine_obs).
|
|
8151
|
+
quarantine, observations = {}, []
|
|
8152
|
+
for rname in (ledger.get("repos") or {}):
|
|
8153
|
+
try:
|
|
8154
|
+
path = _scope_path(ledger, rname)
|
|
8155
|
+
dstate = _delta_state(ledger, rname, path)
|
|
8156
|
+
except SystemExit as exc:
|
|
8157
|
+
# a repo the scan cannot reach is NAMED on stderr, never dropped in silence: a
|
|
8158
|
+
# missing quarantine row would read as "nothing to curate here" (the one lie a
|
|
8159
|
+
# debtor column must never tell). stdout stays pure JSON.
|
|
8160
|
+
print("[qa_ledger] top: repo %s skipped for quarantine scan: %s"
|
|
8161
|
+
% (rname, exc or "unknown scope"), file=sys.stderr)
|
|
8162
|
+
continue
|
|
8163
|
+
if not dstate:
|
|
8164
|
+
continue
|
|
8165
|
+
delta, errors = _load_delta(path)
|
|
8166
|
+
if errors or not delta:
|
|
8167
|
+
continue # a malformed delta is named by the gates, not here
|
|
8168
|
+
uncurated = set(dstate.get("uncurated") or [])
|
|
8169
|
+
for o in delta.get("observations") or []:
|
|
8170
|
+
if o.get("id") not in uncurated:
|
|
8171
|
+
continue
|
|
8172
|
+
cid = o.get("canonical_match")
|
|
8173
|
+
if cid and cid not in quarantine:
|
|
8174
|
+
quarantine[cid] = o["id"]
|
|
8175
|
+
observations.append({
|
|
8176
|
+
"id": o.get("id"), "ac": cid,
|
|
8177
|
+
# no separate short label exists on an observation; `statement` is the only
|
|
8178
|
+
# prose field, so `title` is null and the TUI may head-truncate candidate[0]
|
|
8179
|
+
"title": None,
|
|
8180
|
+
"candidate": [o.get("statement")],
|
|
8181
|
+
"evidence": list((o.get("provenance") or {}).get("files") or []),
|
|
8182
|
+
"age_hours": None})
|
|
8183
|
+
observations.sort(key=lambda o: o.get("id") or "")
|
|
8184
|
+
|
|
8185
|
+
# obligations: one row per DISTINCT tagged criterion of the acceptance file. kind is
|
|
8186
|
+
# "AC" for all of them -- there is no per-INV ledger in the general path (the mirador's
|
|
8187
|
+
# INV list is a fixed hand-mapped set, audit A/obligations), so no INV row is invented.
|
|
8188
|
+
ids, seen = [], set()
|
|
8189
|
+
for it in acc_items:
|
|
8190
|
+
if it.get("id") and it["id"] not in seen:
|
|
8191
|
+
seen.add(it["id"])
|
|
8192
|
+
ids.append(it["id"])
|
|
8193
|
+
obligations = []
|
|
8194
|
+
for cid in sorted(ids, key=_top_ac_num):
|
|
8195
|
+
tag, obs = ac_tags.get(cid), quarantine.get(cid)
|
|
8196
|
+
# red evidence VETOES (fail-closed, the same rule _ac_closed applies). Measured
|
|
8197
|
+
# evidence outranks the lateral QUARANTINE rung so the four buckets partition the
|
|
8198
|
+
# board exactly once: done + machine + you + untagged == total.
|
|
8199
|
+
if tag and tag["red"] >= 1:
|
|
8200
|
+
state, gate = "MEASURED_FAIL", "junit"
|
|
8201
|
+
elif tag and tag["green"] >= 1:
|
|
8202
|
+
state, gate = "MEASURED_PASS", "junit"
|
|
8203
|
+
elif obs:
|
|
8204
|
+
state, gate = "QUARANTINE", "curation"
|
|
8205
|
+
else:
|
|
8206
|
+
state, gate = "UNMEASURED", "junit"
|
|
8207
|
+
obligations.append({
|
|
8208
|
+
"id": cid, "kind": "AC", "state": state,
|
|
8209
|
+
# never "oracle": for a general project the gate that closes a criterion is
|
|
8210
|
+
# JUnit-tag ingestion or curation. "oracle" is Diamond-bench vocabulary and
|
|
8211
|
+
# would mislead here (audit A/gate, ADR-032).
|
|
8212
|
+
"gate": gate,
|
|
8213
|
+
"cases_pass": (tag or {}).get("green", 0),
|
|
8214
|
+
"cases_total": (tag or {}).get("green", 0) + (tag or {}).get("red", 0),
|
|
8215
|
+
"trace": [], # no general AC->implementation map yet (ADR-035/5)
|
|
8216
|
+
"quarantine_obs": obs,
|
|
8217
|
+
"ac": None, # contract slot with no second honest meaning here
|
|
8218
|
+
"age_hours": None}) # no first-seen timestamp exists (ADR-035/1)
|
|
8219
|
+
|
|
8220
|
+
def _n(st):
|
|
8221
|
+
return sum(1 for o in obligations if o["state"] == st)
|
|
8222
|
+
|
|
8223
|
+
total = len(obligations)
|
|
8224
|
+
done, fail, quar = _n("MEASURED_PASS"), _n("MEASURED_FAIL"), _n("QUARANTINE")
|
|
8225
|
+
unmeasured = _n("UNMEASURED") + _n("TRACED")
|
|
8226
|
+
pct = _top_pct(done, total)
|
|
8227
|
+
measured = done + fail
|
|
8228
|
+
out = {
|
|
8229
|
+
"schema": TOP_SCHEMA,
|
|
8230
|
+
"project": _project_name(cfg),
|
|
8231
|
+
"spec_pin": _top_spec_pin(ledger),
|
|
8232
|
+
# the engine's GLOBAL step counter -- not a build number and not a QA-loop pass
|
|
8233
|
+
# count (that is _repo_loop_count); the TUI labels it `step #N` (audit A/run).
|
|
8234
|
+
"step": ledger.get("step_counter"),
|
|
8235
|
+
"generated_at": _now(),
|
|
8236
|
+
"obligations": obligations,
|
|
8237
|
+
"observations": observations,
|
|
8238
|
+
"events_tail": [], # the live feed is M2; the key ships empty, not absent
|
|
8239
|
+
"counts": {"measured_pass": done, "measured_fail": fail, "quarantine": quar,
|
|
8240
|
+
"unmeasured": _n("UNMEASURED"), "traced": 0, "tagged": 0, "total": total},
|
|
8241
|
+
"terminado": {"done": done, "total": total, "pct": pct, "unmeasured": unmeasured},
|
|
8242
|
+
"debtors": {"machine": fail, "you": quar, "untagged": unmeasured},
|
|
8243
|
+
"honesty": {"measured": measured, "total": total,
|
|
8244
|
+
"pct": _top_pct(measured, total)},
|
|
8245
|
+
# ETA = you x median_verdict + machine x median_loop. median_verdict is null in v0.1
|
|
8246
|
+
# (no per-OBS first-seen timestamp), so the product is null and the header reads
|
|
8247
|
+
# `ETA -`. A partial ETA computed from half the formula would be a fabrication.
|
|
8248
|
+
"eta_min": None,
|
|
8249
|
+
"medians": {"verdict_min": None, "loop_min": _top_loop_median_min(ledger)},
|
|
8250
|
+
"checks": _top_checks(ledger),
|
|
8251
|
+
"drift_pct": None, # spec_drift is per-file; an aggregate is ADR-035/3
|
|
8252
|
+
# the ONLY real series is the readiness SCORE history; an obligation-count burn-up
|
|
8253
|
+
# needs new persistence (ADR-035/2), so `kind` is emitted for the TUI to label it a
|
|
8254
|
+
# score trend and never as a count of closed obligations.
|
|
8255
|
+
"burnup": {"kind": "score",
|
|
8256
|
+
"weeks": [h.get("score") for h in ledger.get("readiness_history", [])
|
|
8257
|
+
if isinstance(h.get("score"), (int, float))]},
|
|
8258
|
+
}
|
|
8259
|
+
if getattr(args, "json", False):
|
|
8260
|
+
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
8261
|
+
return
|
|
8262
|
+
print("TOP %s: DONE %d/%d (%d%%) · %d unmeasured — `top --json` prints the full "
|
|
8263
|
+
"contract; `uscha top` renders it live"
|
|
8264
|
+
% (out["project"] or "?", done, total, pct, unmeasured))
|
|
8265
|
+
|
|
8266
|
+
|
|
8006
8267
|
def cmd_readiness(args):
|
|
8007
8268
|
ledger = _load(args.ledger)
|
|
8008
8269
|
defaults = ledger["config"].get("defaults", {})
|
|
@@ -8231,8 +8492,15 @@ def cmd_readiness(args):
|
|
|
8231
8492
|
# hacia adelante (no backfillea). El dashboard lo consume, nunca escribe.
|
|
8232
8493
|
if getattr(args, "record", False):
|
|
8233
8494
|
now = _now()
|
|
8495
|
+
# issue #1 (field note 001, limitation 1): persist the CAUSES per entry, not only the
|
|
8496
|
+
# score -- both facts are already computed above; nothing is re-derived. Additive keys:
|
|
8497
|
+
# every existing reader uses .get("at")/.get("score"). None/0 mean "no cap"/"none
|
|
8498
|
+
# discarded", stated rather than absent.
|
|
8234
8499
|
ledger.setdefault("readiness_history", []).append(
|
|
8235
|
-
{"at": now, "score": round(final, 1)
|
|
8500
|
+
{"at": now, "score": round(final, 1),
|
|
8501
|
+
"cap_applied": ({"reason": cap_reason, "key": cap_key} if cap_reason else None),
|
|
8502
|
+
"stale_discarded": {"count": len(stale_reports),
|
|
8503
|
+
"reports": list(stale_reports)[:8]}})
|
|
8236
8504
|
# kit 1.46.1: persist a compact MEASURED summary so the statusline (uscha_progress.py)
|
|
8237
8505
|
# shows MEASURED acceptance -- the same truth this readiness computed -- instead of
|
|
8238
8506
|
# counting checkboxes (narrated). Write-once/read-many: the fast Stop hook reads this
|
|
@@ -11153,6 +11421,17 @@ def build_parser():
|
|
|
11153
11421
|
pdash.add_argument("--json", action="store_true")
|
|
11154
11422
|
pdash.set_defaults(func=cmd_dashboard)
|
|
11155
11423
|
|
|
11424
|
+
ptop = sub.add_parser("top",
|
|
11425
|
+
help="uscha top: the whole projection of the ledger as one "
|
|
11426
|
+
"read-only JSON (obligations, debtors, medians) — the "
|
|
11427
|
+
"contract the terminal view renders (ADR-032)")
|
|
11428
|
+
add_ledger(ptop)
|
|
11429
|
+
ptop.add_argument("--acceptance", default=None,
|
|
11430
|
+
help="acceptance task list (markdown); overrides config default")
|
|
11431
|
+
ptop.add_argument("--section", default=None)
|
|
11432
|
+
ptop.add_argument("--json", action="store_true")
|
|
11433
|
+
ptop.set_defaults(func=cmd_top)
|
|
11434
|
+
|
|
11156
11435
|
pb = sub.add_parser("rebuild",
|
|
11157
11436
|
help="rebuild test: is the SPEC complete enough to "
|
|
11158
11437
|
"regenerate the system? (completeness, not correctness)")
|