@andresmassello/uscha 1.91.0 → 1.93.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.
@@ -131,6 +131,17 @@ ACTIONS = {
131
131
  "TAGGED": "machine: run the case",
132
132
  }
133
133
 
134
+ # INV-TOP-06 (ADR-038): what a row that is green ON PAPER is actually waiting on when the
135
+ # seal is broken. Presentation, like ACTIONS above: the engine says WHAT broke (the reason),
136
+ # this says what the reader does about it, and the mapping is by reason prefix so a new
137
+ # reason class degrades to the generic line instead of to silence.
138
+ SEAL_ACTIONS = (
139
+ ("stale seal", "seal: snapshot at HEAD"),
140
+ ("repo subtree dirty", "seal: commit or discard, then snapshot"),
141
+ ("evidence altered", "seal: re-run the suite, then snapshot"),
142
+ ("evidence missing", "seal: re-run the suite, then snapshot"),
143
+ )
144
+
134
145
 
135
146
  # --------------------------------------------------------------------------- #
136
147
  # pure rendering #
@@ -216,7 +227,15 @@ def _pct_line(terminado):
216
227
  """INV-TOP-01: the DONE bar carries an explicit `N unmeasured` suffix whenever anything
217
228
  is unmeasured, and the engine has already capped the percentage below 100 while any
218
229
  obligation sits outside MEASURED_PASS -- the renderer republishes that fact, it never
219
- recomputes it (AC-T-01, AC-T-04, AC-T-23)."""
230
+ recomputes it (AC-T-01, AC-T-04, AC-T-23).
231
+
232
+ INV-TOP-06 (ADR-038) rides on the same line: when the engine's seal is MEASURED broken
233
+ (`terminado.sealed.ok is False`) the bar says so and names the first reason -- and the
234
+ percentage beside it is already capped below 100, in the engine, for the same reason the
235
+ unmeasured cap is (single derivation, AC-T-24). An UNMEASURED seal (`ok is null`: no git
236
+ work tree, the state of every frozen fixture) adds NOTHING here: the seal is shown only
237
+ when it is measured, and decorating a header with the absence of a measurement would
238
+ turn INV-TOP-05's `—` into noise on every board."""
220
239
  done = terminado.get("done")
221
240
  total = terminado.get("total")
222
241
  pct = terminado.get("pct")
@@ -224,9 +243,37 @@ def _pct_line(terminado):
224
243
  line = "DONE %s/%s (%s%%)" % (_num(done), _num(total), _num(pct))
225
244
  if unm:
226
245
  line += " %s %d unmeasured" % (MID, unm)
246
+ # the state is a FILE a human can hand us (`--state`), so `sealed` is guarded by TYPE and
247
+ # not merely by truthiness: a string there would answer `.get` with an AttributeError, and a
248
+ # `reasons` that is a string is iterable -- the frame would name its first CHARACTER as the
249
+ # reason. Same guards `_top_spec_diff` applies on the engine side, for the same reason.
250
+ seal = terminado.get("sealed")
251
+ seal = seal if isinstance(seal, dict) else {}
252
+ if seal.get("ok") is False:
253
+ raw = seal.get("reasons")
254
+ reasons = [r for r in raw if isinstance(r, str) and r] if isinstance(raw, list) else []
255
+ line += " %s unsealed (%s)" % (MID, _safe(reasons[0]) if reasons
256
+ else "no reason recorded")
227
257
  return line
228
258
 
229
259
 
260
+ def _seal_action(sealed):
261
+ """The ACTION cell of a row that is green on paper while the seal is broken. Empty
262
+ whenever the seal is not MEASURED broken -- an unmeasured seal changes no row, and a
263
+ `sealed` of the wrong TYPE reads as no seal at all rather than raising mid-frame."""
264
+ seal = sealed if isinstance(sealed, dict) else {}
265
+ if seal.get("ok") is not False:
266
+ return ""
267
+ raw = seal.get("reasons")
268
+ for reason in (raw if isinstance(raw, list) else []):
269
+ if not isinstance(reason, str):
270
+ continue
271
+ for prefix, action in SEAL_ACTIONS:
272
+ if reason.startswith(prefix):
273
+ return action
274
+ return "seal: re-snapshot the current state"
275
+
276
+
230
277
  def _burnup_line(burnup, cols):
231
278
  """The score trend, labelled as a score trend. v0.1 has no obligation-count history
232
279
  (ADR-035/2), so calling this a burn-up of closed obligations would be a lie the label
@@ -264,15 +311,21 @@ def _cases_text(ob):
264
311
  return "%s/%s" % (_num(ob.get("cases_pass")), total)
265
312
 
266
313
 
267
- def _row(ob, selected):
314
+ def _row(ob, selected, seal_action=""):
268
315
  # the three left columns are cut and padded in COLUMNS: an id or state carrying wide
269
316
  # characters used to eat its neighbour's field and walk every column after it.
270
317
  gutter = "> " if selected else " "
318
+ action = ACTIONS.get(ob.get("state"), DASH)
319
+ # INV-TOP-06: only the rows that CLAIM to be done change, and only while the seal is
320
+ # measured broken. A failing or unmeasured row already names its own debtor; telling it
321
+ # about the seal too would bury the thing it is actually waiting for.
322
+ if seal_action and ob.get("state") == "MEASURED_PASS":
323
+ action = seal_action
271
324
  return "%s%s%s%s%7s%5s %s" % (
272
325
  gutter, _pad(_cut(_safe(ob.get("id") or "?"), 8), 8),
273
326
  _pad(_cut(_safe(ob.get("gate") or DASH), 8), 9),
274
327
  _pad(_cut(_safe(ob.get("state") or "?"), 14), 15), _cases_text(ob),
275
- _num(ob.get("age_hours")), ACTIONS.get(ob.get("state"), DASH))
328
+ _num(ob.get("age_hours")), action)
276
329
 
277
330
 
278
331
  def _safe(text):
@@ -382,8 +435,9 @@ def _render_board(state, size, sel, plain, status=""):
382
435
  top = max(0, top)
383
436
 
384
437
  table = []
438
+ seal_action = _seal_action(terminado.get("sealed"))
385
439
  for i, ob in enumerate(obligations[top:top + body], start=top):
386
- line = _fit(_row(ob, i == sel), cols)
440
+ line = _fit(_row(ob, i == sel, seal_action), cols)
387
441
  table.append(line if plain else _colorize(line, ob.get("state")))
388
442
  hidden = len(obligations) - len(table)
389
443
  if hidden > 0:
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "uscha",
4
- "version": "1.91.0",
4
+ "version": "1.93.0",
5
5
  "displayName": "Uscha",
6
- "description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 52 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
6
+ "description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 53 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
7
7
  "author": {
8
8
  "name": "Andres Massello",
9
9
  "url": "https://github.com/andresmassello"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "1.91.0",
3
+ "version": "1.93.0",
4
4
  "description": "Uscha spec-driven development methodology for coding agents. Includes npm/npx router.",
5
5
  "author": {
6
6
  "name": "Andres Massello",
@@ -1,6 +1,6 @@
1
1
  # uscha-kit
2
2
 
3
- **Kit version:** v1.91.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.93.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
4
4
 
5
5
  Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
6
6
  **Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
@@ -591,7 +591,7 @@ python3 $QL simplicity-check --diff changes.diff --json # consumed by usc
591
591
 
592
592
  ## Ledger subcommands
593
593
 
594
- `bench - bench-curate - bench-r2 - bench-roundtrip - bootstrap-oracle - bootstrap-variance - check-coverage - cleanroom - compile-ingest - compile-validate - converged - curate - curation-check - dashboard - discover - doctor - escalate - execution-policy - facts - fastpath-eval - fidelity - flag-blocker - gate-check - golden-coverage - golden-diff - ingest-gate - init - ir-extract - ir-render - lang-compare - log-gate - log-step - oscillation - phase - pit-check - production-finding - promote - readiness - rebuild - regression-check - resolve-escalation - roundtrip - rubric-ingest - simplicity-check - snapshot - spec-change-request - spec-check - spec-doubt - spec-drift - summary - top - waste-check` - the exact current `qa_ledger.py` parser surface (52 subcommands, derived from `SYSTEM-FACTS.json`, itself introspected from `build_parser()`); each supports `--help`.
594
+ `bench - bench-curate - bench-r2 - bench-roundtrip - bootstrap-oracle - bootstrap-variance - check-coverage - check-terminado - cleanroom - compile-ingest - compile-validate - converged - curate - curation-check - dashboard - discover - doctor - escalate - execution-policy - facts - fastpath-eval - fidelity - flag-blocker - gate-check - golden-coverage - golden-diff - ingest-gate - init - ir-extract - ir-render - lang-compare - log-gate - log-step - oscillation - phase - pit-check - production-finding - promote - readiness - rebuild - regression-check - resolve-escalation - roundtrip - rubric-ingest - simplicity-check - snapshot - spec-change-request - spec-check - spec-doubt - spec-drift - summary - top - waste-check` - the exact current `qa_ledger.py` parser surface (53 subcommands, derived from `SYSTEM-FACTS.json`, itself introspected from `build_parser()`); each supports `--help`.
595
595
 
596
596
  The **fact gates** (golden-diff, gate-check, pit-check, simplicity) are PERSISTED with
597
597
  `log-gate`: a fail blocks convergence and caps readiness ≤65 via the ledger. A CONSTITUTION
package/uscha-kit/VERSION CHANGED
@@ -1 +1 @@
1
- uscha-kit 1.91.0
1
+ uscha-kit 1.93.0
@@ -459,6 +459,20 @@ 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, no source-relevant change since the last
470
+ snapshot's commit (a non-source difference — docs, the ledger, the reports themselves —
471
+ seals with a `note` naming what moved; ADR-039), every ingested report still hashing to
472
+ what was recorded. **Exit 1** — do not declare TERMINADO:
473
+ re-snapshot on the CURRENT state (`snapshot --repo <REPO> --phase post`) and record why the
474
+ seal broke. **Exit 2** — the seal is UNMEASURED (no git, or no snapshot recorded): say so
475
+ plainly; an answer nobody could measure is not a TERMINADO either.
462
476
  - Ensure conventional-commit history is clean.
463
477
  - Open the PR(s). Confirm CI is green.
464
478
  - **STOP.** Present the PR link(s) and wait for the human to merge.
@@ -544,6 +558,12 @@ up as `narrated_only` and does NOT close (measured beats narrated, per criterion
544
558
  A JUnit report older than the repo's source code is treated as STALE (the code changed
545
559
  after the tests ran) and is DISCARDED — a criterion backed only by stale reports stays
546
560
  UNMEASURED, never falsely closed or vetoed (kit 1.31.0; surfaced as `stale_reports`).
561
+ Since 1.93.0 (ADR-039) that clock rule is not the only one: a report the clock rejects is still
562
+ FRESH when its `sha256` matches what the last `snapshot` recorded for it AND git shows no
563
+ source-relevant change since that snapshot's commit — so a clone, a `git worktree add`, a merge
564
+ or a CI checkout, which re-date every file without changing a byte, no longer un-measure green
565
+ evidence. Either rule suffices; with no git, no recorded commit or no recorded hash the clock
566
+ rule decides alone, exactly as before.
547
567
  So: when you write the tests for a criterion, put its AC-n in the test name; run
548
568
  `spec-check --acceptance ACCEPTANCE.md` up front (zero traceable criteria / duplicate
549
569
  IDs block as structural FACTS). Files without IDs fall back to the checkbox ratio