@andresmassello/uscha 1.64.0 → 1.66.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.64.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
43
+ **Kit v1.66.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
 
@@ -76,7 +76,7 @@ and see which file, which test, and when.
76
76
  | `/uscha-mirador` | Bird's-eye HTML dashboard: readiness, trail, acceptance, loops |
77
77
  | `/uscha-status` | One-line progress readout, in chat |
78
78
 
79
- **A measurement engine** (`qa_ledger.py`, 34 subcommands, Python stdlib) that ingests
79
+ **A measurement engine** (`qa_ledger.py`, 35 subcommands, Python stdlib) that ingests
80
80
  evidence from **11 language stacks** — maven, gradle, ant, python, node, go, rust, dotnet,
81
81
  cpp, swift, flutter — and computes a readiness score with hard caps and visible provenance.
82
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresmassello/uscha",
3
- "version": "1.64.0",
3
+ "version": "1.66.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",
@@ -488,6 +488,19 @@ count, plateau flag per repo — to `ledger["measured"]` (what the statusline re
488
488
  Without it the trail starves: the mirador shows "no history yet" and the statusline
489
489
  falls back to counting checkboxes. Recording is append-only facts, never a gate.
490
490
 
491
+ Right after readiness, run the spec-maintenance advisory (kit 1.66.0):
492
+
493
+ ```bash
494
+ python3 $QL spec-drift --repo <REPO>
495
+ ```
496
+
497
+ Milliseconds, deterministic, exit 0 always — it never gates, so running it every pass
498
+ adds zero ceremony. What it adds is VISIBILITY: the run lands in the ledger, so the
499
+ mirador card and `/uscha-status` show drift without anyone remembering the command —
500
+ the user this advisory exists for is precisely the one who never types it. If any doc
501
+ reads `SPEC_STALE`, mention it in the close block's `Blocks:` line as advisory context
502
+ (it blocks nothing; it informs the human's next conversation).
503
+
491
504
  **Single-verdict view (kit 1.25.0, anti-ceremony).** By default `readiness` is ONE
492
505
  screen: the verdict line, any conditional warning that actually fired (it speaks only
493
506
  when it matters), and a `--- gates:` line that COLLAPSES every persisted gate record —
@@ -1868,6 +1868,15 @@ def _origin_label(origin):
1868
1868
  return "%s/%s" % (sha, state)
1869
1869
 
1870
1870
 
1871
+ def _scope_path(ledger, name):
1872
+ """Repo path for a scope, guarding the SYNTHETIC `integration` scope -- never present
1873
+ in config["repos"], where _repo_cfg exits on unknown names. This crash class shipped
1874
+ once (the clean-room gate, 1.63.0) and nearly shipped twice (spec-drift at pass close,
1875
+ caught by fresh review): two recurrences make it a helper, not a per-site pattern."""
1876
+ cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": "."}
1877
+ return cfg.get("path", ".")
1878
+
1879
+
1871
1880
  def _evidence_origin(repo_path):
1872
1881
  """WHERE the evidence came from: the commit it was measured at, and whether the tree
1873
1882
  was clean (ADR-007). The engine's freshness check compares file MTIMES, so until now a
@@ -3145,7 +3154,7 @@ def cmd_spec_drift(args):
3145
3154
  cfg = ledger["config"].get("defaults", {}).get("spec_drift") or {}
3146
3155
  lag_days = int(args.max_lag_days if args.max_lag_days is not None
3147
3156
  else cfg.get("max_lag_days", 30))
3148
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3157
+ repo_path = _scope_path(ledger, args.repo)
3149
3158
 
3150
3159
  # The spec surface is fixed by ADR-005: the repo SPEC.md plus every ADR.
3151
3160
  spec_files = []
@@ -3749,6 +3758,68 @@ def _curation_state(repo_path):
3749
3758
  return state
3750
3759
 
3751
3760
 
3761
+ def cmd_roundtrip(args):
3762
+ """Advisory spec-id coverage (ADR-009 slice 2, v1): which PROMOTED candidates are
3763
+ traceable in the code via an embedded `uscha-spec: <candidate>` marker. Coverage by id,
3764
+ deliberately NOT semantic matching -- that stays out of scope until it can be measured
3765
+ (ADR-011). Advisory end to end: exit 0 always, a report, never a gate."""
3766
+ ledger = _load(args.ledger)
3767
+ _repo_node(ledger, args.repo)
3768
+ repo_path = _scope_path(ledger, args.repo)
3769
+ st = _curation_state(repo_path)
3770
+ if st is None:
3771
+ print("ROUNDTRIP %s: no %s/ directory -- feature unused, nothing to trace."
3772
+ % (args.repo, CANDIDATE_DIR))
3773
+ sys.exit(0)
3774
+ promoted = sorted(st["promote_as_is"] + st["promote_with_declared_divergence"])
3775
+ ls = subprocess.run(["git", "ls-files"], cwd=repo_path, capture_output=True,
3776
+ text=True, encoding="utf-8", errors="replace")
3777
+ tracked = [l.strip() for l in ls.stdout.splitlines()
3778
+ if ls.returncode == 0 and l.strip()
3779
+ and not l.strip().startswith(CANDIDATE_DIR + "/")
3780
+ and os.path.basename(l.strip()) != BEHAVIOR_LEDGER_FILE]
3781
+ found = set()
3782
+ pat = re.compile(r"uscha-spec:\s*([\w.\-]+)")
3783
+ ROUNDTRIP_MAX_BYTES = 2 * 1024 * 1024
3784
+ for f in tracked:
3785
+ full = os.path.join(repo_path, f)
3786
+ try:
3787
+ if os.path.getsize(full) > ROUNDTRIP_MAX_BYTES:
3788
+ continue # a 2MB+ tracked file is not where a spec-id marker lives; an
3789
+ # unbounded full-tree read is the T112 lesson, applied here
3790
+ with open(full, encoding="utf-8", errors="replace") as fh:
3791
+ body = fh.read()
3792
+ except OSError:
3793
+ continue
3794
+ for m in pat.finditer(body):
3795
+ mid = m.group(1)
3796
+ found.add(mid if mid.endswith(".md") else mid + ".md")
3797
+ covered = [c for c in promoted if c in found]
3798
+ missing = [c for c in promoted if c not in found]
3799
+ out = {"repo": args.repo, "promoted": len(promoted), "covered": len(covered),
3800
+ "missing": missing, "advisory": True,
3801
+ "coverage_pct": round(100.0 * len(covered) / len(promoted), 1) if promoted else None}
3802
+ # Latest-state record so the mirador/status can surface it without anyone re-running the
3803
+ # command (the spec_drift pattern). A report that evaporates on exit is invisible to
3804
+ # every read surface -- which defeats the point of an advisory (found by auditing which
3805
+ # features actually REACH the user). Advisory data: no step counter, no gate record.
3806
+ ledger["roundtrip"] = dict(out, at=_now())
3807
+ _save(args.ledger, ledger)
3808
+ if args.json:
3809
+ print(json.dumps(out, indent=2, ensure_ascii=False))
3810
+ else:
3811
+ if not promoted:
3812
+ print("ROUNDTRIP %s: no promoted candidates yet -- nothing to trace (advisory)."
3813
+ % args.repo)
3814
+ else:
3815
+ print("ROUNDTRIP %s: %d/%d promoted candidate(s) traceable by uscha-spec id "
3816
+ "(advisory)" % (args.repo, len(covered), len(promoted)))
3817
+ for mss in missing:
3818
+ print(" .. %s: no uscha-spec marker found in the code" % mss)
3819
+ sys.exit(0)
3820
+
3821
+
3822
+
3752
3823
  def cmd_curation_check(args):
3753
3824
  """The INV-CURATION-01 gate, measured. Exit 2: malformation or tampering (config-error
3754
3825
  class -- candidates that cannot be validated, a ledger that cannot be trusted). Exit 1:
@@ -3756,7 +3827,7 @@ def cmd_curation_check(args):
3756
3827
  candidate judged, or the feature unused."""
3757
3828
  ledger = _load(args.ledger)
3758
3829
  _repo_node(ledger, args.repo)
3759
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3830
+ repo_path = _scope_path(ledger, args.repo)
3760
3831
  st = _curation_state(repo_path)
3761
3832
  if st is None:
3762
3833
  if args.json:
@@ -4840,6 +4911,8 @@ def cmd_dashboard(args):
4840
4911
  if ledger.get(CLEAN_ROOM_KEY):
4841
4912
  out["clean_room"] = {r: [e for e in ledger[CLEAN_ROOM_KEY] if e.get("repo") == r][-1]
4842
4913
  for r in {e.get("repo") for e in ledger[CLEAN_ROOM_KEY]}}
4914
+ if ledger.get("roundtrip"):
4915
+ out["roundtrip"] = ledger["roundtrip"]
4843
4916
  if getattr(args, "json", False):
4844
4917
  print(json.dumps(out, indent=2, ensure_ascii=False))
4845
4918
  return
@@ -6933,6 +7006,34 @@ def _golden_approved_path(rec):
6933
7006
 
6934
7007
 
6935
7008
  GOLDEN_SCRUB_FILE = "golden.scrub.json"
7009
+ GOLDEN_DIVERGENCES_FILE = "golden.divergences.json"
7010
+
7011
+
7012
+ def _load_golden_divergences(root):
7013
+ """Expected divergences for `fix` verdicts (ADR-009 slice 2): a golden that MUST differ
7014
+ because its ADR says the behavior was corrected. Shape:
7015
+ {"divergences": {"<fixture basename>": {"adr": "ADR-RD-NNN", "reason": "..."}}}.
7016
+ Strict like the scrub rules: a typo must not degrade into "no declarations" -- that
7017
+ silence would turn every expected divergence back into a blocker, or worse, hide a
7018
+ declared one behind a malformed file. Absent file -> {} (nothing declared)."""
7019
+ path = os.path.join(root, GOLDEN_DIVERGENCES_FILE)
7020
+ if not os.path.isfile(path):
7021
+ return {}
7022
+ try:
7023
+ with open(path, "r", encoding="utf-8") as fh:
7024
+ spec = json.load(fh)
7025
+ if not isinstance(spec, dict) or not isinstance(spec.get("divergences"), dict):
7026
+ raise TypeError('expected {"divergences": {"<fixture>": {"adr":..., "reason":...}}}')
7027
+ for k, v in spec["divergences"].items():
7028
+ if (not isinstance(v, dict) or not re.match(r"^ADR-\S+$", str(v.get("adr", "")))
7029
+ or not str(v.get("reason", "")).strip()):
7030
+ raise TypeError("divergence %r needs adr (ADR-...) and a reason" % k)
7031
+ return spec["divergences"]
7032
+ except (json.JSONDecodeError, TypeError, KeyError) as exc:
7033
+ print("[qa_ledger] %s invalid (%s) - declared divergences are not skipped in "
7034
+ "silence: fix the file or delete it." % (path, exc), file=sys.stderr)
7035
+ sys.exit(2)
7036
+
6936
7037
 
6937
7038
 
6938
7039
  def _load_scrub_rules(root):
@@ -7050,6 +7151,9 @@ def cmd_golden_diff(args):
7050
7151
  received = [p for p in sorted(hits) if os.path.isfile(p)] # skip dirs matched by glob
7051
7152
  rules = _load_scrub_rules(root)
7052
7153
  labels = _load_golden_labels(getattr(args, "labels", None))
7154
+ divergences = _load_golden_divergences(root)
7155
+ expected_diverged = 0
7156
+ consumed_declarations = set()
7053
7157
  scrub_counts = {}
7054
7158
  diverged = [] # (received_path, reason)
7055
7159
  fixtures = []
@@ -7078,21 +7182,60 @@ def cmd_golden_diff(args):
7078
7182
  fixture["result"] = "read_error"
7079
7183
  diverged.append((rec, f"could not read: {exc}"))
7080
7184
  continue
7185
+ decl, decl_key = None, None
7186
+ for cand_key in (os.path.relpath(app, root).replace(os.sep, "/"),
7187
+ os.path.relpath(rec, root).replace(os.sep, "/"),
7188
+ os.path.basename(app), os.path.basename(rec)):
7189
+ # relpath first (the _golden_label pattern: nested suites share basenames and a
7190
+ # declaration must not launder an unrelated module\x27s divergence -- fresh-review
7191
+ # finding); basename stays as the flat-layout convenience.
7192
+ if cand_key in divergences:
7193
+ decl, decl_key = divergences[cand_key], cand_key
7194
+ break
7195
+ if decl:
7196
+ consumed_declarations.add(decl_key) # only what MATCHED: an unexercised twin
7197
+ # key must still show as unconsumed
7081
7198
  if rb == ab:
7199
+ if decl:
7200
+ # a `fix` verdict DECLARED this golden must differ -- identical bytes mean
7201
+ # the corrected behavior never landed. An expected divergence that is not
7202
+ # observed is a red finding, not a quiet pass (ADR-010: fix cases must
7203
+ # diverge exactly as their ADR describes; identical is not that).
7204
+ fixture["result"] = "declared_divergence_not_observed"
7205
+ diverged.append((rec, "declared divergent (%s) but IDENTICAL -- the fix "
7206
+ "this declaration describes is not in the output"
7207
+ % decl["adr"]))
7208
+ continue
7082
7209
  fixture["result"] = "matched"
7083
7210
  matched += 1
7084
7211
  # el conteo reportado es del lado RECEIVED (la captura fresca) — sumar
7085
7212
  # ambos lados duplicaria cada volatil enmascarado en el reporte.
7086
7213
  elif rules and (_scrub(rb, rules, scrub_counts)
7087
7214
  == _scrub(ab, rules, {})):
7215
+ if decl:
7216
+ # scrub-equal IS "not observed": once declared volatiles are masked the
7217
+ # outputs are behaviorally identical, so the fix this declaration
7218
+ # describes is absent -- and letting the scrub branch swallow it hid the
7219
+ # case from every signal (fresh-review HIGH: untested interaction).
7220
+ fixture["result"] = "declared_divergence_not_observed"
7221
+ diverged.append((rec, "declared divergent (%s) but scrub-equal -- "
7222
+ "identical once volatiles are masked; the declared "
7223
+ "fix is not in the output" % decl["adr"]))
7224
+ continue
7088
7225
  # matchea SOLO tras enmascarar volatiles declarados — cuenta como
7089
7226
  # pass pero se reporta APARTE: el masking jamas es invisible.
7090
7227
  fixture["result"] = "matched_scrubbed"
7091
7228
  matched_scrubbed += 1
7229
+ elif decl:
7230
+ # diverges AND a fix verdict declared it would: expected, named, never silent.
7231
+ fixture["result"] = "expected_divergence"
7232
+ fixture["divergence_adr"] = decl["adr"]
7233
+ expected_diverged += 1
7092
7234
  else:
7093
7235
  fixture["result"] = "diverged"
7094
7236
  diverged.append((rec, "diff NO aprobado contra .approved"))
7095
7237
 
7238
+ unconsumed = sorted(k for k in divergences if k not in consumed_declarations)
7096
7239
  passed = len(diverged) == 0
7097
7240
  # zero fixtures is NOT-RUN, never CLEAN: a comparison that had nothing to
7098
7241
  # compare is absent evidence — log it as not-run (absence advises, a present
@@ -7114,6 +7257,8 @@ def cmd_golden_diff(args):
7114
7257
  "scrub_rules": len(rules),
7115
7258
  "scrub_substitutions": scrub_counts,
7116
7259
  "golden_labels": _golden_label_counts(fixtures),
7260
+ "expected_diverged": expected_diverged,
7261
+ "unconsumed_declarations": unconsumed,
7117
7262
  "fixtures": fixtures,
7118
7263
  "diverged": [{"file": f, "reason": r} for f, r in diverged],
7119
7264
  }, indent=2, ensure_ascii=False))
@@ -7551,6 +7696,13 @@ def build_parser():
7551
7696
  pcu.add_argument("--json", action="store_true")
7552
7697
  pcu.set_defaults(func=cmd_curation_check)
7553
7698
 
7699
+ prt = sub.add_parser("roundtrip",
7700
+ help="advisory: which promoted candidates are traceable in code via uscha-spec ids (ADR-009 slice 2)")
7701
+ prt.add_argument("--ledger", default="QA-LEDGER.json")
7702
+ prt.add_argument("--repo", required=True)
7703
+ prt.add_argument("--json", action="store_true")
7704
+ prt.set_defaults(func=cmd_roundtrip)
7705
+
7554
7706
  pcr = sub.add_parser("cleanroom",
7555
7707
  help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
7556
7708
  pcr.add_argument("--ledger", default="QA-LEDGER.json")
@@ -100,6 +100,10 @@ nothing and gates nothing.
100
100
  `spec-drift: N stale / M docs (advisory)` — or `spec-drift: no drift measured` when zero are
101
101
  stale. Always label it advisory; it never explains a blocked phase. Absent key → no line.
102
102
 
103
+ **Roundtrip (ADR-009 slice 2):** if the ledger carries a `roundtrip` run, add ONE line:
104
+ `roundtrip: N/M promoted traceable by uscha-spec id (advisory)`. Absent key → no line —
105
+ silence is honest when the loop was never measured.
106
+
103
107
  ## Degradation (honest, specific)
104
108
 
105
109
  - `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.64.0",
4
+ "version": "1.66.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, 34 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, 35 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.64.0",
3
+ "version": "1.66.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.64.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.66.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`,
@@ -197,6 +197,25 @@ any candidate lacks a verdict, `pr-ready` is blocked naming it (INV-CURATION-01)
197
197
  quarantine is measured, not promised. No `discovery/` directory -> the feature does not
198
198
  exist and nothing changes.
199
199
 
200
+ ## Oracle divergences + roundtrip (slice 2)
201
+
202
+ A `fix` verdict means the new system must NOT match the legacy golden - and that divergence
203
+ is **declared**, never tolerated implicitly:
204
+
205
+ ```json
206
+ // golden.divergences.json
207
+ { "divergences": { "invoice-totals.approved.json": {
208
+ "adr": "ADR-RD-003", "reason": "IVA now rounds; legacy truncated" } } }
209
+ ```
210
+
211
+ `golden-diff` then reads the pair as `expected_divergence` (named, with its ADR) instead of
212
+ a blocker - and a declared pair that comes back **identical** goes red: the fix the
213
+ declaration describes is not in the output. Malformed declarations exit 2.
214
+
215
+ `roundtrip --repo <name>` is the advisory closing of the loop, v1: which promoted
216
+ candidates are traceable in the code via an embedded `uscha-spec: <candidate>` marker -
217
+ coverage by id, deliberately not semantic matching, exit 0 always.
218
+
200
219
  ## End-to-end flow
201
220
 
202
221
  `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.64.0
1
+ uscha-kit 1.66.0
@@ -1 +1 @@
1
- {"AC-RD-07": true, "AC-RD-01": true, "AC-RD-02": true, "AC-RD-03": true, "AC-RD-06": true, "AC-RD-04": true, "AC-RD-05": true}
1
+ {"AC-RD-07": true, "AC-RD-01": true, "AC-RD-02": true, "AC-RD-03": true, "AC-RD-06": true, "AC-RD-04": true, "AC-RD-05": true, "AC-RD-13": true}
@@ -0,0 +1 @@
1
+ {"AC-RD-08": true, "AC-RD-09": true, "AC-RD-10": true, "AC-RD-11": true, "AC-RD-12": true}
@@ -488,6 +488,19 @@ count, plateau flag per repo — to `ledger["measured"]` (what the statusline re
488
488
  Without it the trail starves: the mirador shows "no history yet" and the statusline
489
489
  falls back to counting checkboxes. Recording is append-only facts, never a gate.
490
490
 
491
+ Right after readiness, run the spec-maintenance advisory (kit 1.66.0):
492
+
493
+ ```bash
494
+ python3 $QL spec-drift --repo <REPO>
495
+ ```
496
+
497
+ Milliseconds, deterministic, exit 0 always — it never gates, so running it every pass
498
+ adds zero ceremony. What it adds is VISIBILITY: the run lands in the ledger, so the
499
+ mirador card and `/uscha-status` show drift without anyone remembering the command —
500
+ the user this advisory exists for is precisely the one who never types it. If any doc
501
+ reads `SPEC_STALE`, mention it in the close block's `Blocks:` line as advisory context
502
+ (it blocks nothing; it informs the human's next conversation).
503
+
491
504
  **Single-verdict view (kit 1.25.0, anti-ceremony).** By default `readiness` is ONE
492
505
  screen: the verdict line, any conditional warning that actually fired (it speaks only
493
506
  when it matters), and a `--- gates:` line that COLLAPSES every persisted gate record —
@@ -1868,6 +1868,15 @@ def _origin_label(origin):
1868
1868
  return "%s/%s" % (sha, state)
1869
1869
 
1870
1870
 
1871
+ def _scope_path(ledger, name):
1872
+ """Repo path for a scope, guarding the SYNTHETIC `integration` scope -- never present
1873
+ in config["repos"], where _repo_cfg exits on unknown names. This crash class shipped
1874
+ once (the clean-room gate, 1.63.0) and nearly shipped twice (spec-drift at pass close,
1875
+ caught by fresh review): two recurrences make it a helper, not a per-site pattern."""
1876
+ cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": "."}
1877
+ return cfg.get("path", ".")
1878
+
1879
+
1871
1880
  def _evidence_origin(repo_path):
1872
1881
  """WHERE the evidence came from: the commit it was measured at, and whether the tree
1873
1882
  was clean (ADR-007). The engine's freshness check compares file MTIMES, so until now a
@@ -3145,7 +3154,7 @@ def cmd_spec_drift(args):
3145
3154
  cfg = ledger["config"].get("defaults", {}).get("spec_drift") or {}
3146
3155
  lag_days = int(args.max_lag_days if args.max_lag_days is not None
3147
3156
  else cfg.get("max_lag_days", 30))
3148
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3157
+ repo_path = _scope_path(ledger, args.repo)
3149
3158
 
3150
3159
  # The spec surface is fixed by ADR-005: the repo SPEC.md plus every ADR.
3151
3160
  spec_files = []
@@ -3749,6 +3758,68 @@ def _curation_state(repo_path):
3749
3758
  return state
3750
3759
 
3751
3760
 
3761
+ def cmd_roundtrip(args):
3762
+ """Advisory spec-id coverage (ADR-009 slice 2, v1): which PROMOTED candidates are
3763
+ traceable in the code via an embedded `uscha-spec: <candidate>` marker. Coverage by id,
3764
+ deliberately NOT semantic matching -- that stays out of scope until it can be measured
3765
+ (ADR-011). Advisory end to end: exit 0 always, a report, never a gate."""
3766
+ ledger = _load(args.ledger)
3767
+ _repo_node(ledger, args.repo)
3768
+ repo_path = _scope_path(ledger, args.repo)
3769
+ st = _curation_state(repo_path)
3770
+ if st is None:
3771
+ print("ROUNDTRIP %s: no %s/ directory -- feature unused, nothing to trace."
3772
+ % (args.repo, CANDIDATE_DIR))
3773
+ sys.exit(0)
3774
+ promoted = sorted(st["promote_as_is"] + st["promote_with_declared_divergence"])
3775
+ ls = subprocess.run(["git", "ls-files"], cwd=repo_path, capture_output=True,
3776
+ text=True, encoding="utf-8", errors="replace")
3777
+ tracked = [l.strip() for l in ls.stdout.splitlines()
3778
+ if ls.returncode == 0 and l.strip()
3779
+ and not l.strip().startswith(CANDIDATE_DIR + "/")
3780
+ and os.path.basename(l.strip()) != BEHAVIOR_LEDGER_FILE]
3781
+ found = set()
3782
+ pat = re.compile(r"uscha-spec:\s*([\w.\-]+)")
3783
+ ROUNDTRIP_MAX_BYTES = 2 * 1024 * 1024
3784
+ for f in tracked:
3785
+ full = os.path.join(repo_path, f)
3786
+ try:
3787
+ if os.path.getsize(full) > ROUNDTRIP_MAX_BYTES:
3788
+ continue # a 2MB+ tracked file is not where a spec-id marker lives; an
3789
+ # unbounded full-tree read is the T112 lesson, applied here
3790
+ with open(full, encoding="utf-8", errors="replace") as fh:
3791
+ body = fh.read()
3792
+ except OSError:
3793
+ continue
3794
+ for m in pat.finditer(body):
3795
+ mid = m.group(1)
3796
+ found.add(mid if mid.endswith(".md") else mid + ".md")
3797
+ covered = [c for c in promoted if c in found]
3798
+ missing = [c for c in promoted if c not in found]
3799
+ out = {"repo": args.repo, "promoted": len(promoted), "covered": len(covered),
3800
+ "missing": missing, "advisory": True,
3801
+ "coverage_pct": round(100.0 * len(covered) / len(promoted), 1) if promoted else None}
3802
+ # Latest-state record so the mirador/status can surface it without anyone re-running the
3803
+ # command (the spec_drift pattern). A report that evaporates on exit is invisible to
3804
+ # every read surface -- which defeats the point of an advisory (found by auditing which
3805
+ # features actually REACH the user). Advisory data: no step counter, no gate record.
3806
+ ledger["roundtrip"] = dict(out, at=_now())
3807
+ _save(args.ledger, ledger)
3808
+ if args.json:
3809
+ print(json.dumps(out, indent=2, ensure_ascii=False))
3810
+ else:
3811
+ if not promoted:
3812
+ print("ROUNDTRIP %s: no promoted candidates yet -- nothing to trace (advisory)."
3813
+ % args.repo)
3814
+ else:
3815
+ print("ROUNDTRIP %s: %d/%d promoted candidate(s) traceable by uscha-spec id "
3816
+ "(advisory)" % (args.repo, len(covered), len(promoted)))
3817
+ for mss in missing:
3818
+ print(" .. %s: no uscha-spec marker found in the code" % mss)
3819
+ sys.exit(0)
3820
+
3821
+
3822
+
3752
3823
  def cmd_curation_check(args):
3753
3824
  """The INV-CURATION-01 gate, measured. Exit 2: malformation or tampering (config-error
3754
3825
  class -- candidates that cannot be validated, a ledger that cannot be trusted). Exit 1:
@@ -3756,7 +3827,7 @@ def cmd_curation_check(args):
3756
3827
  candidate judged, or the feature unused."""
3757
3828
  ledger = _load(args.ledger)
3758
3829
  _repo_node(ledger, args.repo)
3759
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3830
+ repo_path = _scope_path(ledger, args.repo)
3760
3831
  st = _curation_state(repo_path)
3761
3832
  if st is None:
3762
3833
  if args.json:
@@ -4840,6 +4911,8 @@ def cmd_dashboard(args):
4840
4911
  if ledger.get(CLEAN_ROOM_KEY):
4841
4912
  out["clean_room"] = {r: [e for e in ledger[CLEAN_ROOM_KEY] if e.get("repo") == r][-1]
4842
4913
  for r in {e.get("repo") for e in ledger[CLEAN_ROOM_KEY]}}
4914
+ if ledger.get("roundtrip"):
4915
+ out["roundtrip"] = ledger["roundtrip"]
4843
4916
  if getattr(args, "json", False):
4844
4917
  print(json.dumps(out, indent=2, ensure_ascii=False))
4845
4918
  return
@@ -6933,6 +7006,34 @@ def _golden_approved_path(rec):
6933
7006
 
6934
7007
 
6935
7008
  GOLDEN_SCRUB_FILE = "golden.scrub.json"
7009
+ GOLDEN_DIVERGENCES_FILE = "golden.divergences.json"
7010
+
7011
+
7012
+ def _load_golden_divergences(root):
7013
+ """Expected divergences for `fix` verdicts (ADR-009 slice 2): a golden that MUST differ
7014
+ because its ADR says the behavior was corrected. Shape:
7015
+ {"divergences": {"<fixture basename>": {"adr": "ADR-RD-NNN", "reason": "..."}}}.
7016
+ Strict like the scrub rules: a typo must not degrade into "no declarations" -- that
7017
+ silence would turn every expected divergence back into a blocker, or worse, hide a
7018
+ declared one behind a malformed file. Absent file -> {} (nothing declared)."""
7019
+ path = os.path.join(root, GOLDEN_DIVERGENCES_FILE)
7020
+ if not os.path.isfile(path):
7021
+ return {}
7022
+ try:
7023
+ with open(path, "r", encoding="utf-8") as fh:
7024
+ spec = json.load(fh)
7025
+ if not isinstance(spec, dict) or not isinstance(spec.get("divergences"), dict):
7026
+ raise TypeError('expected {"divergences": {"<fixture>": {"adr":..., "reason":...}}}')
7027
+ for k, v in spec["divergences"].items():
7028
+ if (not isinstance(v, dict) or not re.match(r"^ADR-\S+$", str(v.get("adr", "")))
7029
+ or not str(v.get("reason", "")).strip()):
7030
+ raise TypeError("divergence %r needs adr (ADR-...) and a reason" % k)
7031
+ return spec["divergences"]
7032
+ except (json.JSONDecodeError, TypeError, KeyError) as exc:
7033
+ print("[qa_ledger] %s invalid (%s) - declared divergences are not skipped in "
7034
+ "silence: fix the file or delete it." % (path, exc), file=sys.stderr)
7035
+ sys.exit(2)
7036
+
6936
7037
 
6937
7038
 
6938
7039
  def _load_scrub_rules(root):
@@ -7050,6 +7151,9 @@ def cmd_golden_diff(args):
7050
7151
  received = [p for p in sorted(hits) if os.path.isfile(p)] # skip dirs matched by glob
7051
7152
  rules = _load_scrub_rules(root)
7052
7153
  labels = _load_golden_labels(getattr(args, "labels", None))
7154
+ divergences = _load_golden_divergences(root)
7155
+ expected_diverged = 0
7156
+ consumed_declarations = set()
7053
7157
  scrub_counts = {}
7054
7158
  diverged = [] # (received_path, reason)
7055
7159
  fixtures = []
@@ -7078,21 +7182,60 @@ def cmd_golden_diff(args):
7078
7182
  fixture["result"] = "read_error"
7079
7183
  diverged.append((rec, f"could not read: {exc}"))
7080
7184
  continue
7185
+ decl, decl_key = None, None
7186
+ for cand_key in (os.path.relpath(app, root).replace(os.sep, "/"),
7187
+ os.path.relpath(rec, root).replace(os.sep, "/"),
7188
+ os.path.basename(app), os.path.basename(rec)):
7189
+ # relpath first (the _golden_label pattern: nested suites share basenames and a
7190
+ # declaration must not launder an unrelated module\x27s divergence -- fresh-review
7191
+ # finding); basename stays as the flat-layout convenience.
7192
+ if cand_key in divergences:
7193
+ decl, decl_key = divergences[cand_key], cand_key
7194
+ break
7195
+ if decl:
7196
+ consumed_declarations.add(decl_key) # only what MATCHED: an unexercised twin
7197
+ # key must still show as unconsumed
7081
7198
  if rb == ab:
7199
+ if decl:
7200
+ # a `fix` verdict DECLARED this golden must differ -- identical bytes mean
7201
+ # the corrected behavior never landed. An expected divergence that is not
7202
+ # observed is a red finding, not a quiet pass (ADR-010: fix cases must
7203
+ # diverge exactly as their ADR describes; identical is not that).
7204
+ fixture["result"] = "declared_divergence_not_observed"
7205
+ diverged.append((rec, "declared divergent (%s) but IDENTICAL -- the fix "
7206
+ "this declaration describes is not in the output"
7207
+ % decl["adr"]))
7208
+ continue
7082
7209
  fixture["result"] = "matched"
7083
7210
  matched += 1
7084
7211
  # el conteo reportado es del lado RECEIVED (la captura fresca) — sumar
7085
7212
  # ambos lados duplicaria cada volatil enmascarado en el reporte.
7086
7213
  elif rules and (_scrub(rb, rules, scrub_counts)
7087
7214
  == _scrub(ab, rules, {})):
7215
+ if decl:
7216
+ # scrub-equal IS "not observed": once declared volatiles are masked the
7217
+ # outputs are behaviorally identical, so the fix this declaration
7218
+ # describes is absent -- and letting the scrub branch swallow it hid the
7219
+ # case from every signal (fresh-review HIGH: untested interaction).
7220
+ fixture["result"] = "declared_divergence_not_observed"
7221
+ diverged.append((rec, "declared divergent (%s) but scrub-equal -- "
7222
+ "identical once volatiles are masked; the declared "
7223
+ "fix is not in the output" % decl["adr"]))
7224
+ continue
7088
7225
  # matchea SOLO tras enmascarar volatiles declarados — cuenta como
7089
7226
  # pass pero se reporta APARTE: el masking jamas es invisible.
7090
7227
  fixture["result"] = "matched_scrubbed"
7091
7228
  matched_scrubbed += 1
7229
+ elif decl:
7230
+ # diverges AND a fix verdict declared it would: expected, named, never silent.
7231
+ fixture["result"] = "expected_divergence"
7232
+ fixture["divergence_adr"] = decl["adr"]
7233
+ expected_diverged += 1
7092
7234
  else:
7093
7235
  fixture["result"] = "diverged"
7094
7236
  diverged.append((rec, "diff NO aprobado contra .approved"))
7095
7237
 
7238
+ unconsumed = sorted(k for k in divergences if k not in consumed_declarations)
7096
7239
  passed = len(diverged) == 0
7097
7240
  # zero fixtures is NOT-RUN, never CLEAN: a comparison that had nothing to
7098
7241
  # compare is absent evidence — log it as not-run (absence advises, a present
@@ -7114,6 +7257,8 @@ def cmd_golden_diff(args):
7114
7257
  "scrub_rules": len(rules),
7115
7258
  "scrub_substitutions": scrub_counts,
7116
7259
  "golden_labels": _golden_label_counts(fixtures),
7260
+ "expected_diverged": expected_diverged,
7261
+ "unconsumed_declarations": unconsumed,
7117
7262
  "fixtures": fixtures,
7118
7263
  "diverged": [{"file": f, "reason": r} for f, r in diverged],
7119
7264
  }, indent=2, ensure_ascii=False))
@@ -7551,6 +7696,13 @@ def build_parser():
7551
7696
  pcu.add_argument("--json", action="store_true")
7552
7697
  pcu.set_defaults(func=cmd_curation_check)
7553
7698
 
7699
+ prt = sub.add_parser("roundtrip",
7700
+ help="advisory: which promoted candidates are traceable in code via uscha-spec ids (ADR-009 slice 2)")
7701
+ prt.add_argument("--ledger", default="QA-LEDGER.json")
7702
+ prt.add_argument("--repo", required=True)
7703
+ prt.add_argument("--json", action="store_true")
7704
+ prt.set_defaults(func=cmd_roundtrip)
7705
+
7554
7706
  pcr = sub.add_parser("cleanroom",
7555
7707
  help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
7556
7708
  pcr.add_argument("--ledger", default="QA-LEDGER.json")
@@ -100,6 +100,10 @@ nothing and gates nothing.
100
100
  `spec-drift: N stale / M docs (advisory)` — or `spec-drift: no drift measured` when zero are
101
101
  stale. Always label it advisory; it never explains a blocked phase. Absent key → no line.
102
102
 
103
+ **Roundtrip (ADR-009 slice 2):** if the ledger carries a `roundtrip` run, add ONE line:
104
+ `roundtrip: N/M promoted traceable by uscha-spec id (advisory)`. Absent key → no line —
105
+ silence is honest when the loop was never measured.
106
+
103
107
  ## Degradation (honest, specific)
104
108
 
105
109
  - `measured` missing entirely → print: *"No measurement recorded yet — the trail
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.64.0",
2
+ "version": "1.66.0",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,