@andresmassello/uscha 1.85.1 → 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 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.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.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.0, 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.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 = cfg.get("project") or cfg.get("name") or (" + ".join(names) if names else None)
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", {})
@@ -11160,6 +11421,17 @@ def build_parser():
11160
11421
  pdash.add_argument("--json", action="store_true")
11161
11422
  pdash.set_defaults(func=cmd_dashboard)
11162
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
+
11163
11435
  pb = sub.add_parser("rebuild",
11164
11436
  help="rebuild test: is the SPEC complete enough to "
11165
11437
  "regenerate the system? (completeness, not correctness)")