@andresmassello/uscha 1.63.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 +423 -0
- package/uscha-kit/.claude/skills/uscha-reverse-discovery/SKILL.md +65 -24
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +39 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/reports/junit/.curation-cases.json +1 -0
- package/uscha-kit/reports/junit/.oracle-cases.json +1 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +423 -0
- package/uscha-kit/skills/uscha-reverse-discovery/SKILL.md +65 -24
- package/uscha-kit/templates/BEHAVIOR-LEDGER.md +13 -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",
|
|
@@ -2634,6 +2634,29 @@ def _derive_phase(ledger, name, node, k, qa_order):
|
|
|
2634
2634
|
# requires clean-room evidence that is GREEN and pinned to the CURRENT HEAD. A new commit
|
|
2635
2635
|
# makes the previous run stale for the gate, the same staleness posture the rest of the
|
|
2636
2636
|
# engine takes: evidence certifies the thing it was measured against, nothing later.
|
|
2637
|
+
# curation gate (ADR-009, INV-CURATION-01). In use the moment discovery/ exists --
|
|
2638
|
+
# creating candidates IS the opt-in. Fail-closed both ways: an unjudged candidate blocks,
|
|
2639
|
+
# and a malformation blocks too, because "could not validate" must never read as judged.
|
|
2640
|
+
_cu_cfg = (_repo_cfg(ledger, name) if name != "integration"
|
|
2641
|
+
else {"path": "."}) # synthetic scope: never in config["repos"]
|
|
2642
|
+
_cu = _curation_state(_cu_cfg.get("path", "."))
|
|
2643
|
+
if _cu is not None:
|
|
2644
|
+
if _cu["malformed"] or _cu["ledger_errors"] or _cu["append_only"] == "violation":
|
|
2645
|
+
_bits = [m["candidate"] for m in _cu["malformed"][:3]]
|
|
2646
|
+
if _cu["ledger_errors"]:
|
|
2647
|
+
_bits.append(BEHAVIOR_LEDGER_FILE + " malformado")
|
|
2648
|
+
if _cu["append_only"] == "violation":
|
|
2649
|
+
_bits.append(BEHAVIOR_LEDGER_FILE + " editado (append-only)")
|
|
2650
|
+
reasons.append("curation invalida: " + "; ".join(_bits)
|
|
2651
|
+
+ " -- corregir antes de avanzar")
|
|
2652
|
+
conv = False
|
|
2653
|
+
elif _cu["unjudged"]:
|
|
2654
|
+
reasons.append("candidata(s) sin veredicto humano: "
|
|
2655
|
+
+ ", ".join(_cu["unjudged"][:3])
|
|
2656
|
+
+ (" (+%d)" % (len(_cu["unjudged"]) - 3)
|
|
2657
|
+
if len(_cu["unjudged"]) > 3 else "")
|
|
2658
|
+
+ " -- INV-CURATION-01: sin juicio no hay promocion")
|
|
2659
|
+
conv = False
|
|
2637
2660
|
_cr = _cr_cfg(ledger)
|
|
2638
2661
|
if _cr and _cr.get("mode") == "final":
|
|
2639
2662
|
_head = None
|
|
@@ -3510,6 +3533,320 @@ def cmd_cleanroom(args):
|
|
|
3510
3533
|
|
|
3511
3534
|
|
|
3512
3535
|
|
|
3536
|
+
# --------------------------------------------------------------------------- #
|
|
3537
|
+
# curation (ADR-009/010: candidates in quarantine, verdicts in the behavior
|
|
3538
|
+
# ledger, and a promotion gate the ENGINE measures -- INV-CURATION-01)
|
|
3539
|
+
# --------------------------------------------------------------------------- #
|
|
3540
|
+
|
|
3541
|
+
BEHAVIOR_LEDGER_FILE = "BEHAVIOR-LEDGER.md"
|
|
3542
|
+
CANDIDATE_DIR = "discovery"
|
|
3543
|
+
_BL_VERDICTS = ("preserve", "fix", "undefined")
|
|
3544
|
+
|
|
3545
|
+
|
|
3546
|
+
def _parse_candidate(path):
|
|
3547
|
+
"""Parse one candidate's frontmatter. Returns (data, errors); a candidate with errors
|
|
3548
|
+
is INVALID and named -- never silently skipped, because a skipped candidate would walk
|
|
3549
|
+
past the promotion gate unjudged."""
|
|
3550
|
+
errors = []
|
|
3551
|
+
try:
|
|
3552
|
+
# utf-8-sig: a BOM-adding editor must not turn a well-formed candidate into a
|
|
3553
|
+
# false "no frontmatter" (fresh-review LOW)
|
|
3554
|
+
with open(path, encoding="utf-8-sig", errors="replace") as fh:
|
|
3555
|
+
lines = fh.read().splitlines()
|
|
3556
|
+
except OSError as exc:
|
|
3557
|
+
return None, ["unreadable: %s" % exc]
|
|
3558
|
+
if not lines or lines[0].strip() != "---":
|
|
3559
|
+
return None, ["no frontmatter (evidence/confidence are mandatory, ADR-009)"]
|
|
3560
|
+
fm, i = [], 1
|
|
3561
|
+
while i < len(lines) and lines[i].strip() != "---":
|
|
3562
|
+
fm.append(lines[i]); i += 1
|
|
3563
|
+
if i >= len(lines):
|
|
3564
|
+
return None, ["frontmatter never closes"]
|
|
3565
|
+
etype, refs, conf, in_refs, in_evidence = None, [], None, False, False
|
|
3566
|
+
for ln in fm:
|
|
3567
|
+
s = ln.strip()
|
|
3568
|
+
indented = ln.startswith((" ", "\t"))
|
|
3569
|
+
if s.startswith("evidence:") and not indented:
|
|
3570
|
+
in_evidence = True; in_refs = False
|
|
3571
|
+
elif s.startswith("type:"):
|
|
3572
|
+
# scope + duplicates are MALFORMATION, not last-value-wins: a stray top-level
|
|
3573
|
+
# type:/confidence: after the evidence block silently overrode the nested one
|
|
3574
|
+
# and walked straight past the inference->low invariant (fresh-review HIGH).
|
|
3575
|
+
if not (in_evidence and indented):
|
|
3576
|
+
errors.append("type: outside the evidence block")
|
|
3577
|
+
elif etype is not None:
|
|
3578
|
+
errors.append("duplicate type: declaration")
|
|
3579
|
+
else:
|
|
3580
|
+
etype = s[len("type:"):].strip().strip("\x27\x22")
|
|
3581
|
+
in_refs = False
|
|
3582
|
+
elif s.startswith("refs:"):
|
|
3583
|
+
if not (in_evidence and indented):
|
|
3584
|
+
errors.append("refs: outside the evidence block")
|
|
3585
|
+
in_refs = in_evidence and indented
|
|
3586
|
+
elif s.startswith("confidence:") and not indented:
|
|
3587
|
+
if conf is not None:
|
|
3588
|
+
errors.append("duplicate confidence: declaration")
|
|
3589
|
+
else:
|
|
3590
|
+
conf = s[len("confidence:"):].strip().strip("\x27\x22")
|
|
3591
|
+
in_refs = False; in_evidence = False
|
|
3592
|
+
elif in_refs and s.startswith("- "):
|
|
3593
|
+
refs.append(s[2:].strip().strip("\x27\x22"))
|
|
3594
|
+
elif s and not indented:
|
|
3595
|
+
in_refs = False; in_evidence = False
|
|
3596
|
+
if etype not in ("test", "code", "inference"):
|
|
3597
|
+
errors.append("evidence.type %r (expected test|code|inference)" % etype)
|
|
3598
|
+
if not refs:
|
|
3599
|
+
errors.append("evidence.refs is empty (a candidate without evidence is a guess)")
|
|
3600
|
+
if conf not in ("high", "medium", "low"):
|
|
3601
|
+
errors.append("confidence %r (expected high|medium|low)" % conf)
|
|
3602
|
+
if etype == "inference" and conf != "low":
|
|
3603
|
+
errors.append("inference is ALWAYS low confidence (ADR-009); %r declared" % conf)
|
|
3604
|
+
return {"type": etype, "refs": refs, "confidence": conf}, errors
|
|
3605
|
+
|
|
3606
|
+
|
|
3607
|
+
def _resolve_ref(repo_path, ref):
|
|
3608
|
+
"""A ref must point at something REAL: `path`, `path:N`, `path:N-M` or `path#name`.
|
|
3609
|
+
Returns None when it resolves, else the reason."""
|
|
3610
|
+
frag = None
|
|
3611
|
+
if "#" in ref:
|
|
3612
|
+
ref, frag = ref.split("#", 1)
|
|
3613
|
+
span = None
|
|
3614
|
+
m = re.match(r"^(.*?):(\d+)(?:-(\d+))?$", ref)
|
|
3615
|
+
if m:
|
|
3616
|
+
ref = m.group(1)
|
|
3617
|
+
span = (int(m.group(2)), int(m.group(3) or m.group(2)))
|
|
3618
|
+
full = os.path.join(repo_path, ref.replace("/", os.sep))
|
|
3619
|
+
if _gc_rel(full, repo_path) is None:
|
|
3620
|
+
# an absolute path makes os.path.join DISCARD repo_path entirely, and ../ walks
|
|
3621
|
+
# out -- either way the "evidence" would point outside the legacy tree it claims
|
|
3622
|
+
# to evidence (fresh-review HIGH). Confinement is part of resolution.
|
|
3623
|
+
return "ref escapes the repo tree: %s" % ref
|
|
3624
|
+
if not os.path.isfile(full):
|
|
3625
|
+
return "file not found: %s" % ref
|
|
3626
|
+
if span or frag:
|
|
3627
|
+
try:
|
|
3628
|
+
with open(full, encoding="utf-8", errors="replace") as fh:
|
|
3629
|
+
body = fh.read()
|
|
3630
|
+
except OSError as exc:
|
|
3631
|
+
return "unreadable: %s" % exc
|
|
3632
|
+
if span:
|
|
3633
|
+
n = body.count("\n") + 1
|
|
3634
|
+
if span[0] < 1 or span[1] > n or span[0] > span[1]:
|
|
3635
|
+
return "lines %d-%d out of range (file has %d)" % (span[0], span[1], n)
|
|
3636
|
+
if frag and frag not in body:
|
|
3637
|
+
return "fragment %r not found in %s" % (frag, ref)
|
|
3638
|
+
return None
|
|
3639
|
+
|
|
3640
|
+
|
|
3641
|
+
def _load_behavior_ledger(path):
|
|
3642
|
+
"""Strict parse of the verdict table. Returns (rows, errors). Malformed is an ERROR,
|
|
3643
|
+
never a degrade: under the promotion gate, a silent "no verdicts" would UNBLOCK exactly
|
|
3644
|
+
what the gate guards (same posture as golden.scrub.json)."""
|
|
3645
|
+
rows, errors = [], []
|
|
3646
|
+
try:
|
|
3647
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
3648
|
+
lines = fh.read().splitlines()
|
|
3649
|
+
except OSError as exc:
|
|
3650
|
+
return [], ["unreadable: %s" % exc]
|
|
3651
|
+
for n, ln in enumerate(lines, 1):
|
|
3652
|
+
s = ln.strip()
|
|
3653
|
+
if not s.startswith("|"):
|
|
3654
|
+
continue
|
|
3655
|
+
cells = [c.strip() for c in s.strip("|").split("|")]
|
|
3656
|
+
if cells and all(c and set(c) <= set("-: ") for c in cells):
|
|
3657
|
+
continue # separator row (empty cells are NOT)
|
|
3658
|
+
low = [c.lower() for c in cells]
|
|
3659
|
+
if "candidate" in low and "verdict" in low:
|
|
3660
|
+
continue # header row
|
|
3661
|
+
if len(cells) != 6:
|
|
3662
|
+
errors.append("line %d: %d cells, expected 6 (# | candidate | evidence | "
|
|
3663
|
+
"confidence | verdict | adr)" % (n, len(cells)))
|
|
3664
|
+
continue
|
|
3665
|
+
_, cand, _ev, _conf, verdict, adr = cells
|
|
3666
|
+
if verdict not in _BL_VERDICTS:
|
|
3667
|
+
errors.append("line %d: verdict %r is not one of %s -- a fourth state is "
|
|
3668
|
+
"malformation, not an option" % (n, verdict, "/".join(_BL_VERDICTS)))
|
|
3669
|
+
if not re.match(r"^ADR-\S+$", adr):
|
|
3670
|
+
errors.append("line %d: adr ref %r -- no verdict without its why (ADR-010)"
|
|
3671
|
+
% (n, adr))
|
|
3672
|
+
if not cand:
|
|
3673
|
+
errors.append("line %d: empty candidate" % n)
|
|
3674
|
+
rows.append({"candidate": cand, "verdict": verdict, "adr": adr, "line": n})
|
|
3675
|
+
return rows, errors
|
|
3676
|
+
|
|
3677
|
+
|
|
3678
|
+
def _bl_append_only(repo_path, rel):
|
|
3679
|
+
"""The rows in HEAD must be a byte-identical prefix of the working file. Deliberately
|
|
3680
|
+
blunt (ADR-010): an audit trail that tolerates rewriting is not an audit trail.
|
|
3681
|
+
Returns "ok" | "new" | "violation" | "unmeasured"."""
|
|
3682
|
+
try:
|
|
3683
|
+
# probe the repo FIRST: "git failed entirely" and "file not in HEAD yet" are
|
|
3684
|
+
# different answers, and conflating them turned no-git into a silent "new"
|
|
3685
|
+
# (caught by T120's AC-RD-05 -- unmeasured must never be mistaken for anything).
|
|
3686
|
+
probe = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repo_path,
|
|
3687
|
+
capture_output=True)
|
|
3688
|
+
if probe.returncode != 0:
|
|
3689
|
+
return "unmeasured"
|
|
3690
|
+
r = subprocess.run(["git", "show", "HEAD:" + rel.replace(os.sep, "/")],
|
|
3691
|
+
cwd=repo_path, capture_output=True)
|
|
3692
|
+
except OSError:
|
|
3693
|
+
return "unmeasured"
|
|
3694
|
+
if r.returncode != 0:
|
|
3695
|
+
return "new" # not in HEAD yet
|
|
3696
|
+
try:
|
|
3697
|
+
with open(os.path.join(repo_path, rel), "rb") as fh:
|
|
3698
|
+
cur = fh.read()
|
|
3699
|
+
except OSError:
|
|
3700
|
+
return "violation" # in HEAD but gone from the tree
|
|
3701
|
+
# normalize line endings on BOTH sides: with core.autocrlf, git stores LF and checks out
|
|
3702
|
+
# CRLF, so a raw byte compare reads that translation as tampering on every Windows box
|
|
3703
|
+
# (found by the first probe). Git itself treats line endings as non-content; so do we.
|
|
3704
|
+
# A verdict edit still cannot hide in a CRLF flip.
|
|
3705
|
+
cur_n = cur.replace(b"\r\n", b"\n")
|
|
3706
|
+
head_n = r.stdout.replace(b"\r\n", b"\n")
|
|
3707
|
+
return "ok" if cur_n.startswith(head_n) else "violation"
|
|
3708
|
+
|
|
3709
|
+
|
|
3710
|
+
def _curation_state(repo_path):
|
|
3711
|
+
"""Everything the gate needs, from one scan. None = feature unused (no discovery/):
|
|
3712
|
+
behavior identical to a release where this code does not exist (AC-RD-07)."""
|
|
3713
|
+
disc = os.path.join(repo_path, CANDIDATE_DIR)
|
|
3714
|
+
if not os.path.isdir(disc):
|
|
3715
|
+
return None
|
|
3716
|
+
cands = sorted(f for f in os.listdir(disc) if f.lower().endswith(".md"))
|
|
3717
|
+
state = {"candidates": [], "malformed": [], "ledger_errors": [],
|
|
3718
|
+
"append_only": None, "unjudged": [], "promote_as_is": [],
|
|
3719
|
+
"promote_with_declared_divergence": [], "excluded": []}
|
|
3720
|
+
for f in cands:
|
|
3721
|
+
data, errs = _parse_candidate(os.path.join(disc, f))
|
|
3722
|
+
if data:
|
|
3723
|
+
for ref in data["refs"]:
|
|
3724
|
+
bad = _resolve_ref(repo_path, ref)
|
|
3725
|
+
if bad:
|
|
3726
|
+
errs.append("ref %r: %s" % (ref, bad))
|
|
3727
|
+
if errs:
|
|
3728
|
+
state["malformed"].append({"candidate": f, "errors": errs})
|
|
3729
|
+
else:
|
|
3730
|
+
state["candidates"].append(f)
|
|
3731
|
+
lpath = os.path.join(repo_path, BEHAVIOR_LEDGER_FILE)
|
|
3732
|
+
verdicts = {}
|
|
3733
|
+
if os.path.isfile(lpath):
|
|
3734
|
+
rows, lerrs = _load_behavior_ledger(lpath)
|
|
3735
|
+
state["ledger_errors"] = lerrs
|
|
3736
|
+
state["append_only"] = _bl_append_only(repo_path, BEHAVIOR_LEDGER_FILE)
|
|
3737
|
+
for row in rows:
|
|
3738
|
+
verdicts[row["candidate"]] = row["verdict"] # append-only: the LATEST row wins
|
|
3739
|
+
for f in state["candidates"]:
|
|
3740
|
+
v = verdicts.get(f)
|
|
3741
|
+
if v is None:
|
|
3742
|
+
state["unjudged"].append(f)
|
|
3743
|
+
elif v == "preserve":
|
|
3744
|
+
state["promote_as_is"].append(f)
|
|
3745
|
+
elif v == "fix":
|
|
3746
|
+
state["promote_with_declared_divergence"].append(f)
|
|
3747
|
+
else:
|
|
3748
|
+
state["excluded"].append(f)
|
|
3749
|
+
return state
|
|
3750
|
+
|
|
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
|
+
|
|
3808
|
+
def cmd_curation_check(args):
|
|
3809
|
+
"""The INV-CURATION-01 gate, measured. Exit 2: malformation or tampering (config-error
|
|
3810
|
+
class -- candidates that cannot be validated, a ledger that cannot be trusted). Exit 1:
|
|
3811
|
+
valid candidates awaiting a human verdict (the quarantine holding). Exit 0: every
|
|
3812
|
+
candidate judged, or the feature unused."""
|
|
3813
|
+
ledger = _load(args.ledger)
|
|
3814
|
+
_repo_node(ledger, args.repo)
|
|
3815
|
+
repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
|
|
3816
|
+
st = _curation_state(repo_path)
|
|
3817
|
+
if st is None:
|
|
3818
|
+
if args.json:
|
|
3819
|
+
print(json.dumps({"repo": args.repo, "in_use": False}))
|
|
3820
|
+
else:
|
|
3821
|
+
print("CURATION %s: no %s/ directory -- feature unused, nothing to gate."
|
|
3822
|
+
% (args.repo, CANDIDATE_DIR))
|
|
3823
|
+
sys.exit(0)
|
|
3824
|
+
out = dict(st)
|
|
3825
|
+
out.update({"repo": args.repo, "in_use": True})
|
|
3826
|
+
hard = bool(st["malformed"] or st["ledger_errors"]
|
|
3827
|
+
or st["append_only"] == "violation")
|
|
3828
|
+
if args.json:
|
|
3829
|
+
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
3830
|
+
else:
|
|
3831
|
+
print("CURATION %s: %d candidate(s), %d judged, %d awaiting verdict"
|
|
3832
|
+
% (args.repo, len(st["candidates"]) + len(st["malformed"]),
|
|
3833
|
+
len(st["promote_as_is"]) + len(st["promote_with_declared_divergence"])
|
|
3834
|
+
+ len(st["excluded"]), len(st["unjudged"])))
|
|
3835
|
+
for m in st["malformed"]:
|
|
3836
|
+
print(" !! %s: %s" % (m["candidate"], "; ".join(m["errors"])))
|
|
3837
|
+
for e in st["ledger_errors"]:
|
|
3838
|
+
print(" !! %s: %s" % (BEHAVIOR_LEDGER_FILE, e))
|
|
3839
|
+
if st["append_only"] == "violation":
|
|
3840
|
+
print(" !! %s: existing rows were EDITED -- append-only violated; revert and "
|
|
3841
|
+
"add a new row + ADR instead" % BEHAVIOR_LEDGER_FILE)
|
|
3842
|
+
elif st["append_only"] == "unmeasured":
|
|
3843
|
+
print(" -- append-only: UNMEASURED (no git) -- reported, never claimed as pass")
|
|
3844
|
+
for f in st["unjudged"]:
|
|
3845
|
+
print(" .. %s: awaiting human verdict (blocks forward)" % f)
|
|
3846
|
+
sys.exit(2 if hard else (1 if st["unjudged"] else 0))
|
|
3847
|
+
|
|
3848
|
+
|
|
3849
|
+
|
|
3513
3850
|
def cmd_escalate(args):
|
|
3514
3851
|
ledger = _load(args.ledger)
|
|
3515
3852
|
_repo_node(ledger, args.repo)
|
|
@@ -6652,6 +6989,34 @@ def _golden_approved_path(rec):
|
|
|
6652
6989
|
|
|
6653
6990
|
|
|
6654
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
|
+
|
|
6655
7020
|
|
|
6656
7021
|
|
|
6657
7022
|
def _load_scrub_rules(root):
|
|
@@ -6769,6 +7134,9 @@ def cmd_golden_diff(args):
|
|
|
6769
7134
|
received = [p for p in sorted(hits) if os.path.isfile(p)] # skip dirs matched by glob
|
|
6770
7135
|
rules = _load_scrub_rules(root)
|
|
6771
7136
|
labels = _load_golden_labels(getattr(args, "labels", None))
|
|
7137
|
+
divergences = _load_golden_divergences(root)
|
|
7138
|
+
expected_diverged = 0
|
|
7139
|
+
consumed_declarations = set()
|
|
6772
7140
|
scrub_counts = {}
|
|
6773
7141
|
diverged = [] # (received_path, reason)
|
|
6774
7142
|
fixtures = []
|
|
@@ -6797,21 +7165,60 @@ def cmd_golden_diff(args):
|
|
|
6797
7165
|
fixture["result"] = "read_error"
|
|
6798
7166
|
diverged.append((rec, f"could not read: {exc}"))
|
|
6799
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
|
|
6800
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
|
|
6801
7192
|
fixture["result"] = "matched"
|
|
6802
7193
|
matched += 1
|
|
6803
7194
|
# el conteo reportado es del lado RECEIVED (la captura fresca) — sumar
|
|
6804
7195
|
# ambos lados duplicaria cada volatil enmascarado en el reporte.
|
|
6805
7196
|
elif rules and (_scrub(rb, rules, scrub_counts)
|
|
6806
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
|
|
6807
7208
|
# matchea SOLO tras enmascarar volatiles declarados — cuenta como
|
|
6808
7209
|
# pass pero se reporta APARTE: el masking jamas es invisible.
|
|
6809
7210
|
fixture["result"] = "matched_scrubbed"
|
|
6810
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
|
|
6811
7217
|
else:
|
|
6812
7218
|
fixture["result"] = "diverged"
|
|
6813
7219
|
diverged.append((rec, "diff NO aprobado contra .approved"))
|
|
6814
7220
|
|
|
7221
|
+
unconsumed = sorted(k for k in divergences if k not in consumed_declarations)
|
|
6815
7222
|
passed = len(diverged) == 0
|
|
6816
7223
|
# zero fixtures is NOT-RUN, never CLEAN: a comparison that had nothing to
|
|
6817
7224
|
# compare is absent evidence — log it as not-run (absence advises, a present
|
|
@@ -6833,6 +7240,8 @@ def cmd_golden_diff(args):
|
|
|
6833
7240
|
"scrub_rules": len(rules),
|
|
6834
7241
|
"scrub_substitutions": scrub_counts,
|
|
6835
7242
|
"golden_labels": _golden_label_counts(fixtures),
|
|
7243
|
+
"expected_diverged": expected_diverged,
|
|
7244
|
+
"unconsumed_declarations": unconsumed,
|
|
6836
7245
|
"fixtures": fixtures,
|
|
6837
7246
|
"diverged": [{"file": f, "reason": r} for f, r in diverged],
|
|
6838
7247
|
}, indent=2, ensure_ascii=False))
|
|
@@ -7263,6 +7672,20 @@ def build_parser():
|
|
|
7263
7672
|
pfp.add_argument("--json", action="store_true")
|
|
7264
7673
|
pfp.set_defaults(func=cmd_fastpath_eval)
|
|
7265
7674
|
|
|
7675
|
+
pcu = sub.add_parser("curation-check",
|
|
7676
|
+
help="the INV-CURATION-01 gate: candidates, verdicts, append-only ledger (ADR-009/010)")
|
|
7677
|
+
pcu.add_argument("--ledger", default="QA-LEDGER.json")
|
|
7678
|
+
pcu.add_argument("--repo", required=True)
|
|
7679
|
+
pcu.add_argument("--json", action="store_true")
|
|
7680
|
+
pcu.set_defaults(func=cmd_curation_check)
|
|
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
|
+
|
|
7266
7689
|
pcr = sub.add_parser("cleanroom",
|
|
7267
7690
|
help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
|
|
7268
7691
|
pcr.add_argument("--ledger", default="QA-LEDGER.json")
|