@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 +2 -2
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +13 -0
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +154 -2
- package/uscha-kit/.claude/skills/uscha-status/SKILL.md +4 -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/.curation-cases.json +1 -1
- package/uscha-kit/reports/junit/.oracle-cases.json +1 -0
- package/uscha-kit/skills/uscha-devloop/SKILL.md +13 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +154 -2
- package/uscha-kit/skills/uscha-status/SKILL.md +4 -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.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`,
|
|
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.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 =
|
|
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 =
|
|
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.
|
|
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,
|
|
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.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.
|
|
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 =
|
|
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 =
|
|
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
|