@andresmassello/uscha 1.64.0 → 1.65.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.65.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.65.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",
@@ -3749,6 +3749,62 @@ def _curation_state(repo_path):
3749
3749
  return state
3750
3750
 
3751
3751
 
3752
+ def cmd_roundtrip(args):
3753
+ """Advisory spec-id coverage (ADR-009 slice 2, v1): which PROMOTED candidates are
3754
+ traceable in the code via an embedded `uscha-spec: <candidate>` marker. Coverage by id,
3755
+ deliberately NOT semantic matching -- that stays out of scope until it can be measured
3756
+ (ADR-011). Advisory end to end: exit 0 always, a report, never a gate."""
3757
+ ledger = _load(args.ledger)
3758
+ _repo_node(ledger, args.repo)
3759
+ repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3760
+ st = _curation_state(repo_path)
3761
+ if st is None:
3762
+ print("ROUNDTRIP %s: no %s/ directory -- feature unused, nothing to trace."
3763
+ % (args.repo, CANDIDATE_DIR))
3764
+ sys.exit(0)
3765
+ promoted = sorted(st["promote_as_is"] + st["promote_with_declared_divergence"])
3766
+ ls = subprocess.run(["git", "ls-files"], cwd=repo_path, capture_output=True,
3767
+ text=True, encoding="utf-8", errors="replace")
3768
+ tracked = [l.strip() for l in ls.stdout.splitlines()
3769
+ if ls.returncode == 0 and l.strip()
3770
+ and not l.strip().startswith(CANDIDATE_DIR + "/")
3771
+ and os.path.basename(l.strip()) != BEHAVIOR_LEDGER_FILE]
3772
+ found = set()
3773
+ pat = re.compile(r"uscha-spec:\s*([\w.\-]+)")
3774
+ ROUNDTRIP_MAX_BYTES = 2 * 1024 * 1024
3775
+ for f in tracked:
3776
+ full = os.path.join(repo_path, f)
3777
+ try:
3778
+ if os.path.getsize(full) > ROUNDTRIP_MAX_BYTES:
3779
+ continue # a 2MB+ tracked file is not where a spec-id marker lives; an
3780
+ # unbounded full-tree read is the T112 lesson, applied here
3781
+ with open(full, encoding="utf-8", errors="replace") as fh:
3782
+ body = fh.read()
3783
+ except OSError:
3784
+ continue
3785
+ for m in pat.finditer(body):
3786
+ mid = m.group(1)
3787
+ found.add(mid if mid.endswith(".md") else mid + ".md")
3788
+ covered = [c for c in promoted if c in found]
3789
+ missing = [c for c in promoted if c not in found]
3790
+ out = {"repo": args.repo, "promoted": len(promoted), "covered": len(covered),
3791
+ "missing": missing, "advisory": True,
3792
+ "coverage_pct": round(100.0 * len(covered) / len(promoted), 1) if promoted else None}
3793
+ if args.json:
3794
+ print(json.dumps(out, indent=2, ensure_ascii=False))
3795
+ else:
3796
+ if not promoted:
3797
+ print("ROUNDTRIP %s: no promoted candidates yet -- nothing to trace (advisory)."
3798
+ % args.repo)
3799
+ else:
3800
+ print("ROUNDTRIP %s: %d/%d promoted candidate(s) traceable by uscha-spec id "
3801
+ "(advisory)" % (args.repo, len(covered), len(promoted)))
3802
+ for mss in missing:
3803
+ print(" .. %s: no uscha-spec marker found in the code" % mss)
3804
+ sys.exit(0)
3805
+
3806
+
3807
+
3752
3808
  def cmd_curation_check(args):
3753
3809
  """The INV-CURATION-01 gate, measured. Exit 2: malformation or tampering (config-error
3754
3810
  class -- candidates that cannot be validated, a ledger that cannot be trusted). Exit 1:
@@ -6933,6 +6989,34 @@ def _golden_approved_path(rec):
6933
6989
 
6934
6990
 
6935
6991
  GOLDEN_SCRUB_FILE = "golden.scrub.json"
6992
+ GOLDEN_DIVERGENCES_FILE = "golden.divergences.json"
6993
+
6994
+
6995
+ def _load_golden_divergences(root):
6996
+ """Expected divergences for `fix` verdicts (ADR-009 slice 2): a golden that MUST differ
6997
+ because its ADR says the behavior was corrected. Shape:
6998
+ {"divergences": {"<fixture basename>": {"adr": "ADR-RD-NNN", "reason": "..."}}}.
6999
+ Strict like the scrub rules: a typo must not degrade into "no declarations" -- that
7000
+ silence would turn every expected divergence back into a blocker, or worse, hide a
7001
+ declared one behind a malformed file. Absent file -> {} (nothing declared)."""
7002
+ path = os.path.join(root, GOLDEN_DIVERGENCES_FILE)
7003
+ if not os.path.isfile(path):
7004
+ return {}
7005
+ try:
7006
+ with open(path, "r", encoding="utf-8") as fh:
7007
+ spec = json.load(fh)
7008
+ if not isinstance(spec, dict) or not isinstance(spec.get("divergences"), dict):
7009
+ raise TypeError('expected {"divergences": {"<fixture>": {"adr":..., "reason":...}}}')
7010
+ for k, v in spec["divergences"].items():
7011
+ if (not isinstance(v, dict) or not re.match(r"^ADR-\S+$", str(v.get("adr", "")))
7012
+ or not str(v.get("reason", "")).strip()):
7013
+ raise TypeError("divergence %r needs adr (ADR-...) and a reason" % k)
7014
+ return spec["divergences"]
7015
+ except (json.JSONDecodeError, TypeError, KeyError) as exc:
7016
+ print("[qa_ledger] %s invalid (%s) - declared divergences are not skipped in "
7017
+ "silence: fix the file or delete it." % (path, exc), file=sys.stderr)
7018
+ sys.exit(2)
7019
+
6936
7020
 
6937
7021
 
6938
7022
  def _load_scrub_rules(root):
@@ -7050,6 +7134,9 @@ def cmd_golden_diff(args):
7050
7134
  received = [p for p in sorted(hits) if os.path.isfile(p)] # skip dirs matched by glob
7051
7135
  rules = _load_scrub_rules(root)
7052
7136
  labels = _load_golden_labels(getattr(args, "labels", None))
7137
+ divergences = _load_golden_divergences(root)
7138
+ expected_diverged = 0
7139
+ consumed_declarations = set()
7053
7140
  scrub_counts = {}
7054
7141
  diverged = [] # (received_path, reason)
7055
7142
  fixtures = []
@@ -7078,21 +7165,60 @@ def cmd_golden_diff(args):
7078
7165
  fixture["result"] = "read_error"
7079
7166
  diverged.append((rec, f"could not read: {exc}"))
7080
7167
  continue
7168
+ decl, decl_key = None, None
7169
+ for cand_key in (os.path.relpath(app, root).replace(os.sep, "/"),
7170
+ os.path.relpath(rec, root).replace(os.sep, "/"),
7171
+ os.path.basename(app), os.path.basename(rec)):
7172
+ # relpath first (the _golden_label pattern: nested suites share basenames and a
7173
+ # declaration must not launder an unrelated module\x27s divergence -- fresh-review
7174
+ # finding); basename stays as the flat-layout convenience.
7175
+ if cand_key in divergences:
7176
+ decl, decl_key = divergences[cand_key], cand_key
7177
+ break
7178
+ if decl:
7179
+ consumed_declarations.add(decl_key) # only what MATCHED: an unexercised twin
7180
+ # key must still show as unconsumed
7081
7181
  if rb == ab:
7182
+ if decl:
7183
+ # a `fix` verdict DECLARED this golden must differ -- identical bytes mean
7184
+ # the corrected behavior never landed. An expected divergence that is not
7185
+ # observed is a red finding, not a quiet pass (ADR-010: fix cases must
7186
+ # diverge exactly as their ADR describes; identical is not that).
7187
+ fixture["result"] = "declared_divergence_not_observed"
7188
+ diverged.append((rec, "declared divergent (%s) but IDENTICAL -- the fix "
7189
+ "this declaration describes is not in the output"
7190
+ % decl["adr"]))
7191
+ continue
7082
7192
  fixture["result"] = "matched"
7083
7193
  matched += 1
7084
7194
  # el conteo reportado es del lado RECEIVED (la captura fresca) — sumar
7085
7195
  # ambos lados duplicaria cada volatil enmascarado en el reporte.
7086
7196
  elif rules and (_scrub(rb, rules, scrub_counts)
7087
7197
  == _scrub(ab, rules, {})):
7198
+ if decl:
7199
+ # scrub-equal IS "not observed": once declared volatiles are masked the
7200
+ # outputs are behaviorally identical, so the fix this declaration
7201
+ # describes is absent -- and letting the scrub branch swallow it hid the
7202
+ # case from every signal (fresh-review HIGH: untested interaction).
7203
+ fixture["result"] = "declared_divergence_not_observed"
7204
+ diverged.append((rec, "declared divergent (%s) but scrub-equal -- "
7205
+ "identical once volatiles are masked; the declared "
7206
+ "fix is not in the output" % decl["adr"]))
7207
+ continue
7088
7208
  # matchea SOLO tras enmascarar volatiles declarados — cuenta como
7089
7209
  # pass pero se reporta APARTE: el masking jamas es invisible.
7090
7210
  fixture["result"] = "matched_scrubbed"
7091
7211
  matched_scrubbed += 1
7212
+ elif decl:
7213
+ # diverges AND a fix verdict declared it would: expected, named, never silent.
7214
+ fixture["result"] = "expected_divergence"
7215
+ fixture["divergence_adr"] = decl["adr"]
7216
+ expected_diverged += 1
7092
7217
  else:
7093
7218
  fixture["result"] = "diverged"
7094
7219
  diverged.append((rec, "diff NO aprobado contra .approved"))
7095
7220
 
7221
+ unconsumed = sorted(k for k in divergences if k not in consumed_declarations)
7096
7222
  passed = len(diverged) == 0
7097
7223
  # zero fixtures is NOT-RUN, never CLEAN: a comparison that had nothing to
7098
7224
  # compare is absent evidence — log it as not-run (absence advises, a present
@@ -7114,6 +7240,8 @@ def cmd_golden_diff(args):
7114
7240
  "scrub_rules": len(rules),
7115
7241
  "scrub_substitutions": scrub_counts,
7116
7242
  "golden_labels": _golden_label_counts(fixtures),
7243
+ "expected_diverged": expected_diverged,
7244
+ "unconsumed_declarations": unconsumed,
7117
7245
  "fixtures": fixtures,
7118
7246
  "diverged": [{"file": f, "reason": r} for f, r in diverged],
7119
7247
  }, indent=2, ensure_ascii=False))
@@ -7551,6 +7679,13 @@ def build_parser():
7551
7679
  pcu.add_argument("--json", action="store_true")
7552
7680
  pcu.set_defaults(func=cmd_curation_check)
7553
7681
 
7682
+ prt = sub.add_parser("roundtrip",
7683
+ help="advisory: which promoted candidates are traceable in code via uscha-spec ids (ADR-009 slice 2)")
7684
+ prt.add_argument("--ledger", default="QA-LEDGER.json")
7685
+ prt.add_argument("--repo", required=True)
7686
+ prt.add_argument("--json", action="store_true")
7687
+ prt.set_defaults(func=cmd_roundtrip)
7688
+
7554
7689
  pcr = sub.add_parser("cleanroom",
7555
7690
  help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
7556
7691
  pcr.add_argument("--ledger", default="QA-LEDGER.json")
@@ -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.65.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.65.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.65.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.65.0
@@ -0,0 +1 @@
1
+ {"AC-RD-08": true, "AC-RD-09": true, "AC-RD-10": true, "AC-RD-11": true}
@@ -3749,6 +3749,62 @@ def _curation_state(repo_path):
3749
3749
  return state
3750
3750
 
3751
3751
 
3752
+ def cmd_roundtrip(args):
3753
+ """Advisory spec-id coverage (ADR-009 slice 2, v1): which PROMOTED candidates are
3754
+ traceable in the code via an embedded `uscha-spec: <candidate>` marker. Coverage by id,
3755
+ deliberately NOT semantic matching -- that stays out of scope until it can be measured
3756
+ (ADR-011). Advisory end to end: exit 0 always, a report, never a gate."""
3757
+ ledger = _load(args.ledger)
3758
+ _repo_node(ledger, args.repo)
3759
+ repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3760
+ st = _curation_state(repo_path)
3761
+ if st is None:
3762
+ print("ROUNDTRIP %s: no %s/ directory -- feature unused, nothing to trace."
3763
+ % (args.repo, CANDIDATE_DIR))
3764
+ sys.exit(0)
3765
+ promoted = sorted(st["promote_as_is"] + st["promote_with_declared_divergence"])
3766
+ ls = subprocess.run(["git", "ls-files"], cwd=repo_path, capture_output=True,
3767
+ text=True, encoding="utf-8", errors="replace")
3768
+ tracked = [l.strip() for l in ls.stdout.splitlines()
3769
+ if ls.returncode == 0 and l.strip()
3770
+ and not l.strip().startswith(CANDIDATE_DIR + "/")
3771
+ and os.path.basename(l.strip()) != BEHAVIOR_LEDGER_FILE]
3772
+ found = set()
3773
+ pat = re.compile(r"uscha-spec:\s*([\w.\-]+)")
3774
+ ROUNDTRIP_MAX_BYTES = 2 * 1024 * 1024
3775
+ for f in tracked:
3776
+ full = os.path.join(repo_path, f)
3777
+ try:
3778
+ if os.path.getsize(full) > ROUNDTRIP_MAX_BYTES:
3779
+ continue # a 2MB+ tracked file is not where a spec-id marker lives; an
3780
+ # unbounded full-tree read is the T112 lesson, applied here
3781
+ with open(full, encoding="utf-8", errors="replace") as fh:
3782
+ body = fh.read()
3783
+ except OSError:
3784
+ continue
3785
+ for m in pat.finditer(body):
3786
+ mid = m.group(1)
3787
+ found.add(mid if mid.endswith(".md") else mid + ".md")
3788
+ covered = [c for c in promoted if c in found]
3789
+ missing = [c for c in promoted if c not in found]
3790
+ out = {"repo": args.repo, "promoted": len(promoted), "covered": len(covered),
3791
+ "missing": missing, "advisory": True,
3792
+ "coverage_pct": round(100.0 * len(covered) / len(promoted), 1) if promoted else None}
3793
+ if args.json:
3794
+ print(json.dumps(out, indent=2, ensure_ascii=False))
3795
+ else:
3796
+ if not promoted:
3797
+ print("ROUNDTRIP %s: no promoted candidates yet -- nothing to trace (advisory)."
3798
+ % args.repo)
3799
+ else:
3800
+ print("ROUNDTRIP %s: %d/%d promoted candidate(s) traceable by uscha-spec id "
3801
+ "(advisory)" % (args.repo, len(covered), len(promoted)))
3802
+ for mss in missing:
3803
+ print(" .. %s: no uscha-spec marker found in the code" % mss)
3804
+ sys.exit(0)
3805
+
3806
+
3807
+
3752
3808
  def cmd_curation_check(args):
3753
3809
  """The INV-CURATION-01 gate, measured. Exit 2: malformation or tampering (config-error
3754
3810
  class -- candidates that cannot be validated, a ledger that cannot be trusted). Exit 1:
@@ -6933,6 +6989,34 @@ def _golden_approved_path(rec):
6933
6989
 
6934
6990
 
6935
6991
  GOLDEN_SCRUB_FILE = "golden.scrub.json"
6992
+ GOLDEN_DIVERGENCES_FILE = "golden.divergences.json"
6993
+
6994
+
6995
+ def _load_golden_divergences(root):
6996
+ """Expected divergences for `fix` verdicts (ADR-009 slice 2): a golden that MUST differ
6997
+ because its ADR says the behavior was corrected. Shape:
6998
+ {"divergences": {"<fixture basename>": {"adr": "ADR-RD-NNN", "reason": "..."}}}.
6999
+ Strict like the scrub rules: a typo must not degrade into "no declarations" -- that
7000
+ silence would turn every expected divergence back into a blocker, or worse, hide a
7001
+ declared one behind a malformed file. Absent file -> {} (nothing declared)."""
7002
+ path = os.path.join(root, GOLDEN_DIVERGENCES_FILE)
7003
+ if not os.path.isfile(path):
7004
+ return {}
7005
+ try:
7006
+ with open(path, "r", encoding="utf-8") as fh:
7007
+ spec = json.load(fh)
7008
+ if not isinstance(spec, dict) or not isinstance(spec.get("divergences"), dict):
7009
+ raise TypeError('expected {"divergences": {"<fixture>": {"adr":..., "reason":...}}}')
7010
+ for k, v in spec["divergences"].items():
7011
+ if (not isinstance(v, dict) or not re.match(r"^ADR-\S+$", str(v.get("adr", "")))
7012
+ or not str(v.get("reason", "")).strip()):
7013
+ raise TypeError("divergence %r needs adr (ADR-...) and a reason" % k)
7014
+ return spec["divergences"]
7015
+ except (json.JSONDecodeError, TypeError, KeyError) as exc:
7016
+ print("[qa_ledger] %s invalid (%s) - declared divergences are not skipped in "
7017
+ "silence: fix the file or delete it." % (path, exc), file=sys.stderr)
7018
+ sys.exit(2)
7019
+
6936
7020
 
6937
7021
 
6938
7022
  def _load_scrub_rules(root):
@@ -7050,6 +7134,9 @@ def cmd_golden_diff(args):
7050
7134
  received = [p for p in sorted(hits) if os.path.isfile(p)] # skip dirs matched by glob
7051
7135
  rules = _load_scrub_rules(root)
7052
7136
  labels = _load_golden_labels(getattr(args, "labels", None))
7137
+ divergences = _load_golden_divergences(root)
7138
+ expected_diverged = 0
7139
+ consumed_declarations = set()
7053
7140
  scrub_counts = {}
7054
7141
  diverged = [] # (received_path, reason)
7055
7142
  fixtures = []
@@ -7078,21 +7165,60 @@ def cmd_golden_diff(args):
7078
7165
  fixture["result"] = "read_error"
7079
7166
  diverged.append((rec, f"could not read: {exc}"))
7080
7167
  continue
7168
+ decl, decl_key = None, None
7169
+ for cand_key in (os.path.relpath(app, root).replace(os.sep, "/"),
7170
+ os.path.relpath(rec, root).replace(os.sep, "/"),
7171
+ os.path.basename(app), os.path.basename(rec)):
7172
+ # relpath first (the _golden_label pattern: nested suites share basenames and a
7173
+ # declaration must not launder an unrelated module\x27s divergence -- fresh-review
7174
+ # finding); basename stays as the flat-layout convenience.
7175
+ if cand_key in divergences:
7176
+ decl, decl_key = divergences[cand_key], cand_key
7177
+ break
7178
+ if decl:
7179
+ consumed_declarations.add(decl_key) # only what MATCHED: an unexercised twin
7180
+ # key must still show as unconsumed
7081
7181
  if rb == ab:
7182
+ if decl:
7183
+ # a `fix` verdict DECLARED this golden must differ -- identical bytes mean
7184
+ # the corrected behavior never landed. An expected divergence that is not
7185
+ # observed is a red finding, not a quiet pass (ADR-010: fix cases must
7186
+ # diverge exactly as their ADR describes; identical is not that).
7187
+ fixture["result"] = "declared_divergence_not_observed"
7188
+ diverged.append((rec, "declared divergent (%s) but IDENTICAL -- the fix "
7189
+ "this declaration describes is not in the output"
7190
+ % decl["adr"]))
7191
+ continue
7082
7192
  fixture["result"] = "matched"
7083
7193
  matched += 1
7084
7194
  # el conteo reportado es del lado RECEIVED (la captura fresca) — sumar
7085
7195
  # ambos lados duplicaria cada volatil enmascarado en el reporte.
7086
7196
  elif rules and (_scrub(rb, rules, scrub_counts)
7087
7197
  == _scrub(ab, rules, {})):
7198
+ if decl:
7199
+ # scrub-equal IS "not observed": once declared volatiles are masked the
7200
+ # outputs are behaviorally identical, so the fix this declaration
7201
+ # describes is absent -- and letting the scrub branch swallow it hid the
7202
+ # case from every signal (fresh-review HIGH: untested interaction).
7203
+ fixture["result"] = "declared_divergence_not_observed"
7204
+ diverged.append((rec, "declared divergent (%s) but scrub-equal -- "
7205
+ "identical once volatiles are masked; the declared "
7206
+ "fix is not in the output" % decl["adr"]))
7207
+ continue
7088
7208
  # matchea SOLO tras enmascarar volatiles declarados — cuenta como
7089
7209
  # pass pero se reporta APARTE: el masking jamas es invisible.
7090
7210
  fixture["result"] = "matched_scrubbed"
7091
7211
  matched_scrubbed += 1
7212
+ elif decl:
7213
+ # diverges AND a fix verdict declared it would: expected, named, never silent.
7214
+ fixture["result"] = "expected_divergence"
7215
+ fixture["divergence_adr"] = decl["adr"]
7216
+ expected_diverged += 1
7092
7217
  else:
7093
7218
  fixture["result"] = "diverged"
7094
7219
  diverged.append((rec, "diff NO aprobado contra .approved"))
7095
7220
 
7221
+ unconsumed = sorted(k for k in divergences if k not in consumed_declarations)
7096
7222
  passed = len(diverged) == 0
7097
7223
  # zero fixtures is NOT-RUN, never CLEAN: a comparison that had nothing to
7098
7224
  # compare is absent evidence — log it as not-run (absence advises, a present
@@ -7114,6 +7240,8 @@ def cmd_golden_diff(args):
7114
7240
  "scrub_rules": len(rules),
7115
7241
  "scrub_substitutions": scrub_counts,
7116
7242
  "golden_labels": _golden_label_counts(fixtures),
7243
+ "expected_diverged": expected_diverged,
7244
+ "unconsumed_declarations": unconsumed,
7117
7245
  "fixtures": fixtures,
7118
7246
  "diverged": [{"file": f, "reason": r} for f, r in diverged],
7119
7247
  }, indent=2, ensure_ascii=False))
@@ -7551,6 +7679,13 @@ def build_parser():
7551
7679
  pcu.add_argument("--json", action="store_true")
7552
7680
  pcu.set_defaults(func=cmd_curation_check)
7553
7681
 
7682
+ prt = sub.add_parser("roundtrip",
7683
+ help="advisory: which promoted candidates are traceable in code via uscha-spec ids (ADR-009 slice 2)")
7684
+ prt.add_argument("--ledger", default="QA-LEDGER.json")
7685
+ prt.add_argument("--repo", required=True)
7686
+ prt.add_argument("--json", action="store_true")
7687
+ prt.set_defaults(func=cmd_roundtrip)
7688
+
7554
7689
  pcr = sub.add_parser("cleanroom",
7555
7690
  help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
7556
7691
  pcr.add_argument("--ledger", default="QA-LEDGER.json")
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.64.0",
2
+ "version": "1.65.0",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,