@andresmassello/uscha 1.56.0 → 1.59.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.
@@ -278,6 +278,11 @@ svg{display:block;width:100%;height:auto}
278
278
  The verdict above is all you need to look at. These roll up into it; they're here if you want to dig.
279
279
  </p>
280
280
  </div>
281
+
282
+ <div class="card col6" id="card-modes" hidden>
283
+ <h2>Fast-path &middot; Spec-drift <span class="n" id="modes-n">&mdash;</span></h2>
284
+ <div class="speclist" id="modes"></div>
285
+ </div>
281
286
  </section>
282
287
 
283
288
  <section class="telemetry" id="telemetry" hidden>
@@ -654,6 +659,39 @@ function renderTelemetry(){
654
659
  if(sec)sec.hidden=false;
655
660
  }
656
661
 
662
+ function renderModes(){
663
+ /* fast_path: latest measured verdict per repo (ADR-003). spec_drift: latest advisory
664
+ run (ADR-005) -- advisory means it never feeds readiness; this card only makes the
665
+ ledger fact visible. BOTH keys are conditional in dashboard --json, so a project
666
+ that never used either sees no card at all (absent block = identical view). */
667
+ const card=$("#card-modes");if(!card)return;
668
+ const fp=DATA.fast_path,sd=DATA.spec_drift;
669
+ if(!fp&&!sd){card.hidden=true;return;}
670
+ const host=$("#modes");host.replaceChildren();
671
+ const parts=[];
672
+ if(fp){
673
+ const cls={ALLOW:"st-done",ESCALATED:"st-prog",DENY:"st-block"};
674
+ Object.keys(fp).forEach(r=>{const e=fp[r]||{};
675
+ append(host,append(node("div","spec"),node("span","id","fast-path"),
676
+ node("span","t",r+(e.intent?(" — "+e.intent):"")),
677
+ node("span","chip "+(cls[e.verdict]||"st-todo"),String(e.verdict||"?"))));});
678
+ parts.push(Object.keys(fp).length+" repo"+(Object.keys(fp).length>1?"s":""));
679
+ }
680
+ if(sd){
681
+ const cls={SPEC_STALE:"st-block",CLEAN:"st-done",UNMAPPED:"st-todo",UNTRACKED:"st-todo"};
682
+ const res=sd.results||[];
683
+ res.forEach(d=>{const extra=d.verdict==="SPEC_STALE"
684
+ ?(" — "+(d.newer_files_total||0)+" file(s) newer than the spec"):"";
685
+ append(host,append(node("div","spec"),node("span","id","spec-drift"),
686
+ node("span","t",String(d.file||"")+extra),
687
+ node("span","chip "+(cls[d.verdict]||"st-todo"),String(d.verdict||"?"))));});
688
+ const stale=res.filter(d=>d.verdict==="SPEC_STALE").length;
689
+ parts.push(stale?stale+" stale spec"+(stale>1?"s":""):"no drift measured");
690
+ host.appendChild(emptyHint("spec-drift is ADVISORY (ADR-005): it prompts a human look, it never gates."));
691
+ }
692
+ $("#modes-n").textContent=parts.join(" · ");
693
+ card.hidden=false;
694
+ }
657
695
  /* ==== status story: cómo viene / qué lo traba / qué sigue — all from measured DATA ==== */
658
696
  function renderStatus(){
659
697
  const sec=$("#status");if(!sec)return;
@@ -741,7 +779,7 @@ function applySnapshot(i){
741
779
  scrub.addEventListener("input",()=>applySnapshot(+scrub.value));
742
780
 
743
781
  /* ==== init ==== */
744
- renderStatus();renderAcceptance();renderAdrs();renderInv();renderHeat();renderLoops();renderSubs();renderExecutionPolicy();renderTelemetry();
782
+ renderStatus();renderModes();renderAcceptance();renderAdrs();renderInv();renderHeat();renderLoops();renderSubs();renderExecutionPolicy();renderTelemetry();
745
783
  $("#asof").textContent=snaps.length?snaps[snaps.length-1].date:"no history yet (readiness --record)";
746
784
  renderTrail(null);
747
785
  </script>
@@ -86,6 +86,14 @@ Line guide:
86
86
  WHEN the evidence was captured — facts do not move without new evidence. If the
87
87
  field is absent (nothing recorded yet), omit the line; never guess a timestamp.
88
88
 
89
+ **Fast-path mode (ADR-003):** if the ledger carries `fast_path` entries, add ONE line to the
90
+ block with the latest verdict per repo (`fast-path: ALLOW (intent...)` / `ESCALATED`). Absent
91
+ entries → no line at all: silence is honest when no mode was requested.
92
+
93
+ **Spec-drift (ADR-005):** if the ledger carries a `spec_drift` run, add ONE line:
94
+ `spec-drift: N stale / M docs (advisory)` — or `spec-drift: no drift measured` when zero are
95
+ stale. Always label it advisory; it never explains a blocked phase. Absent key → no line.
96
+
89
97
  ## Degradation (honest, specific)
90
98
 
91
99
  - `measured` missing entirely → print: *"No measurement recorded yet — the trail
@@ -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.56.0",
4
+ "version": "1.59.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, 29 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, 31 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.56.0",
3
+ "version": "1.59.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.56.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.59.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`,
@@ -38,6 +38,59 @@ uscha-kit/
38
38
  └─ uscha-rubric/ # (optional) rubric grading — thin adapter; the core is agnostic
39
39
  ```
40
40
 
41
+ ## Fast-path (ADR-003) — measured entry for trivial changes
42
+
43
+ A one-line fix should not pay the ceremony of a schema migration. `fastpath-eval` grants the
44
+ shortcut from **measured signals**, never from anyone's opinion of "small":
45
+
46
+ ```bash
47
+ python qa_ledger.py fastpath-eval --repo <name> --json # dry-run: verdict only
48
+ python qa_ledger.py fastpath-eval --repo <name> --intent "fix: x" # records it in the ledger
49
+ ```
50
+
51
+ Config (`defaults.fast_path`; absent block = feature off, behavior identical to earlier
52
+ releases): `max_files_changed` (3) and `max_loc_delta` (80) measured from
53
+ `git diff --numstat` against the merge-base with `origin/main` (fallback `main`) **plus
54
+ untracked files** — a new file is still a change; `protected_paths` globs deny regardless of
55
+ size; `require_asserting_test` caps readiness while a fast-path run has no measured test.
56
+
57
+ **Fail-closed:** no git, no resolvable base, no config → `DENY` with the reason named.
58
+ "Could not measure" never grants the shortcut. **Escalation:** re-running `fastpath-eval`
59
+ mid-run (the devloop does, before the PR step) with thresholds now exceeded flips the run to
60
+ `ESCALATED` through the standard escalation machinery — the derived phase blocks `pr-ready`
61
+ and readiness is capped until a human runs `resolve-escalation`, after producing the ADR +
62
+ ACCEPTANCE the change turned out to deserve. **The override is asymmetric (INV-RIGOR-02):**
63
+ you can always force the full path; nothing can force `ALLOW` over a measured `DENY`.
64
+
65
+ ## Spec-drift (ADR-005) — the spec maintenance tax, made visible
66
+
67
+ Specs rot silently: the code moves and `SPEC.md` stays where it was. `spec-drift` detects
68
+ that lag **mechanically** — and reports it as **advisory, never a gate**, because whether an
69
+ older spec still covers newer code is a relevance judgment, and a guess advises.
70
+
71
+ ```bash
72
+ python qa_ledger.py spec-drift --repo <name> --json # advisory report; exit 0 always
73
+ ```
74
+
75
+ Map specs to code with a `governs:` glob list in the frontmatter of `SPEC.md` and each
76
+ `docs/adr/*.md`:
77
+
78
+ ```markdown
79
+ ---
80
+ governs:
81
+ - src/payments/**
82
+ - db/**
83
+ ---
84
+ ```
85
+
86
+ Per document: **`SPEC_STALE`** when governed code outran the spec by more than
87
+ `defaults.spec_drift.max_lag_days` (default 30), listing the newer files; **`CLEAN`** when it
88
+ did not; **`UNMAPPED`** when there is no `governs:` frontmatter *or its globs match nothing*
89
+ — absence of a mapping is absence of measurement, not "no drift"; **`UNTRACKED`** when the
90
+ spec has no commit date to compare. The latest run lands in the ledger (`spec_drift`) so the
91
+ mirador can surface it. No readiness impact, no exit-code gate: a stale spec is a prompt for
92
+ a human conversation, not a blocked pipeline.
93
+
41
94
  ## End-to-end flow
42
95
 
43
96
  `uscha-discovery` is the front for something new (you only have the idea); `uscha-adr-refine` is the front
package/uscha-kit/VERSION CHANGED
@@ -1 +1 @@
1
- uscha-kit 1.56.0
1
+ uscha-kit 1.59.0
@@ -0,0 +1 @@
1
+ {"AC-FP-01": true, "AC-FP-11": true, "AC-FP-07": true, "AC-FP-06": true, "AC-FP-05": true, "AC-FP-02": true, "AC-FP-03": true, "AC-FP-09": true, "AC-FP-10": true, "AC-FP-08": true}
@@ -0,0 +1 @@
1
+ {"AC-SD-01": true, "AC-SD-03": true, "AC-SD-02": true, "AC-SD-04": true}
@@ -169,6 +169,27 @@ install `hooks/block-approved-writes.py` as a `PreToolUse` hook in
169
169
  `settings.json`, and add `*.approved.* binary` to `.gitattributes` (ships in
170
170
  `templates/.gitattributes`) so line endings can't lie in the byte-compare.
171
171
 
172
+ ## Phase 0a — Fast-path check (ADR-003; run FIRST, before planning ceremony)
173
+
174
+ If `defaults.fast_path` exists in config, run the measured classifier before demanding a
175
+ full spec package:
176
+
177
+ ```bash
178
+ python qa_ledger.py fastpath-eval --repo <name> --json # dry-run first
179
+ ```
180
+
181
+ - **ALLOW** and the operator wants the shortcut: re-run with
182
+ `--intent "<one sentence: what and why>"` to record it, then skip Phase 0's full-package
183
+ demand. The micro-contract replaces it: the recorded INTENT plus at least one new/modified
184
+ asserting test (readiness stays capped until that test shows up in measured evidence).
185
+ - **DENY**: state WHICH measured signal denied it — echo the engine's breakdown verbatim.
186
+ The skill wires; it never computes and never argues with the verdict. Proceed with the
187
+ normal full path. The operator may always choose the full path over an ALLOW; nothing —
188
+ operator, agent or flag — can force ALLOW over a DENY (INV-RIGOR-02).
189
+ - **Re-evaluate before the PR step** (same command, same intent): thresholds exceeded mid-run
190
+ flip the run to `ESCALATED` — the derived phase blocks pr-ready and readiness is capped.
191
+ Produce the ADR + ACCEPTANCE the change turned out to deserve, then `resolve-escalation`.
192
+
172
193
  ## Phase 0 — Plan (ADR-first)
173
194
 
174
195
  - **Read `CONSTITUTION.md` first (if present).** It lists the project invariants no SPEC