@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 +2 -2
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +135 -0
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +20 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/reports/junit/.oracle-cases.json +1 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +135 -0
- package/uscha-kit/uscha.config.json +1 -1
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.
|
|
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`,
|
|
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.
|
|
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.
|
|
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,
|
|
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"
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.
|
|
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.
|
|
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")
|