@andresmassello/uscha 1.85.1 → 1.86.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ Requires **Python 3.8+** on the machine (the engine is Python stdlib — no pip
40
40
  runtime dependencies). The npm package is a thin router; the canonical installer is
41
41
  `uscha-kit/install-uscha.py`.
42
42
 
43
- **Kit v1.85.1** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
43
+ **Kit v1.86.1** <!-- 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.062 measured (12 archetypes) — names, not yet semantics
86
86
  ```
87
87
 
88
- **What each arrow is, in the engine (kit 1.85.1, 51 subcommands, all measured):**
88
+ **What each arrow is, in the engine (kit 1.86.1, 52 subcommands, all measured):**
89
89
 
90
90
  | Leg | Subcommands | What it establishes |
91
91
  |---|---|---|
@@ -141,7 +141,7 @@ and see which file, which test, and when.
141
141
  | `/uscha-mirador` | Bird's-eye HTML dashboard: readiness, trail, acceptance, loops |
142
142
  | `/uscha-status` | One-line progress readout, in chat |
143
143
 
144
- **A measurement engine** (`qa_ledger.py`, 51 subcommands, Python stdlib) that ingests
144
+ **A measurement engine** (`qa_ledger.py`, 52 subcommands, Python stdlib) that ingests
145
145
  evidence from **11 language stacks** — maven, gradle, ant, python, node, go, rust, dotnet,
146
146
  cpp, swift, flutter — and computes a readiness score with hard caps and visible provenance.
147
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresmassello/uscha",
3
- "version": "1.85.1",
3
+ "version": "1.86.1",
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",
@@ -142,7 +142,7 @@ def _integrity_hash(data):
142
142
  return hashlib.sha256(blob.encode("utf-8")).hexdigest()
143
143
 
144
144
 
145
- def _load(path):
145
+ def _load(path, what="ledger", flag="--ledger"):
146
146
  """Carga blindada (kit 1.13.0, Topic 34: los recursos compartidos mutables
147
147
  incluyen ARCHIVOS). JSON corrupto/truncado = hecho bloqueante con mensaje
148
148
  de recuperacion, no un traceback. Si el archivo trae campo integrity
@@ -153,6 +153,12 @@ def _load(path):
153
153
  try:
154
154
  with open(path, "r", encoding="utf-8") as fh:
155
155
  data = json.load(fh)
156
+ except FileNotFoundError:
157
+ # what/flag name the file kind and the flag that points at it (1.86.1 re-judge: a
158
+ # hardcoded "ledger ... --ledger" misled the very first command of a fresh clone,
159
+ # init --config, and rebuild --baseline).
160
+ hint = " -- run the dev loop first, or pass --ledger" if what == "ledger" else f" -- pass {flag}"
161
+ raise SystemExit(f"[qa_ledger] {what} '{path}' not found here{hint}")
156
162
  except json.JSONDecodeError as exc:
157
163
  raise SystemExit(
158
164
  f"[qa_ledger] {path} corrupto (JSON invalido: {exc}). Es un artefacto "
@@ -820,6 +826,24 @@ def _ac_tags(repo_path, repo_type):
820
826
  return tags, stale
821
827
 
822
828
 
829
+ def _sum_ac_tags(ledger):
830
+ """One derivation of "how many green/red JUnit testcases does AC-n have, summed across
831
+ every configured repo" -- shared by cmd_readiness (which also keeps up to 8 example case
832
+ receipts per AC and the combined stale-report list) and cmd_top (which only needs
833
+ green/red). One place, so the two readouts of the same fact cannot silently disagree."""
834
+ ac_tags = {}
835
+ stale_reports = []
836
+ for rcfg in (ledger.get("config") or {}).get("repos", []):
837
+ rtags, rstale = _ac_tags(rcfg.get("path", "."), rcfg.get("type", "maven"))
838
+ for cid, v in rtags.items():
839
+ d = ac_tags.setdefault(cid, {"green": 0, "red": 0, "cases": []})
840
+ d["green"] += v["green"]
841
+ d["red"] += v["red"]
842
+ d["cases"] = (d["cases"] + v.get("cases", []))[:8]
843
+ stale_reports.extend(rstale)
844
+ return ac_tags, sorted(set(stale_reports))
845
+
846
+
823
847
  def junit_test_count(repo_path, extra_files=None):
824
848
  """JUnit-family XML: `pytest --junitxml=...` (python) and jest-junit /
825
849
  vitest --reporter=junit (node). Modern emitters WRAP the root:
@@ -1832,7 +1856,7 @@ def _validate_log_step_counts(args):
1832
1856
 
1833
1857
 
1834
1858
  def cmd_init(args):
1835
- cfg = _load(args.config)
1859
+ cfg = _load(args.config, what="config", flag="--config")
1836
1860
  _validate_init_config(cfg)
1837
1861
  defaults = cfg.get("defaults", {})
1838
1862
  ledger = {
@@ -6892,6 +6916,9 @@ def cmd_facts(args):
6892
6916
  except OSError as exc:
6893
6917
  problems.append((path, 0, "file", "unreadable: %s" % exc, ""))
6894
6918
  continue
6919
+ in_table = False
6920
+ table_names = []
6921
+ table_start = 0
6895
6922
  for n, line in enumerate(lines, 1):
6896
6923
  # an HTML comment is not a published claim -- the first live run flagged a
6897
6924
  # section marker (a comment reading "2 Skills") as a drifted count
@@ -6905,6 +6932,28 @@ def cmd_facts(args):
6905
6932
  actual = _fact_value(facts, key)
6906
6933
  if claimed != actual:
6907
6934
  problems.append((path, n, key, claimed, actual))
6935
+ # the parser-surface table (Subcommand/Subcomando header, one `<td class="t">`
6936
+ # row per subcommand) is a claim too, just not a numeric one -- a row can go
6937
+ # missing while the count beside it stays correct (the `top` row did, once).
6938
+ if not in_table:
6939
+ if re.search(r"<th>Sub ?comm?ando?s?</th>", line, re.I):
6940
+ in_table, table_names, table_start = True, [], n
6941
+ continue
6942
+ if "</table>" in line:
6943
+ in_table = False
6944
+ want = set(facts["subcommands"]["list"])
6945
+ got = set(table_names)
6946
+ for name in sorted(want - got):
6947
+ problems.append((path, table_start,
6948
+ "subcommand table: %s" % name,
6949
+ "absent", "present (engine subcommand list)"))
6950
+ for name in sorted(got - want):
6951
+ problems.append((path, table_start,
6952
+ "subcommand table: %s" % name,
6953
+ "present", "absent (not an engine subcommand)"))
6954
+ continue
6955
+ for m in re.finditer(r'<td class="t">([^<]+)</td>', line):
6956
+ table_names.append(m.group(1).strip())
6908
6957
  if problems:
6909
6958
  print("FACTUAL DRIFT: %d claim(s) disagree with the derived facts"
6910
6959
  % len(problems))
@@ -7616,6 +7665,14 @@ def _mirador_adrs(adr_dir):
7616
7665
  return out
7617
7666
 
7618
7667
 
7668
+ def _project_name(cfg):
7669
+ """Project label: DECLARED by the human in config (project/name); otherwise derived by
7670
+ joining the configured repo names; None when there is nothing to join. One derivation,
7671
+ shared by `dashboard` and `top` -- two readouts must never disagree about the name."""
7672
+ names = [r.get("name") for r in cfg.get("repos", []) if r.get("name")]
7673
+ return cfg.get("project") or cfg.get("name") or (" + ".join(names) if names else None)
7674
+
7675
+
7619
7676
  def cmd_dashboard(args):
7620
7677
  """mirador — vista bird's-eye del estado. Agrega SOLO hechos que el ledger ya tiene
7621
7678
  al contrato DATA del template. Read-only, determinista, cero narracion. Campos sin
@@ -7780,10 +7837,9 @@ def cmd_dashboard(args):
7780
7837
  "reached": _reached_index(h.get("score"))}
7781
7838
  for h in ledger.get("readiness_history", [])]
7782
7839
 
7783
- names = [r.get("name") for r in cfg.get("repos", []) if r.get("name")]
7784
7840
  # nombre de proyecto: lo declara el humano en config (project/name); si no,
7785
7841
  # se deriva juntando los repos. Truth-pass: nombre puesto si existe, si no derivado.
7786
- project = cfg.get("project") or cfg.get("name") or (" + ".join(names) if names else None)
7842
+ project = _project_name(cfg)
7787
7843
  adrs = _mirador_adrs(getattr(args, "adr_dir", "docs/adr"))
7788
7844
 
7789
7845
  # evidence (kit 1.50.0): RECEIPTS. The template shipped a click-a-milestone drawer
@@ -8003,6 +8059,257 @@ def cmd_dashboard(args):
8003
8059
  f"{len(snapshots)} snapshot(s) en el time-lapse")
8004
8060
 
8005
8061
 
8062
+ TOP_SCHEMA = "uscha-top/v0.1"
8063
+
8064
+ # `uscha top` state ladder (ADR-032). TRACED and TAGGED are declared here and NEVER emitted
8065
+ # in v0.1: no general-project source exists for either (the only "does source name this AC"
8066
+ # scan is bench-wired, and JUnit has no "written but unexecuted" case). They keep their names
8067
+ # so the renderer can class them gray instead of inventing them into PASS (INV-TOP-02).
8068
+ TOP_STATES = ("UNMEASURED", "TRACED", "TAGGED", "MEASURED_PASS", "MEASURED_FAIL",
8069
+ "QUARANTINE")
8070
+
8071
+
8072
+ def _top_dt(iso):
8073
+ """Tolerant ISO-8601 -> datetime, or None. Never raises: a malformed timestamp in one
8074
+ ledger record must degrade that record, not the read-only readout."""
8075
+ if not isinstance(iso, str) or not iso.strip():
8076
+ return None
8077
+ txt = iso.strip()
8078
+ if txt.endswith("Z"): # py3.8's fromisoformat does not take the Z suffix
8079
+ txt = txt[:-1] + "+00:00"
8080
+ try:
8081
+ return datetime.fromisoformat(txt)
8082
+ except ValueError:
8083
+ return None
8084
+
8085
+
8086
+ def _top_loop_median_min(ledger):
8087
+ """medians.loop_min: the median gap in MINUTES between consecutive QA iterations, over
8088
+ the timestamps the ledger already carries (repos[r].iterations[*].at, grouped by
8089
+ iteration number). Fewer than two iterations -> None: an honest absence, never a zero
8090
+ (audit A/medians.loop_min)."""
8091
+ gaps = []
8092
+ for node in (ledger.get("repos") or {}).values():
8093
+ first = {}
8094
+ for s in node.get("iterations") or []:
8095
+ it, at = s.get("iteration"), _top_dt(s.get("at"))
8096
+ if it is None or at is None:
8097
+ continue
8098
+ if it not in first or at < first[it]:
8099
+ first[it] = at
8100
+ ordered = [first[k] for k in sorted(first)]
8101
+ for a, b in zip(ordered, ordered[1:]):
8102
+ gaps.append((b - a).total_seconds() / 60.0)
8103
+ if not gaps:
8104
+ return None
8105
+ gaps.sort()
8106
+ mid = len(gaps) // 2
8107
+ return int(round(gaps[mid] if len(gaps) % 2 else (gaps[mid - 1] + gaps[mid]) / 2.0))
8108
+
8109
+
8110
+ def _top_checks(ledger):
8111
+ """checks{pass,fail,total} from the LATEST snapshot per repo -- the whole suite's run,
8112
+ NOT the AC-tagged subset (which travels per obligation as cases_pass/cases_total).
8113
+ None when no repo carries an ingested report: no evidence, no number."""
8114
+ got = False
8115
+ tot = {"pass": 0, "fail": 0, "total": 0}
8116
+ for node in (ledger.get("repos") or {}).values():
8117
+ snaps = node.get("snapshots") or []
8118
+ if not snaps:
8119
+ continue
8120
+ t = snaps[-1].get("tests") or {}
8121
+ if not t.get("report_found"):
8122
+ continue
8123
+ got = True
8124
+ tot["pass"] += t.get("passed") or 0
8125
+ tot["fail"] += (t.get("failures") or 0) + (t.get("errors") or 0)
8126
+ tot["total"] += t.get("executed") or 0
8127
+ return tot if got else None
8128
+
8129
+
8130
+ def _top_spec_pin(ledger):
8131
+ """spec_pin (v0.1): git HEAD of the FIRST configured repo, labelled NOT clean-room
8132
+ verified unless a clean_room GREEN record exists at that exact sha. There is no pinned-
8133
+ spec concept in the engine yet (a designed pin is ADR-035); this is the honest interim
8134
+ proxy, and a non-git tree returns None so the TUI renders an em dash rather than a
8135
+ fabricated sha (INV-TOP-05)."""
8136
+ repos = (ledger.get("config", {}) or {}).get("repos") or []
8137
+ if not repos:
8138
+ return None
8139
+ name, path = repos[0].get("name"), repos[0].get("path", ".")
8140
+ sha = (_evidence_origin(path) or {}).get("commit")
8141
+ if not sha:
8142
+ return None
8143
+ cr = _cr_latest(ledger, name, sha) if name else None
8144
+ return {"sha": sha[:7],
8145
+ "clean_room_verified": bool(cr and cr.get("status") == "GREEN")}
8146
+
8147
+
8148
+ def _top_pct(done, total):
8149
+ """A whole-number percentage with INV-TOP-01 enforced AT THE SOURCE: 999 of 1000 rounds
8150
+ to 100, and a board reading 100% while one obligation sits outside MEASURED_PASS is the
8151
+ exact lie the invariant forbids. Capped at 99 until the last one is really measured --
8152
+ in the engine, so no renderer can be the place the rounding happens. The same cap covers
8153
+ the honesty ratio: "100% measured" with one criterion unmeasured is the same lie."""
8154
+ if not total:
8155
+ return 0
8156
+ pct = int(round(done * 100.0 / total))
8157
+ return 99 if (done < total and pct >= 100) else pct
8158
+
8159
+
8160
+ def _top_ac_num(cid):
8161
+ """Stable numeric order for the normalized 'AC-<n>' ids _parse_acceptance_items emits."""
8162
+ try:
8163
+ return int(str(cid).split("-")[1])
8164
+ except (IndexError, ValueError):
8165
+ return 0
8166
+
8167
+
8168
+ def cmd_top(args):
8169
+ """`uscha top` — the WHOLE projection of the ledger as one read-only JSON (ADR-032).
8170
+
8171
+ Single derivation: every state, cardinality, median and percentage the TUI shows is
8172
+ computed HERE, from the same helpers readiness/dashboard use (_parse_acceptance_items,
8173
+ _ac_tags, _delta_state, _cr_latest, _evidence_origin). The renderer (uscha_top.py) is a
8174
+ pure function of this object and computes no KPI of its own (ADR-034, AC-T-24).
8175
+
8176
+ Read-only and truth-pass: it never writes, never runs tests, never calls a model, and a
8177
+ field with no honest source is null -- eta_min, medians.verdict_min, drift_pct, every
8178
+ age_hours, and every trace[] are null/empty in v0.1 BY DESIGN, each with its deferred
8179
+ wiring recorded in ADR-035. Under-claim, then wire, then re-claim."""
8180
+ ledger = _load(args.ledger)
8181
+ cfg = ledger.get("config", {}) or {}
8182
+ acc_path = args.acceptance or (cfg.get("defaults") or {}).get("acceptance_file")
8183
+ acc_items, _acc_found = _parse_acceptance_items(acc_path, args.section)
8184
+
8185
+ # measured evidence: the SAME helper readiness closes criteria with (_ac_tags via
8186
+ # _sum_ac_tags), summed across repos exactly as cmd_readiness sums it. Re-deriving it
8187
+ # here would be the 1.48.1 mirador sin (two derivations of one number, free to disagree).
8188
+ ac_tags, _stale = _sum_ac_tags(ledger)
8189
+
8190
+ # quarantine: UNCURATED observations whose statement literally names an AC id. The link
8191
+ # is `canonical_match`, a heuristic TEXT match (_match_canonical) -- not a designed
8192
+ # AC<->OBS field -- so an observation that matches nothing carries ac: null rather than
8193
+ # a guessed criterion (audit A/quarantine_obs).
8194
+ quarantine, observations = {}, []
8195
+ for rname in (ledger.get("repos") or {}):
8196
+ try:
8197
+ path = _scope_path(ledger, rname)
8198
+ dstate = _delta_state(ledger, rname, path)
8199
+ except SystemExit as exc:
8200
+ # a repo the scan cannot reach is NAMED on stderr, never dropped in silence: a
8201
+ # missing quarantine row would read as "nothing to curate here" (the one lie a
8202
+ # debtor column must never tell). stdout stays pure JSON.
8203
+ print("[qa_ledger] top: repo %s skipped for quarantine scan: %s"
8204
+ % (rname, exc or "unknown scope"), file=sys.stderr)
8205
+ continue
8206
+ if not dstate:
8207
+ continue
8208
+ delta, errors = _load_delta(path)
8209
+ if errors or not delta:
8210
+ continue # a malformed delta is named by the gates, not here
8211
+ uncurated = set(dstate.get("uncurated") or [])
8212
+ for o in delta.get("observations") or []:
8213
+ if o.get("id") not in uncurated:
8214
+ continue
8215
+ cid = o.get("canonical_match")
8216
+ if cid and cid not in quarantine:
8217
+ quarantine[cid] = o["id"]
8218
+ observations.append({
8219
+ "id": o.get("id"), "ac": cid,
8220
+ # no separate short label exists on an observation; `statement` is the only
8221
+ # prose field, so `title` is null and the TUI may head-truncate candidate[0]
8222
+ "title": None,
8223
+ "candidate": [o.get("statement")],
8224
+ "evidence": list((o.get("provenance") or {}).get("files") or []),
8225
+ "age_hours": None})
8226
+ observations.sort(key=lambda o: o.get("id") or "")
8227
+
8228
+ # obligations: one row per DISTINCT tagged criterion of the acceptance file. kind is
8229
+ # "AC" for all of them -- there is no per-INV ledger in the general path (the mirador's
8230
+ # INV list is a fixed hand-mapped set, audit A/obligations), so no INV row is invented.
8231
+ ids, seen = [], set()
8232
+ for it in acc_items:
8233
+ if it.get("id") and it["id"] not in seen:
8234
+ seen.add(it["id"])
8235
+ ids.append(it["id"])
8236
+ obligations = []
8237
+ for cid in sorted(ids, key=_top_ac_num):
8238
+ tag, obs = ac_tags.get(cid), quarantine.get(cid)
8239
+ # red evidence VETOES (fail-closed, the same rule _ac_closed applies). Measured
8240
+ # evidence outranks the lateral QUARANTINE rung so the four buckets partition the
8241
+ # board exactly once: done + machine + you + untagged == total.
8242
+ if tag and tag["red"] >= 1:
8243
+ state, gate = "MEASURED_FAIL", "junit"
8244
+ elif tag and tag["green"] >= 1:
8245
+ state, gate = "MEASURED_PASS", "junit"
8246
+ elif obs:
8247
+ state, gate = "QUARANTINE", "curation"
8248
+ else:
8249
+ state, gate = "UNMEASURED", "junit"
8250
+ obligations.append({
8251
+ "id": cid, "kind": "AC", "state": state,
8252
+ # never "oracle": for a general project the gate that closes a criterion is
8253
+ # JUnit-tag ingestion or curation. "oracle" is Diamond-bench vocabulary and
8254
+ # would mislead here (audit A/gate, ADR-032).
8255
+ "gate": gate,
8256
+ "cases_pass": (tag or {}).get("green", 0),
8257
+ "cases_total": (tag or {}).get("green", 0) + (tag or {}).get("red", 0),
8258
+ "trace": [], # no general AC->implementation map yet (ADR-035/5)
8259
+ # red/green evidence outranks the lateral QUARANTINE rung (the state ladder just
8260
+ # above), so an OBS that merely NAMES this AC must not travel into the JSON once
8261
+ # the criterion is already measured -- only a state of QUARANTINE ever carries it.
8262
+ "quarantine_obs": obs if state == "QUARANTINE" else None,
8263
+ "ac": None, # contract slot with no second honest meaning here
8264
+ "age_hours": None}) # no first-seen timestamp exists (ADR-035/1)
8265
+
8266
+ def _n(st):
8267
+ return sum(1 for o in obligations if o["state"] == st)
8268
+
8269
+ total = len(obligations)
8270
+ done, fail, quar = _n("MEASURED_PASS"), _n("MEASURED_FAIL"), _n("QUARANTINE")
8271
+ unmeasured = _n("UNMEASURED") + _n("TRACED")
8272
+ pct = _top_pct(done, total)
8273
+ measured = done + fail
8274
+ out = {
8275
+ "schema": TOP_SCHEMA,
8276
+ "project": _project_name(cfg),
8277
+ "spec_pin": _top_spec_pin(ledger),
8278
+ # the engine's GLOBAL step counter -- not a build number and not a QA-loop pass
8279
+ # count (that is _repo_loop_count); the TUI labels it `step #N` (audit A/run).
8280
+ "step": ledger.get("step_counter"),
8281
+ "generated_at": _now(),
8282
+ "obligations": obligations,
8283
+ "observations": observations,
8284
+ "events_tail": [], # the live feed is M2; the key ships empty, not absent
8285
+ "counts": {"measured_pass": done, "measured_fail": fail, "quarantine": quar,
8286
+ "unmeasured": _n("UNMEASURED"), "traced": 0, "tagged": 0, "total": total},
8287
+ "terminado": {"done": done, "total": total, "pct": pct, "unmeasured": unmeasured},
8288
+ "debtors": {"machine": fail, "you": quar, "untagged": unmeasured},
8289
+ "honesty": {"measured": measured, "total": total,
8290
+ "pct": _top_pct(measured, total)},
8291
+ # ETA = you x median_verdict + machine x median_loop. median_verdict is null in v0.1
8292
+ # (no per-OBS first-seen timestamp), so the product is null and the header reads
8293
+ # `ETA -`. A partial ETA computed from half the formula would be a fabrication.
8294
+ "eta_min": None,
8295
+ "medians": {"verdict_min": None, "loop_min": _top_loop_median_min(ledger)},
8296
+ "checks": _top_checks(ledger),
8297
+ "drift_pct": None, # spec_drift is per-file; an aggregate is ADR-035/3
8298
+ # the ONLY real series is the readiness SCORE history; an obligation-count burn-up
8299
+ # needs new persistence (ADR-035/2), so `kind` is emitted for the TUI to label it a
8300
+ # score trend and never as a count of closed obligations.
8301
+ "burnup": {"kind": "score",
8302
+ "weeks": [h.get("score") for h in ledger.get("readiness_history", [])
8303
+ if isinstance(h.get("score"), (int, float))]},
8304
+ }
8305
+ if getattr(args, "json", False):
8306
+ print(json.dumps(out, indent=2, ensure_ascii=False))
8307
+ return
8308
+ print("TOP %s: DONE %d/%d (%d%%) · %d unmeasured — `top --json` prints the full "
8309
+ "contract; `uscha top` renders it live"
8310
+ % (out["project"] or "?", done, total, pct, unmeasured))
8311
+
8312
+
8006
8313
  def cmd_readiness(args):
8007
8314
  ledger = _load(args.ledger)
8008
8315
  defaults = ledger["config"].get("defaults", {})
@@ -8030,18 +8337,7 @@ def cmd_readiness(args):
8030
8337
  # cierra solo con >=1 testcase verde taggeado en los reportes JUnit ya
8031
8338
  # ingeridos (y 0 rojos). El checkbox es RELATO; el testcase es HECHO.
8032
8339
  ac_ids = [i for i in acc_items if i["id"]]
8033
- ac_tags = {}
8034
- stale_reports = []
8035
- for rcfg in ledger["config"].get("repos", []):
8036
- rtags, rstale = _ac_tags(rcfg.get("path", "."),
8037
- rcfg.get("type", "maven"))
8038
- for cid, v in rtags.items():
8039
- d = ac_tags.setdefault(cid, {"green": 0, "red": 0, "cases": []})
8040
- d["green"] += v["green"]
8041
- d["red"] += v["red"]
8042
- d["cases"] = (d["cases"] + v.get("cases", []))[:8]
8043
- stale_reports.extend(rstale)
8044
- stale_reports = sorted(set(stale_reports))
8340
+ ac_tags, stale_reports = _sum_ac_tags(ledger)
8045
8341
 
8046
8342
  def _ac_closed(cid):
8047
8343
  d = ac_tags.get(cid)
@@ -8566,7 +8862,7 @@ def cmd_rebuild(args):
8566
8862
 
8567
8863
 
8568
8864
  def _rebuild_baseline(args):
8569
- cfg = _load(args.config)
8865
+ cfg = _load(args.config, what="config", flag="--config")
8570
8866
  defaults = cfg.get("defaults", {})
8571
8867
  acc_default = args.acceptance or defaults.get("acceptance_file")
8572
8868
  tol = (args.coverage_tolerance if args.coverage_tolerance is not None
@@ -8592,7 +8888,7 @@ def _rebuild_baseline(args):
8592
8888
 
8593
8889
 
8594
8890
  def _rebuild_compare(args):
8595
- base = _load(args.baseline)
8891
+ base = _load(args.baseline, what="baseline", flag="--baseline")
8596
8892
  tol = base.get("coverage_tolerance", DEFAULT_COVERAGE_TOLERANCE)
8597
8893
  acc_path = args.acceptance or base.get("acceptance_file")
8598
8894
  section = args.section if args.section is not None else base.get("section")
@@ -11160,6 +11456,17 @@ def build_parser():
11160
11456
  pdash.add_argument("--json", action="store_true")
11161
11457
  pdash.set_defaults(func=cmd_dashboard)
11162
11458
 
11459
+ ptop = sub.add_parser("top",
11460
+ help="uscha top: the whole projection of the ledger as one "
11461
+ "read-only JSON (obligations, debtors, medians) — the "
11462
+ "contract the terminal view renders (ADR-032)")
11463
+ add_ledger(ptop)
11464
+ ptop.add_argument("--acceptance", default=None,
11465
+ help="acceptance task list (markdown); overrides config default")
11466
+ ptop.add_argument("--section", default=None)
11467
+ ptop.add_argument("--json", action="store_true")
11468
+ ptop.set_defaults(func=cmd_top)
11469
+
11163
11470
  pb = sub.add_parser("rebuild",
11164
11471
  help="rebuild test: is the SPEC complete enough to "
11165
11472
  "regenerate the system? (completeness, not correctness)")