@andresmassello/uscha 1.70.0 → 1.72.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 +356 -11
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +1 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/reports/junit/.fidelity-cases.json +1 -1
- package/uscha-kit/reports/junit/.ir-cases.json +1 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +356 -11
- 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.72.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`, 42 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.72.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",
|
|
@@ -4533,6 +4533,10 @@ def cmd_fidelity(args):
|
|
|
4533
4533
|
sys.exit(2)
|
|
4534
4534
|
obs = (delta.get("observations") or []) if delta else []
|
|
4535
4535
|
verdicts = _curation_verdicts(ledger, args.repo)
|
|
4536
|
+
# fidelity respects the SAME bound that produced the delta (user decision, FR-001): a
|
|
4537
|
+
# bounded discovery is measured over its own subtree, so unexplained_code and the other
|
|
4538
|
+
# mechanical dimensions never mix the delta's scope with the whole repo's.
|
|
4539
|
+
bound = delta.get("path") if delta else None
|
|
4536
4540
|
dims = {}
|
|
4537
4541
|
# traceability: canonical items reachable in code via the uscha-spec id machinery
|
|
4538
4542
|
canon_items = []
|
|
@@ -4543,7 +4547,17 @@ def cmd_fidelity(args):
|
|
|
4543
4547
|
canon_items = json.load(fh).get("items") or []
|
|
4544
4548
|
except (OSError, ValueError):
|
|
4545
4549
|
canon_items = []
|
|
4546
|
-
tracked = _tracked_files(repo_path) or []
|
|
4550
|
+
tracked = [f for f in (_tracked_files(repo_path) or []) if _under_bound(f, bound)]
|
|
4551
|
+
_scope = " (bounded to %s)" % bound if bound else ""
|
|
4552
|
+
if bound:
|
|
4553
|
+
# scope the DENOMINATOR too, not just the file scan: promote MERGES into CANONICAL
|
|
4554
|
+
# repo-wide, so an earlier unbounded promote would otherwise count out-of-bound
|
|
4555
|
+
# items as "no longer derives" -- the exact scope-mixing this release kills, half-
|
|
4556
|
+
# done if only the numerator moves (fresh-review LOW). An item is under the bound
|
|
4557
|
+
# when its primary provenance file is.
|
|
4558
|
+
canon_items = [it for it in canon_items
|
|
4559
|
+
if any(_under_bound(f.split(":")[0].split("#")[0], bound)
|
|
4560
|
+
for f in (it.get("provenance") or {}).get("files") or [])]
|
|
4547
4561
|
marked, marker_files = set(), set()
|
|
4548
4562
|
for f, mid in _scan_spec_markers(repo_path, tracked):
|
|
4549
4563
|
marked.add(mid)
|
|
@@ -4552,8 +4566,8 @@ def cmd_fidelity(args):
|
|
|
4552
4566
|
traced = sum(1 for it in canon_items if (it.get("derived_from") or "") in marked)
|
|
4553
4567
|
dims["traceability"] = _fid_dim(round(traced / len(canon_items), 4),
|
|
4554
4568
|
"uscha-spec marker scan over %d git-tracked "
|
|
4555
|
-
"files vs %d canonical item(s)"
|
|
4556
|
-
% (len(tracked), len(canon_items)))
|
|
4569
|
+
"files%s vs %d canonical item(s)"
|
|
4570
|
+
% (len(tracked), _scope, len(canon_items)))
|
|
4557
4571
|
else:
|
|
4558
4572
|
dims["traceability"] = _fid_dim(None, "UNMEASURED: no canonical items promoted yet")
|
|
4559
4573
|
# behavior: the latest ingested golden-diff gate verdict
|
|
@@ -4579,13 +4593,30 @@ def cmd_fidelity(args):
|
|
|
4579
4593
|
cur_ids = {o["id"] for o in cur_static}
|
|
4580
4594
|
okc = sum(1 for it in static_canon if it.get("derived_from") in cur_ids)
|
|
4581
4595
|
dims["contracts"] = _fid_dim(round(okc / len(static_canon), 4),
|
|
4582
|
-
"re-ran static extractors; %d/%d canonical static "
|
|
4596
|
+
"re-ran static extractors%s; %d/%d canonical static "
|
|
4583
4597
|
"item(s) still derive from the code"
|
|
4584
|
-
% (okc, len(static_canon)))
|
|
4598
|
+
% (_scope, okc, len(static_canon)))
|
|
4585
4599
|
else:
|
|
4586
4600
|
dims["contracts"] = _fid_dim(None, "UNMEASURED: no static-class canonical items")
|
|
4587
|
-
# curation_closure: curated OBS / total OBS in the active delta
|
|
4588
|
-
|
|
4601
|
+
# curation_closure: curated OBS / total OBS in the active delta. With --ir (ADR-015) it is
|
|
4602
|
+
# answered as a path query over the graph -- OBS nodes reachable to a CURATION node via an
|
|
4603
|
+
# OBS->CURATION edge / OBS nodes. It reproduces v0 when the graph's OBS set equals the
|
|
4604
|
+
# delta's (the FIELD-RUN-001 case); a canonical carrying OBS beyond the active delta would
|
|
4605
|
+
# widen the denominator, which is the graph's honest answer, not v0's.
|
|
4606
|
+
if args.ir:
|
|
4607
|
+
graph = _extract_ir(repo_path, ledger)
|
|
4608
|
+
obs_nodes = [nd["id"] for nd in graph["nodes"] if nd["type"] == "OBS"]
|
|
4609
|
+
cured = {e["from"] for e in graph["edges"] if e["type"] == "OBS->CURATION"}
|
|
4610
|
+
if obs_nodes:
|
|
4611
|
+
hit = sum(1 for oid in obs_nodes if oid in cured)
|
|
4612
|
+
dims["curation_closure"] = _fid_dim(
|
|
4613
|
+
round(hit / len(obs_nodes), 4),
|
|
4614
|
+
"IR path query: %d/%d OBS node(s) reach a CURATION node (IR v%s)"
|
|
4615
|
+
% (hit, len(obs_nodes), IR_SCHEMA))
|
|
4616
|
+
else:
|
|
4617
|
+
dims["curation_closure"] = _fid_dim(
|
|
4618
|
+
None, "UNMEASURED: no OBS nodes in the IR graph")
|
|
4619
|
+
elif obs:
|
|
4589
4620
|
cur = sum(1 for o in obs if o["id"] in verdicts)
|
|
4590
4621
|
dims["curation_closure"] = _fid_dim(round(cur / len(obs), 4),
|
|
4591
4622
|
"%d/%d OBS in %s carry a ledger verdict"
|
|
@@ -4616,14 +4647,15 @@ def cmd_fidelity(args):
|
|
|
4616
4647
|
unex = [f for f in prod if f not in lineage]
|
|
4617
4648
|
dims["unexplained_code"] = _fid_dim(
|
|
4618
4649
|
round(len(unex) / len(prod), 4),
|
|
4619
|
-
"%d/%d tracked prod source file(s) with no path to a canonical item or "
|
|
4620
|
-
"preserved OBS (v0 granularity: FILE)" % (len(unex), len(prod)),
|
|
4650
|
+
"%d/%d tracked prod source file(s)%s with no path to a canonical item or "
|
|
4651
|
+
"preserved OBS (v0 granularity: FILE)" % (len(unex), len(prod), _scope),
|
|
4621
4652
|
files=unex[:10] + (["(+%d)" % (len(unex) - 10)] if len(unex) > 10 else []))
|
|
4622
4653
|
else:
|
|
4623
4654
|
dims["unexplained_code"] = _fid_dim(None, "UNMEASURED: no tracked prod source files")
|
|
4624
4655
|
dims["semantic"] = _fid_dim(None, "not wired: an LLM-judged comparison enters as "
|
|
4625
4656
|
"advisory only and can NEVER gate (INV-ADVISORY-01)")
|
|
4626
4657
|
out = {"repo": args.repo,
|
|
4658
|
+
**({"path": bound} if bound else {}),
|
|
4627
4659
|
"dimensions": {k: dict(dims[k], **{"class": FIDELITY_DIMENSIONS[k]})
|
|
4628
4660
|
for k in ("traceability", "behavior", "contracts",
|
|
4629
4661
|
"curation_closure", "unexplained_code", "semantic")}}
|
|
@@ -4632,14 +4664,307 @@ def cmd_fidelity(args):
|
|
|
4632
4664
|
if args.json:
|
|
4633
4665
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
4634
4666
|
else:
|
|
4635
|
-
print("FIDELITY %s (vector -- no blend; each number stands on its own evidence):"
|
|
4636
|
-
% args.repo)
|
|
4667
|
+
print("FIDELITY %s%s (vector -- no blend; each number stands on its own evidence):"
|
|
4668
|
+
% (args.repo, " [bounded to %s]" % bound if bound else ""))
|
|
4637
4669
|
for k, d in out["dimensions"].items():
|
|
4638
4670
|
val = "UNMEASURED" if d["value"] is None else "%.2f" % d["value"]
|
|
4639
4671
|
print(" %-17s %-10s [%s] %s" % (k, val, d["class"], d["provenance"]))
|
|
4640
4672
|
sys.exit(0)
|
|
4641
4673
|
|
|
4642
4674
|
|
|
4675
|
+
# --------------------------------------------------------------------------- #
|
|
4676
|
+
# IR (Diamond M2: the canonical package extracts into a typed graph. ADR-015.
|
|
4677
|
+
# Markdown stays canonical; the IR is a derived index. What cannot be typed
|
|
4678
|
+
# deterministically is UNTYPED -- visible, counted, never guessed.)
|
|
4679
|
+
# --------------------------------------------------------------------------- #
|
|
4680
|
+
|
|
4681
|
+
IR_DIR = "ir"
|
|
4682
|
+
IR_FILE = "IR.json"
|
|
4683
|
+
IR_TWIN = "IR.md"
|
|
4684
|
+
IR_SCHEMA = "0.1"
|
|
4685
|
+
IR_NODE_TYPES = ("REQ", "INV", "AC", "CONTRACT", "DECISION", "NFR",
|
|
4686
|
+
"GOLDEN", "OBS", "CURATION", "EVIDENCE")
|
|
4687
|
+
IR_EDGE_TYPES = ("REQ->AC", "AC->EVIDENCE", "DECISION->INV", "OBS->CURATION",
|
|
4688
|
+
"CURATION->canonical", "supersedes", "derived_from")
|
|
4689
|
+
# broader than _AC_ID: the forward package uses sub-namespaced ids (AC-DD-07, AC-FV-06)
|
|
4690
|
+
_IR_AC_LINE = re.compile(r"^\s*[-*]\s*\[[ xX]\]\s*(AC-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\b\s*[-—:]*\s*(.*)$")
|
|
4691
|
+
_IR_INV_LINE = re.compile(r"^\s*[-*]\s*\*\*(INV-[A-Za-z0-9-]+)\s*[—-]+\s*(.*?)\*\*")
|
|
4692
|
+
_IR_ADR_REF = re.compile(r"\bADR-0*(\d+)\b")
|
|
4693
|
+
_IR_INV_REF = re.compile(r"\bINV-[A-Za-z0-9-]+\b")
|
|
4694
|
+
_IR_SUPERSEDE = re.compile(r"(?i)supersed\w*\s+(ADR-0*\d+)")
|
|
4695
|
+
_IR_BANNER = ("GENERATED by qa_ledger.py ir-extract (ADR-015). Rendered view of "
|
|
4696
|
+
+ IR_FILE + " -- hand edits are overwritten on regeneration.")
|
|
4697
|
+
|
|
4698
|
+
|
|
4699
|
+
def _ir_node_id(ntype, text, source):
|
|
4700
|
+
"""Content-address the ID-less (ADR-015 option B): nodes with a native human id keep it;
|
|
4701
|
+
only nodes with nothing human to anchor to get NODE-sha256(type+text+source)[:12]."""
|
|
4702
|
+
norm = re.sub(r"\s+", " ", (text or "").strip().lower())
|
|
4703
|
+
return "NODE-" + hashlib.sha256(
|
|
4704
|
+
(ntype + "\n" + norm + "\n" + source).encode("utf-8")).hexdigest()[:12]
|
|
4705
|
+
|
|
4706
|
+
|
|
4707
|
+
def _ir_read_lines(path):
|
|
4708
|
+
try:
|
|
4709
|
+
with open(path, encoding="utf-8-sig", errors="replace") as fh:
|
|
4710
|
+
return fh.read().splitlines()
|
|
4711
|
+
except OSError:
|
|
4712
|
+
return None
|
|
4713
|
+
|
|
4714
|
+
|
|
4715
|
+
def _extract_ir(repo_path, ledger):
|
|
4716
|
+
"""Deterministic extraction of the forward canonical package into a typed graph. Every
|
|
4717
|
+
node/edge is derived from an EXISTING structural convention or reference -- nothing is
|
|
4718
|
+
inferred. A line that fills a structural slot but cannot be typed lands in `untyped`."""
|
|
4719
|
+
nodes, edges, untyped = [], [], []
|
|
4720
|
+
seen_ids = set()
|
|
4721
|
+
|
|
4722
|
+
def add(node):
|
|
4723
|
+
if node["id"] not in seen_ids:
|
|
4724
|
+
seen_ids.add(node["id"])
|
|
4725
|
+
nodes.append(node)
|
|
4726
|
+
|
|
4727
|
+
# AC nodes <- ACCEPTANCE.md checkboxes carrying an AC id; a checkbox WITHOUT an id is an
|
|
4728
|
+
# acceptance slot the conventions do not type -> untyped (never a guessed id).
|
|
4729
|
+
acc_lines = _ir_read_lines(os.path.join(repo_path, "ACCEPTANCE.md")) or []
|
|
4730
|
+
for n, ln in enumerate(acc_lines, 1):
|
|
4731
|
+
s = ln.strip()
|
|
4732
|
+
if s[:5].lower() not in ("- [x]", "- [ ]", "* [x]", "* [ ]"):
|
|
4733
|
+
continue
|
|
4734
|
+
m = _IR_AC_LINE.match(ln)
|
|
4735
|
+
src = {"file": "ACCEPTANCE.md", "line": n}
|
|
4736
|
+
if m:
|
|
4737
|
+
add({"id": m.group(1).upper(), "type": "AC",
|
|
4738
|
+
"statement": m.group(2).strip(), "source": src})
|
|
4739
|
+
else:
|
|
4740
|
+
untyped.append({"text": s[5:].strip(), "source": src,
|
|
4741
|
+
"reason": "acceptance checkbox without a traceable AC-id"})
|
|
4742
|
+
|
|
4743
|
+
# INV nodes <- CONSTITUTION.md `- **INV-XXX-NN — Title.**` headings
|
|
4744
|
+
con_lines = _ir_read_lines(os.path.join(repo_path, "CONSTITUTION.md")) or []
|
|
4745
|
+
inv_ids = set()
|
|
4746
|
+
for n, ln in enumerate(con_lines, 1):
|
|
4747
|
+
m = _IR_INV_LINE.match(ln)
|
|
4748
|
+
if m:
|
|
4749
|
+
add({"id": m.group(1).upper(), "type": "INV",
|
|
4750
|
+
"statement": m.group(2).strip().rstrip("."),
|
|
4751
|
+
"source": {"file": "CONSTITUTION.md", "line": n}})
|
|
4752
|
+
inv_ids.add(m.group(1).upper())
|
|
4753
|
+
|
|
4754
|
+
# DECISION nodes <- docs/adr/*.md (native ADR-NNN id, title, file); edges to the INVs the
|
|
4755
|
+
# ADR governs/states, and supersedes edges -- both from references already in the body.
|
|
4756
|
+
adr_dir = os.path.join(repo_path, "docs", "adr")
|
|
4757
|
+
ac_ids = {nd["id"] for nd in nodes if nd["type"] == "AC"}
|
|
4758
|
+
for row in _mirador_adrs(adr_dir):
|
|
4759
|
+
add({"id": row["id"], "type": "DECISION", "statement": row["t"],
|
|
4760
|
+
"source": {"file": os.path.relpath(row["file"], repo_path).replace(os.sep, "/"),
|
|
4761
|
+
"line": 1}})
|
|
4762
|
+
body = _ir_read_lines(row["file"]) or []
|
|
4763
|
+
text = "\n".join(body)
|
|
4764
|
+
for inv in set(_IR_INV_REF.findall(text)):
|
|
4765
|
+
if inv.upper() in inv_ids:
|
|
4766
|
+
edges.append({"from": row["id"], "to": inv.upper(), "type": "DECISION->INV"})
|
|
4767
|
+
for sup in set(_IR_SUPERSEDE.findall(text)):
|
|
4768
|
+
edges.append({"from": row["id"],
|
|
4769
|
+
"to": "ADR-%03d" % int(re.search(r"\d+", sup).group()),
|
|
4770
|
+
"type": "supersedes"})
|
|
4771
|
+
# an ADR's Verification checklist references its ACs -> REQ->AC is absent here (no REQ
|
|
4772
|
+
# layer yet), but AC nodes referenced by an ADR are real edges DECISION mentions AC:
|
|
4773
|
+
for aid in {m.group(0)[1:-1].upper()
|
|
4774
|
+
for m in re.finditer(r"\(AC-[A-Za-z0-9-]+\)", text)}:
|
|
4775
|
+
if aid in ac_ids: # set(): an AC cited twice is one edge
|
|
4776
|
+
edges.append({"from": row["id"], "to": aid, "type": "REQ->AC"})
|
|
4777
|
+
|
|
4778
|
+
# GOLDEN nodes <- git-tracked approved fixtures (the path is a native, stable id)
|
|
4779
|
+
tracked = _tracked_files(repo_path) or []
|
|
4780
|
+
gold_marker = ".appro" + "ved."
|
|
4781
|
+
for f in tracked:
|
|
4782
|
+
if gold_marker in f:
|
|
4783
|
+
add({"id": f, "type": "GOLDEN", "statement": "approved golden fixture",
|
|
4784
|
+
"source": {"file": f, "line": 1}})
|
|
4785
|
+
|
|
4786
|
+
# OBS / CURATION <- the active delta's observations (all of them, so curation_closure is
|
|
4787
|
+
# a real path query over the graph) + the ledger's curation records. Absent in a repo
|
|
4788
|
+
# that never ran a field run -> simply no such nodes.
|
|
4789
|
+
delta, derrs = _load_delta(repo_path)
|
|
4790
|
+
if delta and not derrs:
|
|
4791
|
+
for o in delta.get("observations") or []:
|
|
4792
|
+
prov = (o.get("provenance") or {}).get("files") or []
|
|
4793
|
+
add({"id": o["id"], "type": "OBS", "statement": o.get("statement") or "",
|
|
4794
|
+
"source": {"file": prov[0].split(":")[0] if prov else "delta", "line": 1}})
|
|
4795
|
+
canon_path = os.path.join(repo_path, CANDIDATE_DIR, CANONICAL_FILE)
|
|
4796
|
+
canon_items = []
|
|
4797
|
+
if os.path.isfile(canon_path):
|
|
4798
|
+
try:
|
|
4799
|
+
with open(canon_path, encoding="utf-8-sig") as fh:
|
|
4800
|
+
canon_items = json.load(fh).get("items") or []
|
|
4801
|
+
except (OSError, ValueError):
|
|
4802
|
+
canon_items = []
|
|
4803
|
+
for it in canon_items:
|
|
4804
|
+
oid = it.get("derived_from")
|
|
4805
|
+
if not oid:
|
|
4806
|
+
continue
|
|
4807
|
+
prov = (it.get("provenance") or {}).get("files") or []
|
|
4808
|
+
add({"id": oid, "type": "OBS", "statement": it.get("statement") or "",
|
|
4809
|
+
"source": {"file": prov[0].split(":")[0] if prov else "canonical", "line": 1}})
|
|
4810
|
+
for rec in ledger.get("curation") or []:
|
|
4811
|
+
oid = rec.get("obs_id")
|
|
4812
|
+
if not oid:
|
|
4813
|
+
continue
|
|
4814
|
+
cid = "CUR-" + hashlib.sha256(
|
|
4815
|
+
(oid + "\n" + (rec.get("at") or "")).encode("utf-8")).hexdigest()[:12]
|
|
4816
|
+
add({"id": cid, "type": "CURATION",
|
|
4817
|
+
"statement": "%s: %s" % (rec.get("verdict"), oid),
|
|
4818
|
+
"source": {"file": "QA-LEDGER.json", "line": 0}})
|
|
4819
|
+
if oid in seen_ids:
|
|
4820
|
+
edges.append({"from": oid, "to": cid, "type": "OBS->CURATION"})
|
|
4821
|
+
|
|
4822
|
+
node_ids = {nd["id"] for nd in nodes}
|
|
4823
|
+
kept, dropped = [], 0
|
|
4824
|
+
for e in edges:
|
|
4825
|
+
if e["from"] in node_ids and e["to"] in node_ids:
|
|
4826
|
+
kept.append(e)
|
|
4827
|
+
elif e["type"] == "supersedes" and e["from"] in node_ids:
|
|
4828
|
+
kept.append(e) # a superseded ADR may be archived; keep it
|
|
4829
|
+
else:
|
|
4830
|
+
dropped += 1
|
|
4831
|
+
nodes.sort(key=lambda nd: (nd["type"], nd["id"]))
|
|
4832
|
+
kept.sort(key=lambda e: (e["type"], e["from"], e["to"]))
|
|
4833
|
+
untyped.sort(key=lambda u: (u["source"]["file"], u["source"]["line"]))
|
|
4834
|
+
counts = {t: sum(1 for nd in nodes if nd["type"] == t) for t in IR_NODE_TYPES}
|
|
4835
|
+
stats = {"nodes": len(nodes), "edges": len(kept),
|
|
4836
|
+
"edges_dropped": dropped, "untyped": len(untyped),
|
|
4837
|
+
"by_type": {t: c for t, c in counts.items() if c},
|
|
4838
|
+
"untyped_rate": round(len(untyped) / (len(nodes) + len(untyped)), 4)
|
|
4839
|
+
if (nodes or untyped) else 0.0}
|
|
4840
|
+
graph = {"schema_version": IR_SCHEMA,
|
|
4841
|
+
"_generated_by": "qa_ledger.py ir-extract (ADR-015) -- derived index of the "
|
|
4842
|
+
"canonical package; never hand-edit",
|
|
4843
|
+
"nodes": nodes, "edges": kept, "untyped": untyped, "stats": stats}
|
|
4844
|
+
# the seal covers stats too (fresh-review MEDIUM): a doctored summary that ir-render
|
|
4845
|
+
# would faithfully print must trip the strict loader, not slip past it -- the
|
|
4846
|
+
# derived-but-unsealed lesson, applied a third time (path @1.70, evidence_class @1.69).
|
|
4847
|
+
graph["_integrity"] = _ir_seal(graph)
|
|
4848
|
+
return graph
|
|
4849
|
+
|
|
4850
|
+
|
|
4851
|
+
def _ir_seal(graph):
|
|
4852
|
+
return _integrity_hash({k: graph.get(k) for k in
|
|
4853
|
+
("schema_version", "nodes", "edges", "untyped", "stats")})
|
|
4854
|
+
|
|
4855
|
+
|
|
4856
|
+
def _load_ir(repo_path):
|
|
4857
|
+
"""Strict loader: an unknown schema_version or a broken graph is exit-2 class, never
|
|
4858
|
+
mis-read (AC-IR-05). Returns (graph, errors); graph None when absent."""
|
|
4859
|
+
path = os.path.join(repo_path, IR_DIR, IR_FILE)
|
|
4860
|
+
if not os.path.isfile(path):
|
|
4861
|
+
return None, []
|
|
4862
|
+
try:
|
|
4863
|
+
with open(path, encoding="utf-8-sig") as fh:
|
|
4864
|
+
g = json.load(fh)
|
|
4865
|
+
except (OSError, ValueError) as exc:
|
|
4866
|
+
return {}, ["unreadable: %s" % exc]
|
|
4867
|
+
errors = []
|
|
4868
|
+
if g.get("schema_version") != IR_SCHEMA:
|
|
4869
|
+
errors.append("schema_version %r != %r (a version this engine does not know is not "
|
|
4870
|
+
"read, it is refused)" % (g.get("schema_version"), IR_SCHEMA))
|
|
4871
|
+
return g, errors
|
|
4872
|
+
if not isinstance(g.get("nodes"), list) or not isinstance(g.get("edges"), list):
|
|
4873
|
+
return g, ["nodes/edges missing or not lists"]
|
|
4874
|
+
if g.get("_integrity") != _ir_seal(g):
|
|
4875
|
+
errors.append("integrity seal does not match the graph -- nodes, edges, untyped or "
|
|
4876
|
+
"stats was hand-edited (regenerate via ir-extract)")
|
|
4877
|
+
return g, errors
|
|
4878
|
+
|
|
4879
|
+
|
|
4880
|
+
def _render_ir_md(graph):
|
|
4881
|
+
st = graph.get("stats") or {}
|
|
4882
|
+
lines = ["<!-- %s -->" % _IR_BANNER, "", "# Uscha IR v%s (rendered view)" % IR_SCHEMA, "",
|
|
4883
|
+
"%d nodes · %d edges · %d UNTYPED (rate %.2f)"
|
|
4884
|
+
% (st.get("nodes", 0), st.get("edges", 0), st.get("untyped", 0),
|
|
4885
|
+
st.get("untyped_rate", 0.0)), "",
|
|
4886
|
+
"## Nodes", "", "| id | type | statement | source |",
|
|
4887
|
+
"|----|------|-----------|--------|"]
|
|
4888
|
+
for nd in graph.get("nodes") or []:
|
|
4889
|
+
src = "%s:%s" % (nd["source"]["file"], nd["source"]["line"])
|
|
4890
|
+
stmt = (nd.get("statement") or "").replace("|", "\\|")
|
|
4891
|
+
lines.append("| %s | %s | %s | %s |" % (nd["id"], nd["type"], stmt, src))
|
|
4892
|
+
lines += ["", "## Edges", "", "| from | type | to |", "|------|------|----|"]
|
|
4893
|
+
for e in graph.get("edges") or []:
|
|
4894
|
+
lines.append("| %s | %s | %s |" % (e["from"], e["type"], e["to"]))
|
|
4895
|
+
if graph.get("untyped"):
|
|
4896
|
+
lines += ["", "## UNTYPED (conventions the human layer is missing)", "",
|
|
4897
|
+
"| text | source | reason |", "|------|--------|--------|"]
|
|
4898
|
+
for u in graph["untyped"]:
|
|
4899
|
+
src = "%s:%s" % (u["source"]["file"], u["source"]["line"])
|
|
4900
|
+
txt = (u.get("text") or "").replace("|", "\\|")[:80]
|
|
4901
|
+
lines.append("| %s | %s | %s |" % (txt, src, u.get("reason", "")))
|
|
4902
|
+
lines.append("")
|
|
4903
|
+
return "\n".join(lines)
|
|
4904
|
+
|
|
4905
|
+
|
|
4906
|
+
def cmd_ir_extract(args):
|
|
4907
|
+
ledger = _load(args.ledger)
|
|
4908
|
+
_repo_node(ledger, args.repo)
|
|
4909
|
+
repo_path = _scope_path(ledger, args.repo)
|
|
4910
|
+
graph = _extract_ir(repo_path, ledger)
|
|
4911
|
+
ir_dir = os.path.join(repo_path, IR_DIR)
|
|
4912
|
+
os.makedirs(ir_dir, exist_ok=True)
|
|
4913
|
+
with open(os.path.join(ir_dir, IR_FILE), "w", encoding="utf-8", newline="\n") as fh:
|
|
4914
|
+
fh.write(json.dumps(graph, indent=2, ensure_ascii=False) + "\n")
|
|
4915
|
+
twin_path = os.path.join(ir_dir, IR_TWIN)
|
|
4916
|
+
twin = _render_ir_md(graph)
|
|
4917
|
+
prev = None
|
|
4918
|
+
if os.path.isfile(twin_path):
|
|
4919
|
+
try:
|
|
4920
|
+
with open(twin_path, encoding="utf-8-sig") as fh:
|
|
4921
|
+
prev = fh.read()
|
|
4922
|
+
except OSError:
|
|
4923
|
+
prev = None
|
|
4924
|
+
with open(twin_path, "w", encoding="utf-8", newline="\n") as fh:
|
|
4925
|
+
fh.write(twin)
|
|
4926
|
+
if prev is not None and prev != twin:
|
|
4927
|
+
print("[qa_ledger] ir-extract: %s regenerated (rendered view, never a source)"
|
|
4928
|
+
% IR_TWIN, file=sys.stderr)
|
|
4929
|
+
st = graph["stats"]
|
|
4930
|
+
if args.json:
|
|
4931
|
+
print(json.dumps(st, indent=2, ensure_ascii=False))
|
|
4932
|
+
else:
|
|
4933
|
+
print("IR-EXTRACT %s: %d nodes, %d edges -> %s"
|
|
4934
|
+
% (args.repo, st["nodes"], st["edges"],
|
|
4935
|
+
os.path.join(IR_DIR, IR_FILE)))
|
|
4936
|
+
print(" by type: " + ", ".join("%s=%d" % (t, c)
|
|
4937
|
+
for t, c in st["by_type"].items()))
|
|
4938
|
+
print(" UNTYPED: %d (rate %.2f) -- the size of what the conventions cannot yet type"
|
|
4939
|
+
% (st["untyped"], st["untyped_rate"]))
|
|
4940
|
+
if st["edges_dropped"]:
|
|
4941
|
+
print(" %d edge(s) dropped (endpoint not a node) -- counted, never dangling"
|
|
4942
|
+
% st["edges_dropped"])
|
|
4943
|
+
sys.exit(0)
|
|
4944
|
+
|
|
4945
|
+
|
|
4946
|
+
def cmd_ir_render(args):
|
|
4947
|
+
ledger = _load(args.ledger)
|
|
4948
|
+
_repo_node(ledger, args.repo)
|
|
4949
|
+
repo_path = _scope_path(ledger, args.repo)
|
|
4950
|
+
graph, errors = _load_ir(repo_path)
|
|
4951
|
+
if graph is None:
|
|
4952
|
+
print("[qa_ledger] ir-render: no %s -- run ir-extract first."
|
|
4953
|
+
% os.path.join(IR_DIR, IR_FILE), file=sys.stderr)
|
|
4954
|
+
sys.exit(2)
|
|
4955
|
+
if errors:
|
|
4956
|
+
for e in errors:
|
|
4957
|
+
print("[qa_ledger] ir-render: %s" % e, file=sys.stderr)
|
|
4958
|
+
sys.exit(2)
|
|
4959
|
+
twin = _render_ir_md(graph)
|
|
4960
|
+
with open(os.path.join(repo_path, IR_DIR, IR_TWIN), "w", encoding="utf-8",
|
|
4961
|
+
newline="\n") as fh:
|
|
4962
|
+
fh.write(twin)
|
|
4963
|
+
print("IR-RENDER %s: %s regenerated from the graph"
|
|
4964
|
+
% (args.repo, os.path.join(IR_DIR, IR_TWIN)))
|
|
4965
|
+
sys.exit(0)
|
|
4966
|
+
|
|
4967
|
+
|
|
4643
4968
|
# --------------------------------------------------------------------------- #
|
|
4644
4969
|
# facts (T0 / SYSTEM-FACTS: public claims become compiled artifacts of repo
|
|
4645
4970
|
# facts -- Diamond applied to Diamond. ADR-012.)
|
|
@@ -8687,9 +9012,29 @@ def build_parser():
|
|
|
8687
9012
|
pfv.add_argument("--repo", required=True)
|
|
8688
9013
|
pfv.add_argument("--config", default="uscha.config.json",
|
|
8689
9014
|
help="checked for defaults.fidelity.gate -- advisory there is a refusal")
|
|
9015
|
+
pfv.add_argument("--ir", action="store_true",
|
|
9016
|
+
help="answer curation_closure as a path query over the IR graph "
|
|
9017
|
+
"(ADR-015); reproduces v0 from the derived index")
|
|
8690
9018
|
pfv.add_argument("--json", action="store_true")
|
|
8691
9019
|
pfv.set_defaults(func=cmd_fidelity)
|
|
8692
9020
|
|
|
9021
|
+
pie = sub.add_parser(
|
|
9022
|
+
"ir-extract",
|
|
9023
|
+
help="extract the canonical package into a typed graph (ir/IR.json); what cannot be "
|
|
9024
|
+
"typed deterministically is UNTYPED, counted, never guessed (ADR-015)")
|
|
9025
|
+
pie.add_argument("--ledger", default="QA-LEDGER.json")
|
|
9026
|
+
pie.add_argument("--repo", required=True)
|
|
9027
|
+
pie.add_argument("--json", action="store_true")
|
|
9028
|
+
pie.set_defaults(func=cmd_ir_extract)
|
|
9029
|
+
|
|
9030
|
+
pir = sub.add_parser(
|
|
9031
|
+
"ir-render",
|
|
9032
|
+
help="regenerate the human view (ir/IR.md) from the graph; round-trip content-stable "
|
|
9033
|
+
"for the structured parts (ADR-015)")
|
|
9034
|
+
pir.add_argument("--ledger", default="QA-LEDGER.json")
|
|
9035
|
+
pir.add_argument("--repo", required=True)
|
|
9036
|
+
pir.set_defaults(func=cmd_ir_render)
|
|
9037
|
+
|
|
8693
9038
|
pcr = sub.add_parser("cleanroom",
|
|
8694
9039
|
help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
|
|
8695
9040
|
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.72.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, 42 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.72.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`,
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit 1.
|
|
1
|
+
uscha-kit 1.72.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"AC-FV-01": true, "AC-FV-02": true, "AC-FV-04": true, "AC-FV-05": true, "AC-FV-03": true, "review-h2": true, "review-m4": true}
|
|
1
|
+
{"AC-FV-01": true, "AC-FV-02": true, "AC-FV-04": true, "AC-FV-05": true, "AC-FV-03": true, "review-h2": true, "AC-FV-06": true, "review-m4": true}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"AC-IR-01": true, "AC-IR-02": true, "AC-IR-03": true, "AC-IR-04": true, "AC-IR-05": true, "AC-IR-06": true}
|
|
@@ -4533,6 +4533,10 @@ def cmd_fidelity(args):
|
|
|
4533
4533
|
sys.exit(2)
|
|
4534
4534
|
obs = (delta.get("observations") or []) if delta else []
|
|
4535
4535
|
verdicts = _curation_verdicts(ledger, args.repo)
|
|
4536
|
+
# fidelity respects the SAME bound that produced the delta (user decision, FR-001): a
|
|
4537
|
+
# bounded discovery is measured over its own subtree, so unexplained_code and the other
|
|
4538
|
+
# mechanical dimensions never mix the delta's scope with the whole repo's.
|
|
4539
|
+
bound = delta.get("path") if delta else None
|
|
4536
4540
|
dims = {}
|
|
4537
4541
|
# traceability: canonical items reachable in code via the uscha-spec id machinery
|
|
4538
4542
|
canon_items = []
|
|
@@ -4543,7 +4547,17 @@ def cmd_fidelity(args):
|
|
|
4543
4547
|
canon_items = json.load(fh).get("items") or []
|
|
4544
4548
|
except (OSError, ValueError):
|
|
4545
4549
|
canon_items = []
|
|
4546
|
-
tracked = _tracked_files(repo_path) or []
|
|
4550
|
+
tracked = [f for f in (_tracked_files(repo_path) or []) if _under_bound(f, bound)]
|
|
4551
|
+
_scope = " (bounded to %s)" % bound if bound else ""
|
|
4552
|
+
if bound:
|
|
4553
|
+
# scope the DENOMINATOR too, not just the file scan: promote MERGES into CANONICAL
|
|
4554
|
+
# repo-wide, so an earlier unbounded promote would otherwise count out-of-bound
|
|
4555
|
+
# items as "no longer derives" -- the exact scope-mixing this release kills, half-
|
|
4556
|
+
# done if only the numerator moves (fresh-review LOW). An item is under the bound
|
|
4557
|
+
# when its primary provenance file is.
|
|
4558
|
+
canon_items = [it for it in canon_items
|
|
4559
|
+
if any(_under_bound(f.split(":")[0].split("#")[0], bound)
|
|
4560
|
+
for f in (it.get("provenance") or {}).get("files") or [])]
|
|
4547
4561
|
marked, marker_files = set(), set()
|
|
4548
4562
|
for f, mid in _scan_spec_markers(repo_path, tracked):
|
|
4549
4563
|
marked.add(mid)
|
|
@@ -4552,8 +4566,8 @@ def cmd_fidelity(args):
|
|
|
4552
4566
|
traced = sum(1 for it in canon_items if (it.get("derived_from") or "") in marked)
|
|
4553
4567
|
dims["traceability"] = _fid_dim(round(traced / len(canon_items), 4),
|
|
4554
4568
|
"uscha-spec marker scan over %d git-tracked "
|
|
4555
|
-
"files vs %d canonical item(s)"
|
|
4556
|
-
% (len(tracked), len(canon_items)))
|
|
4569
|
+
"files%s vs %d canonical item(s)"
|
|
4570
|
+
% (len(tracked), _scope, len(canon_items)))
|
|
4557
4571
|
else:
|
|
4558
4572
|
dims["traceability"] = _fid_dim(None, "UNMEASURED: no canonical items promoted yet")
|
|
4559
4573
|
# behavior: the latest ingested golden-diff gate verdict
|
|
@@ -4579,13 +4593,30 @@ def cmd_fidelity(args):
|
|
|
4579
4593
|
cur_ids = {o["id"] for o in cur_static}
|
|
4580
4594
|
okc = sum(1 for it in static_canon if it.get("derived_from") in cur_ids)
|
|
4581
4595
|
dims["contracts"] = _fid_dim(round(okc / len(static_canon), 4),
|
|
4582
|
-
"re-ran static extractors; %d/%d canonical static "
|
|
4596
|
+
"re-ran static extractors%s; %d/%d canonical static "
|
|
4583
4597
|
"item(s) still derive from the code"
|
|
4584
|
-
% (okc, len(static_canon)))
|
|
4598
|
+
% (_scope, okc, len(static_canon)))
|
|
4585
4599
|
else:
|
|
4586
4600
|
dims["contracts"] = _fid_dim(None, "UNMEASURED: no static-class canonical items")
|
|
4587
|
-
# curation_closure: curated OBS / total OBS in the active delta
|
|
4588
|
-
|
|
4601
|
+
# curation_closure: curated OBS / total OBS in the active delta. With --ir (ADR-015) it is
|
|
4602
|
+
# answered as a path query over the graph -- OBS nodes reachable to a CURATION node via an
|
|
4603
|
+
# OBS->CURATION edge / OBS nodes. It reproduces v0 when the graph's OBS set equals the
|
|
4604
|
+
# delta's (the FIELD-RUN-001 case); a canonical carrying OBS beyond the active delta would
|
|
4605
|
+
# widen the denominator, which is the graph's honest answer, not v0's.
|
|
4606
|
+
if args.ir:
|
|
4607
|
+
graph = _extract_ir(repo_path, ledger)
|
|
4608
|
+
obs_nodes = [nd["id"] for nd in graph["nodes"] if nd["type"] == "OBS"]
|
|
4609
|
+
cured = {e["from"] for e in graph["edges"] if e["type"] == "OBS->CURATION"}
|
|
4610
|
+
if obs_nodes:
|
|
4611
|
+
hit = sum(1 for oid in obs_nodes if oid in cured)
|
|
4612
|
+
dims["curation_closure"] = _fid_dim(
|
|
4613
|
+
round(hit / len(obs_nodes), 4),
|
|
4614
|
+
"IR path query: %d/%d OBS node(s) reach a CURATION node (IR v%s)"
|
|
4615
|
+
% (hit, len(obs_nodes), IR_SCHEMA))
|
|
4616
|
+
else:
|
|
4617
|
+
dims["curation_closure"] = _fid_dim(
|
|
4618
|
+
None, "UNMEASURED: no OBS nodes in the IR graph")
|
|
4619
|
+
elif obs:
|
|
4589
4620
|
cur = sum(1 for o in obs if o["id"] in verdicts)
|
|
4590
4621
|
dims["curation_closure"] = _fid_dim(round(cur / len(obs), 4),
|
|
4591
4622
|
"%d/%d OBS in %s carry a ledger verdict"
|
|
@@ -4616,14 +4647,15 @@ def cmd_fidelity(args):
|
|
|
4616
4647
|
unex = [f for f in prod if f not in lineage]
|
|
4617
4648
|
dims["unexplained_code"] = _fid_dim(
|
|
4618
4649
|
round(len(unex) / len(prod), 4),
|
|
4619
|
-
"%d/%d tracked prod source file(s) with no path to a canonical item or "
|
|
4620
|
-
"preserved OBS (v0 granularity: FILE)" % (len(unex), len(prod)),
|
|
4650
|
+
"%d/%d tracked prod source file(s)%s with no path to a canonical item or "
|
|
4651
|
+
"preserved OBS (v0 granularity: FILE)" % (len(unex), len(prod), _scope),
|
|
4621
4652
|
files=unex[:10] + (["(+%d)" % (len(unex) - 10)] if len(unex) > 10 else []))
|
|
4622
4653
|
else:
|
|
4623
4654
|
dims["unexplained_code"] = _fid_dim(None, "UNMEASURED: no tracked prod source files")
|
|
4624
4655
|
dims["semantic"] = _fid_dim(None, "not wired: an LLM-judged comparison enters as "
|
|
4625
4656
|
"advisory only and can NEVER gate (INV-ADVISORY-01)")
|
|
4626
4657
|
out = {"repo": args.repo,
|
|
4658
|
+
**({"path": bound} if bound else {}),
|
|
4627
4659
|
"dimensions": {k: dict(dims[k], **{"class": FIDELITY_DIMENSIONS[k]})
|
|
4628
4660
|
for k in ("traceability", "behavior", "contracts",
|
|
4629
4661
|
"curation_closure", "unexplained_code", "semantic")}}
|
|
@@ -4632,14 +4664,307 @@ def cmd_fidelity(args):
|
|
|
4632
4664
|
if args.json:
|
|
4633
4665
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
4634
4666
|
else:
|
|
4635
|
-
print("FIDELITY %s (vector -- no blend; each number stands on its own evidence):"
|
|
4636
|
-
% args.repo)
|
|
4667
|
+
print("FIDELITY %s%s (vector -- no blend; each number stands on its own evidence):"
|
|
4668
|
+
% (args.repo, " [bounded to %s]" % bound if bound else ""))
|
|
4637
4669
|
for k, d in out["dimensions"].items():
|
|
4638
4670
|
val = "UNMEASURED" if d["value"] is None else "%.2f" % d["value"]
|
|
4639
4671
|
print(" %-17s %-10s [%s] %s" % (k, val, d["class"], d["provenance"]))
|
|
4640
4672
|
sys.exit(0)
|
|
4641
4673
|
|
|
4642
4674
|
|
|
4675
|
+
# --------------------------------------------------------------------------- #
|
|
4676
|
+
# IR (Diamond M2: the canonical package extracts into a typed graph. ADR-015.
|
|
4677
|
+
# Markdown stays canonical; the IR is a derived index. What cannot be typed
|
|
4678
|
+
# deterministically is UNTYPED -- visible, counted, never guessed.)
|
|
4679
|
+
# --------------------------------------------------------------------------- #
|
|
4680
|
+
|
|
4681
|
+
IR_DIR = "ir"
|
|
4682
|
+
IR_FILE = "IR.json"
|
|
4683
|
+
IR_TWIN = "IR.md"
|
|
4684
|
+
IR_SCHEMA = "0.1"
|
|
4685
|
+
IR_NODE_TYPES = ("REQ", "INV", "AC", "CONTRACT", "DECISION", "NFR",
|
|
4686
|
+
"GOLDEN", "OBS", "CURATION", "EVIDENCE")
|
|
4687
|
+
IR_EDGE_TYPES = ("REQ->AC", "AC->EVIDENCE", "DECISION->INV", "OBS->CURATION",
|
|
4688
|
+
"CURATION->canonical", "supersedes", "derived_from")
|
|
4689
|
+
# broader than _AC_ID: the forward package uses sub-namespaced ids (AC-DD-07, AC-FV-06)
|
|
4690
|
+
_IR_AC_LINE = re.compile(r"^\s*[-*]\s*\[[ xX]\]\s*(AC-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\b\s*[-—:]*\s*(.*)$")
|
|
4691
|
+
_IR_INV_LINE = re.compile(r"^\s*[-*]\s*\*\*(INV-[A-Za-z0-9-]+)\s*[—-]+\s*(.*?)\*\*")
|
|
4692
|
+
_IR_ADR_REF = re.compile(r"\bADR-0*(\d+)\b")
|
|
4693
|
+
_IR_INV_REF = re.compile(r"\bINV-[A-Za-z0-9-]+\b")
|
|
4694
|
+
_IR_SUPERSEDE = re.compile(r"(?i)supersed\w*\s+(ADR-0*\d+)")
|
|
4695
|
+
_IR_BANNER = ("GENERATED by qa_ledger.py ir-extract (ADR-015). Rendered view of "
|
|
4696
|
+
+ IR_FILE + " -- hand edits are overwritten on regeneration.")
|
|
4697
|
+
|
|
4698
|
+
|
|
4699
|
+
def _ir_node_id(ntype, text, source):
|
|
4700
|
+
"""Content-address the ID-less (ADR-015 option B): nodes with a native human id keep it;
|
|
4701
|
+
only nodes with nothing human to anchor to get NODE-sha256(type+text+source)[:12]."""
|
|
4702
|
+
norm = re.sub(r"\s+", " ", (text or "").strip().lower())
|
|
4703
|
+
return "NODE-" + hashlib.sha256(
|
|
4704
|
+
(ntype + "\n" + norm + "\n" + source).encode("utf-8")).hexdigest()[:12]
|
|
4705
|
+
|
|
4706
|
+
|
|
4707
|
+
def _ir_read_lines(path):
|
|
4708
|
+
try:
|
|
4709
|
+
with open(path, encoding="utf-8-sig", errors="replace") as fh:
|
|
4710
|
+
return fh.read().splitlines()
|
|
4711
|
+
except OSError:
|
|
4712
|
+
return None
|
|
4713
|
+
|
|
4714
|
+
|
|
4715
|
+
def _extract_ir(repo_path, ledger):
|
|
4716
|
+
"""Deterministic extraction of the forward canonical package into a typed graph. Every
|
|
4717
|
+
node/edge is derived from an EXISTING structural convention or reference -- nothing is
|
|
4718
|
+
inferred. A line that fills a structural slot but cannot be typed lands in `untyped`."""
|
|
4719
|
+
nodes, edges, untyped = [], [], []
|
|
4720
|
+
seen_ids = set()
|
|
4721
|
+
|
|
4722
|
+
def add(node):
|
|
4723
|
+
if node["id"] not in seen_ids:
|
|
4724
|
+
seen_ids.add(node["id"])
|
|
4725
|
+
nodes.append(node)
|
|
4726
|
+
|
|
4727
|
+
# AC nodes <- ACCEPTANCE.md checkboxes carrying an AC id; a checkbox WITHOUT an id is an
|
|
4728
|
+
# acceptance slot the conventions do not type -> untyped (never a guessed id).
|
|
4729
|
+
acc_lines = _ir_read_lines(os.path.join(repo_path, "ACCEPTANCE.md")) or []
|
|
4730
|
+
for n, ln in enumerate(acc_lines, 1):
|
|
4731
|
+
s = ln.strip()
|
|
4732
|
+
if s[:5].lower() not in ("- [x]", "- [ ]", "* [x]", "* [ ]"):
|
|
4733
|
+
continue
|
|
4734
|
+
m = _IR_AC_LINE.match(ln)
|
|
4735
|
+
src = {"file": "ACCEPTANCE.md", "line": n}
|
|
4736
|
+
if m:
|
|
4737
|
+
add({"id": m.group(1).upper(), "type": "AC",
|
|
4738
|
+
"statement": m.group(2).strip(), "source": src})
|
|
4739
|
+
else:
|
|
4740
|
+
untyped.append({"text": s[5:].strip(), "source": src,
|
|
4741
|
+
"reason": "acceptance checkbox without a traceable AC-id"})
|
|
4742
|
+
|
|
4743
|
+
# INV nodes <- CONSTITUTION.md `- **INV-XXX-NN — Title.**` headings
|
|
4744
|
+
con_lines = _ir_read_lines(os.path.join(repo_path, "CONSTITUTION.md")) or []
|
|
4745
|
+
inv_ids = set()
|
|
4746
|
+
for n, ln in enumerate(con_lines, 1):
|
|
4747
|
+
m = _IR_INV_LINE.match(ln)
|
|
4748
|
+
if m:
|
|
4749
|
+
add({"id": m.group(1).upper(), "type": "INV",
|
|
4750
|
+
"statement": m.group(2).strip().rstrip("."),
|
|
4751
|
+
"source": {"file": "CONSTITUTION.md", "line": n}})
|
|
4752
|
+
inv_ids.add(m.group(1).upper())
|
|
4753
|
+
|
|
4754
|
+
# DECISION nodes <- docs/adr/*.md (native ADR-NNN id, title, file); edges to the INVs the
|
|
4755
|
+
# ADR governs/states, and supersedes edges -- both from references already in the body.
|
|
4756
|
+
adr_dir = os.path.join(repo_path, "docs", "adr")
|
|
4757
|
+
ac_ids = {nd["id"] for nd in nodes if nd["type"] == "AC"}
|
|
4758
|
+
for row in _mirador_adrs(adr_dir):
|
|
4759
|
+
add({"id": row["id"], "type": "DECISION", "statement": row["t"],
|
|
4760
|
+
"source": {"file": os.path.relpath(row["file"], repo_path).replace(os.sep, "/"),
|
|
4761
|
+
"line": 1}})
|
|
4762
|
+
body = _ir_read_lines(row["file"]) or []
|
|
4763
|
+
text = "\n".join(body)
|
|
4764
|
+
for inv in set(_IR_INV_REF.findall(text)):
|
|
4765
|
+
if inv.upper() in inv_ids:
|
|
4766
|
+
edges.append({"from": row["id"], "to": inv.upper(), "type": "DECISION->INV"})
|
|
4767
|
+
for sup in set(_IR_SUPERSEDE.findall(text)):
|
|
4768
|
+
edges.append({"from": row["id"],
|
|
4769
|
+
"to": "ADR-%03d" % int(re.search(r"\d+", sup).group()),
|
|
4770
|
+
"type": "supersedes"})
|
|
4771
|
+
# an ADR's Verification checklist references its ACs -> REQ->AC is absent here (no REQ
|
|
4772
|
+
# layer yet), but AC nodes referenced by an ADR are real edges DECISION mentions AC:
|
|
4773
|
+
for aid in {m.group(0)[1:-1].upper()
|
|
4774
|
+
for m in re.finditer(r"\(AC-[A-Za-z0-9-]+\)", text)}:
|
|
4775
|
+
if aid in ac_ids: # set(): an AC cited twice is one edge
|
|
4776
|
+
edges.append({"from": row["id"], "to": aid, "type": "REQ->AC"})
|
|
4777
|
+
|
|
4778
|
+
# GOLDEN nodes <- git-tracked approved fixtures (the path is a native, stable id)
|
|
4779
|
+
tracked = _tracked_files(repo_path) or []
|
|
4780
|
+
gold_marker = ".appro" + "ved."
|
|
4781
|
+
for f in tracked:
|
|
4782
|
+
if gold_marker in f:
|
|
4783
|
+
add({"id": f, "type": "GOLDEN", "statement": "approved golden fixture",
|
|
4784
|
+
"source": {"file": f, "line": 1}})
|
|
4785
|
+
|
|
4786
|
+
# OBS / CURATION <- the active delta's observations (all of them, so curation_closure is
|
|
4787
|
+
# a real path query over the graph) + the ledger's curation records. Absent in a repo
|
|
4788
|
+
# that never ran a field run -> simply no such nodes.
|
|
4789
|
+
delta, derrs = _load_delta(repo_path)
|
|
4790
|
+
if delta and not derrs:
|
|
4791
|
+
for o in delta.get("observations") or []:
|
|
4792
|
+
prov = (o.get("provenance") or {}).get("files") or []
|
|
4793
|
+
add({"id": o["id"], "type": "OBS", "statement": o.get("statement") or "",
|
|
4794
|
+
"source": {"file": prov[0].split(":")[0] if prov else "delta", "line": 1}})
|
|
4795
|
+
canon_path = os.path.join(repo_path, CANDIDATE_DIR, CANONICAL_FILE)
|
|
4796
|
+
canon_items = []
|
|
4797
|
+
if os.path.isfile(canon_path):
|
|
4798
|
+
try:
|
|
4799
|
+
with open(canon_path, encoding="utf-8-sig") as fh:
|
|
4800
|
+
canon_items = json.load(fh).get("items") or []
|
|
4801
|
+
except (OSError, ValueError):
|
|
4802
|
+
canon_items = []
|
|
4803
|
+
for it in canon_items:
|
|
4804
|
+
oid = it.get("derived_from")
|
|
4805
|
+
if not oid:
|
|
4806
|
+
continue
|
|
4807
|
+
prov = (it.get("provenance") or {}).get("files") or []
|
|
4808
|
+
add({"id": oid, "type": "OBS", "statement": it.get("statement") or "",
|
|
4809
|
+
"source": {"file": prov[0].split(":")[0] if prov else "canonical", "line": 1}})
|
|
4810
|
+
for rec in ledger.get("curation") or []:
|
|
4811
|
+
oid = rec.get("obs_id")
|
|
4812
|
+
if not oid:
|
|
4813
|
+
continue
|
|
4814
|
+
cid = "CUR-" + hashlib.sha256(
|
|
4815
|
+
(oid + "\n" + (rec.get("at") or "")).encode("utf-8")).hexdigest()[:12]
|
|
4816
|
+
add({"id": cid, "type": "CURATION",
|
|
4817
|
+
"statement": "%s: %s" % (rec.get("verdict"), oid),
|
|
4818
|
+
"source": {"file": "QA-LEDGER.json", "line": 0}})
|
|
4819
|
+
if oid in seen_ids:
|
|
4820
|
+
edges.append({"from": oid, "to": cid, "type": "OBS->CURATION"})
|
|
4821
|
+
|
|
4822
|
+
node_ids = {nd["id"] for nd in nodes}
|
|
4823
|
+
kept, dropped = [], 0
|
|
4824
|
+
for e in edges:
|
|
4825
|
+
if e["from"] in node_ids and e["to"] in node_ids:
|
|
4826
|
+
kept.append(e)
|
|
4827
|
+
elif e["type"] == "supersedes" and e["from"] in node_ids:
|
|
4828
|
+
kept.append(e) # a superseded ADR may be archived; keep it
|
|
4829
|
+
else:
|
|
4830
|
+
dropped += 1
|
|
4831
|
+
nodes.sort(key=lambda nd: (nd["type"], nd["id"]))
|
|
4832
|
+
kept.sort(key=lambda e: (e["type"], e["from"], e["to"]))
|
|
4833
|
+
untyped.sort(key=lambda u: (u["source"]["file"], u["source"]["line"]))
|
|
4834
|
+
counts = {t: sum(1 for nd in nodes if nd["type"] == t) for t in IR_NODE_TYPES}
|
|
4835
|
+
stats = {"nodes": len(nodes), "edges": len(kept),
|
|
4836
|
+
"edges_dropped": dropped, "untyped": len(untyped),
|
|
4837
|
+
"by_type": {t: c for t, c in counts.items() if c},
|
|
4838
|
+
"untyped_rate": round(len(untyped) / (len(nodes) + len(untyped)), 4)
|
|
4839
|
+
if (nodes or untyped) else 0.0}
|
|
4840
|
+
graph = {"schema_version": IR_SCHEMA,
|
|
4841
|
+
"_generated_by": "qa_ledger.py ir-extract (ADR-015) -- derived index of the "
|
|
4842
|
+
"canonical package; never hand-edit",
|
|
4843
|
+
"nodes": nodes, "edges": kept, "untyped": untyped, "stats": stats}
|
|
4844
|
+
# the seal covers stats too (fresh-review MEDIUM): a doctored summary that ir-render
|
|
4845
|
+
# would faithfully print must trip the strict loader, not slip past it -- the
|
|
4846
|
+
# derived-but-unsealed lesson, applied a third time (path @1.70, evidence_class @1.69).
|
|
4847
|
+
graph["_integrity"] = _ir_seal(graph)
|
|
4848
|
+
return graph
|
|
4849
|
+
|
|
4850
|
+
|
|
4851
|
+
def _ir_seal(graph):
|
|
4852
|
+
return _integrity_hash({k: graph.get(k) for k in
|
|
4853
|
+
("schema_version", "nodes", "edges", "untyped", "stats")})
|
|
4854
|
+
|
|
4855
|
+
|
|
4856
|
+
def _load_ir(repo_path):
|
|
4857
|
+
"""Strict loader: an unknown schema_version or a broken graph is exit-2 class, never
|
|
4858
|
+
mis-read (AC-IR-05). Returns (graph, errors); graph None when absent."""
|
|
4859
|
+
path = os.path.join(repo_path, IR_DIR, IR_FILE)
|
|
4860
|
+
if not os.path.isfile(path):
|
|
4861
|
+
return None, []
|
|
4862
|
+
try:
|
|
4863
|
+
with open(path, encoding="utf-8-sig") as fh:
|
|
4864
|
+
g = json.load(fh)
|
|
4865
|
+
except (OSError, ValueError) as exc:
|
|
4866
|
+
return {}, ["unreadable: %s" % exc]
|
|
4867
|
+
errors = []
|
|
4868
|
+
if g.get("schema_version") != IR_SCHEMA:
|
|
4869
|
+
errors.append("schema_version %r != %r (a version this engine does not know is not "
|
|
4870
|
+
"read, it is refused)" % (g.get("schema_version"), IR_SCHEMA))
|
|
4871
|
+
return g, errors
|
|
4872
|
+
if not isinstance(g.get("nodes"), list) or not isinstance(g.get("edges"), list):
|
|
4873
|
+
return g, ["nodes/edges missing or not lists"]
|
|
4874
|
+
if g.get("_integrity") != _ir_seal(g):
|
|
4875
|
+
errors.append("integrity seal does not match the graph -- nodes, edges, untyped or "
|
|
4876
|
+
"stats was hand-edited (regenerate via ir-extract)")
|
|
4877
|
+
return g, errors
|
|
4878
|
+
|
|
4879
|
+
|
|
4880
|
+
def _render_ir_md(graph):
|
|
4881
|
+
st = graph.get("stats") or {}
|
|
4882
|
+
lines = ["<!-- %s -->" % _IR_BANNER, "", "# Uscha IR v%s (rendered view)" % IR_SCHEMA, "",
|
|
4883
|
+
"%d nodes · %d edges · %d UNTYPED (rate %.2f)"
|
|
4884
|
+
% (st.get("nodes", 0), st.get("edges", 0), st.get("untyped", 0),
|
|
4885
|
+
st.get("untyped_rate", 0.0)), "",
|
|
4886
|
+
"## Nodes", "", "| id | type | statement | source |",
|
|
4887
|
+
"|----|------|-----------|--------|"]
|
|
4888
|
+
for nd in graph.get("nodes") or []:
|
|
4889
|
+
src = "%s:%s" % (nd["source"]["file"], nd["source"]["line"])
|
|
4890
|
+
stmt = (nd.get("statement") or "").replace("|", "\\|")
|
|
4891
|
+
lines.append("| %s | %s | %s | %s |" % (nd["id"], nd["type"], stmt, src))
|
|
4892
|
+
lines += ["", "## Edges", "", "| from | type | to |", "|------|------|----|"]
|
|
4893
|
+
for e in graph.get("edges") or []:
|
|
4894
|
+
lines.append("| %s | %s | %s |" % (e["from"], e["type"], e["to"]))
|
|
4895
|
+
if graph.get("untyped"):
|
|
4896
|
+
lines += ["", "## UNTYPED (conventions the human layer is missing)", "",
|
|
4897
|
+
"| text | source | reason |", "|------|--------|--------|"]
|
|
4898
|
+
for u in graph["untyped"]:
|
|
4899
|
+
src = "%s:%s" % (u["source"]["file"], u["source"]["line"])
|
|
4900
|
+
txt = (u.get("text") or "").replace("|", "\\|")[:80]
|
|
4901
|
+
lines.append("| %s | %s | %s |" % (txt, src, u.get("reason", "")))
|
|
4902
|
+
lines.append("")
|
|
4903
|
+
return "\n".join(lines)
|
|
4904
|
+
|
|
4905
|
+
|
|
4906
|
+
def cmd_ir_extract(args):
|
|
4907
|
+
ledger = _load(args.ledger)
|
|
4908
|
+
_repo_node(ledger, args.repo)
|
|
4909
|
+
repo_path = _scope_path(ledger, args.repo)
|
|
4910
|
+
graph = _extract_ir(repo_path, ledger)
|
|
4911
|
+
ir_dir = os.path.join(repo_path, IR_DIR)
|
|
4912
|
+
os.makedirs(ir_dir, exist_ok=True)
|
|
4913
|
+
with open(os.path.join(ir_dir, IR_FILE), "w", encoding="utf-8", newline="\n") as fh:
|
|
4914
|
+
fh.write(json.dumps(graph, indent=2, ensure_ascii=False) + "\n")
|
|
4915
|
+
twin_path = os.path.join(ir_dir, IR_TWIN)
|
|
4916
|
+
twin = _render_ir_md(graph)
|
|
4917
|
+
prev = None
|
|
4918
|
+
if os.path.isfile(twin_path):
|
|
4919
|
+
try:
|
|
4920
|
+
with open(twin_path, encoding="utf-8-sig") as fh:
|
|
4921
|
+
prev = fh.read()
|
|
4922
|
+
except OSError:
|
|
4923
|
+
prev = None
|
|
4924
|
+
with open(twin_path, "w", encoding="utf-8", newline="\n") as fh:
|
|
4925
|
+
fh.write(twin)
|
|
4926
|
+
if prev is not None and prev != twin:
|
|
4927
|
+
print("[qa_ledger] ir-extract: %s regenerated (rendered view, never a source)"
|
|
4928
|
+
% IR_TWIN, file=sys.stderr)
|
|
4929
|
+
st = graph["stats"]
|
|
4930
|
+
if args.json:
|
|
4931
|
+
print(json.dumps(st, indent=2, ensure_ascii=False))
|
|
4932
|
+
else:
|
|
4933
|
+
print("IR-EXTRACT %s: %d nodes, %d edges -> %s"
|
|
4934
|
+
% (args.repo, st["nodes"], st["edges"],
|
|
4935
|
+
os.path.join(IR_DIR, IR_FILE)))
|
|
4936
|
+
print(" by type: " + ", ".join("%s=%d" % (t, c)
|
|
4937
|
+
for t, c in st["by_type"].items()))
|
|
4938
|
+
print(" UNTYPED: %d (rate %.2f) -- the size of what the conventions cannot yet type"
|
|
4939
|
+
% (st["untyped"], st["untyped_rate"]))
|
|
4940
|
+
if st["edges_dropped"]:
|
|
4941
|
+
print(" %d edge(s) dropped (endpoint not a node) -- counted, never dangling"
|
|
4942
|
+
% st["edges_dropped"])
|
|
4943
|
+
sys.exit(0)
|
|
4944
|
+
|
|
4945
|
+
|
|
4946
|
+
def cmd_ir_render(args):
|
|
4947
|
+
ledger = _load(args.ledger)
|
|
4948
|
+
_repo_node(ledger, args.repo)
|
|
4949
|
+
repo_path = _scope_path(ledger, args.repo)
|
|
4950
|
+
graph, errors = _load_ir(repo_path)
|
|
4951
|
+
if graph is None:
|
|
4952
|
+
print("[qa_ledger] ir-render: no %s -- run ir-extract first."
|
|
4953
|
+
% os.path.join(IR_DIR, IR_FILE), file=sys.stderr)
|
|
4954
|
+
sys.exit(2)
|
|
4955
|
+
if errors:
|
|
4956
|
+
for e in errors:
|
|
4957
|
+
print("[qa_ledger] ir-render: %s" % e, file=sys.stderr)
|
|
4958
|
+
sys.exit(2)
|
|
4959
|
+
twin = _render_ir_md(graph)
|
|
4960
|
+
with open(os.path.join(repo_path, IR_DIR, IR_TWIN), "w", encoding="utf-8",
|
|
4961
|
+
newline="\n") as fh:
|
|
4962
|
+
fh.write(twin)
|
|
4963
|
+
print("IR-RENDER %s: %s regenerated from the graph"
|
|
4964
|
+
% (args.repo, os.path.join(IR_DIR, IR_TWIN)))
|
|
4965
|
+
sys.exit(0)
|
|
4966
|
+
|
|
4967
|
+
|
|
4643
4968
|
# --------------------------------------------------------------------------- #
|
|
4644
4969
|
# facts (T0 / SYSTEM-FACTS: public claims become compiled artifacts of repo
|
|
4645
4970
|
# facts -- Diamond applied to Diamond. ADR-012.)
|
|
@@ -8687,9 +9012,29 @@ def build_parser():
|
|
|
8687
9012
|
pfv.add_argument("--repo", required=True)
|
|
8688
9013
|
pfv.add_argument("--config", default="uscha.config.json",
|
|
8689
9014
|
help="checked for defaults.fidelity.gate -- advisory there is a refusal")
|
|
9015
|
+
pfv.add_argument("--ir", action="store_true",
|
|
9016
|
+
help="answer curation_closure as a path query over the IR graph "
|
|
9017
|
+
"(ADR-015); reproduces v0 from the derived index")
|
|
8690
9018
|
pfv.add_argument("--json", action="store_true")
|
|
8691
9019
|
pfv.set_defaults(func=cmd_fidelity)
|
|
8692
9020
|
|
|
9021
|
+
pie = sub.add_parser(
|
|
9022
|
+
"ir-extract",
|
|
9023
|
+
help="extract the canonical package into a typed graph (ir/IR.json); what cannot be "
|
|
9024
|
+
"typed deterministically is UNTYPED, counted, never guessed (ADR-015)")
|
|
9025
|
+
pie.add_argument("--ledger", default="QA-LEDGER.json")
|
|
9026
|
+
pie.add_argument("--repo", required=True)
|
|
9027
|
+
pie.add_argument("--json", action="store_true")
|
|
9028
|
+
pie.set_defaults(func=cmd_ir_extract)
|
|
9029
|
+
|
|
9030
|
+
pir = sub.add_parser(
|
|
9031
|
+
"ir-render",
|
|
9032
|
+
help="regenerate the human view (ir/IR.md) from the graph; round-trip content-stable "
|
|
9033
|
+
"for the structured parts (ADR-015)")
|
|
9034
|
+
pir.add_argument("--ledger", default="QA-LEDGER.json")
|
|
9035
|
+
pir.add_argument("--repo", required=True)
|
|
9036
|
+
pir.set_defaults(func=cmd_ir_render)
|
|
9037
|
+
|
|
8693
9038
|
pcr = sub.add_parser("cleanroom",
|
|
8694
9039
|
help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
|
|
8695
9040
|
pcr.add_argument("--ledger", default="QA-LEDGER.json")
|