#!/usr/bin/env python3 """Assert that the day-one kit's distillation cites (or explicitly dispositions) EVERY rule and EVERY hindsight entry of the source project. P33.5 task 14.5 (Drew, 2026-09-07: "the whole of our experience?"). The kit's registry seed (G-rules) and kernels (DK) were written from the distilled records; this tool derives the two source populations and refuses a silent gap (R32): * the RULES — every `- **R` heading of phase-ends/DIGEST.md §3 (asserted contiguous 1..N) — must be cited by a `provenance:` line of decomp-architect/templates/registry-E.decomp.md, OR have a disposition row in config/kit_coverage_map.tsv (kind=rule): `G` (it became those rules — the ids must exist), `DK-` (realised as a kernel), `ENV` (environment-specific, not portable), `PA` (ProjectArchitect's own rule), `FOLDED:G` (absorbed into a named rule without a citation), `SEED:` (a memory seed of the kit), `KIT:` (a template or installer step), `RECORD` (kept only in the verbatim record — a project-specific procedure rule); * the ACCELERATORS — every entry of docs/accelerators.md at SUB-ENTRY granularity (the `## ` headings, and within a heading the bold `**(n)` / `**n.` items) — must be cited by a `provenance:` line of decomp-architect/corpus/decomp-kernels.md or of the registry seed, OR have a disposition row (kind=accelerator): `DK-` / `G` (folded, uncited), `COOKBOOK` (an idiom-level entry that lives in the cookbook corpus), `RECORD`, `NOT-PORTABLE` (this project only — the note says why). Every count is printed with its denominator (R41); an unknown key or an unknown G/DK id in the map is refused (R43); a row for an entry that IS cited is informational (printed, not an error). Exit 1 on any gap. In `make tools-health` after tool_census --check. """ import csv import pathlib import re import sys REPO = pathlib.Path(__file__).resolve().parent.parent DIGEST = REPO / "phase-ends" / "DIGEST.md" ACCEL = REPO / "docs" / "accelerators.md" REGISTRY = REPO / "decomp-architect" / "templates" / "registry-E.decomp.md" KERNELS = REPO / "decomp-architect" / "corpus" / "decomp-kernels.md" MAP = REPO / "config" / "kit_coverage_map.tsv" RULE_DISPOSITIONS = ("ENV", "PA", "RECORD") ACCEL_DISPOSITIONS = ("COOKBOOK", "RECORD", "NOT-PORTABLE") def provenance_lines(path): return [ln for ln in path.read_text(encoding="utf-8").splitlines() if ln.startswith("provenance:")] def rule_ids(): ids = sorted({int(m) for m in re.findall(r"^- \*\*R(\d+)\b", DIGEST.read_text(encoding="utf-8"), re.M)}) if ids != list(range(1, ids[-1] + 1)): sys.exit(f"kit_coverage: DIGEST §3 rule ids are not contiguous 1..{ids[-1]}: missing {sorted(set(range(1, ids[-1] + 1)) - set(ids))}") return ids def g_ids(): return {int(m) for m in re.findall(r"^### G(\d+)\b", REGISTRY.read_text(encoding="utf-8"), re.M)} def dk_ids(): return {int(m) for m in re.findall(r"^### DK-(\d+)\b", KERNELS.read_text(encoding="utf-8"), re.M)} def accel_entries(): """[(key, heading-or-item text, line)] — one per `## ` heading; a heading with bold numbered items yields one per item instead.""" lines = ACCEL.read_text(encoding="utf-8").splitlines() heads = [(i, ln[3:].strip()) for i, ln in enumerate(lines) if ln.startswith("## ")] out = [] for n, (i, title) in enumerate(heads): end = heads[n + 1][0] if n + 1 < len(heads) else len(lines) m = re.match(r"(A\d+|#\d+|S\d+(?: \(\d+\))?|P\d+(?:\.\d+)?(?: [A-Z]\w*)?(?: S\d+)?)\b", title) if m: group = m.group(1) else: t = re.search(r"\((P\d+ S\d+)\)", title) group = t.group(1) if t else title items = [(j, lines[j]) for j in range(i + 1, end) if re.match(r"^\*\*(\(\d+\)|\d+\.)\s", lines[j])] if items: for j, ln in items: num = re.match(r"^\*\*\(?(\d+)", ln).group(1) out.append((f"{group} ({num})", ln[2:80].strip("* "), j + 1)) else: out.append((group, title, i + 1)) return out def accel_cited(key, prov_text): """Is this entry cited by any provenance line? Token rules mirror how the kernels cite the ledger (see the census in the task log).""" m = re.match(r"^(A\d+|#\d+|S\d+|P\d+(?:\.\d+)?(?: [A-Z]\w*)?(?: S\d+)?)(?: \((\d+)\))?$", key) if not m: return False group, num = m.group(1), m.group(2) tok = re.escape(group.split(" ")[0]) if group.startswith("P31 S58"): # the harness wounds, cited as "harness wound n" or "… wounds" 1–5 (a range) for ln in prov_text: if "accelerator" not in ln.lower() or "wound" not in ln.lower(): continue tail = ln.lower().split("wound", 1)[1][:60] nums = set() for a, b in re.findall(r"(\d+)\s*[–-]\s*(\d+)", tail): nums |= set(range(int(a), int(b) + 1)) nums |= {int(x) for x in re.findall(r"(?