#!/usr/bin/env python3 """path_claim_check.py -- does every in-namespace path this archive names exist at this revision? WHY THIS EXISTS `citation_resolve_check.py` grades `:` citations: it can resolve them because a line number makes the claim decidable. A path named WITHOUT a line -- "`recoveries/foo.md` (10,232 B)", "the record `docs/bar.md`" -- is the same kind of claim with one fewer field, and nothing here graded it. The defect is on the record: checkins/2026-10-19-volt.md records a report that "named `check.sh` and `files.sha256`" and never wrote them, and the ledger's rule is that a reader must be able to go and look. A path a reader cannot open is a citation that fails the only test a stranger can run. The measurement that made this tool possible also bounded it. Over the whole corpus, most distinct path tokens are not candidates for "this archive holds that file" at all: they begin with a component this repo does not track -- scratch trees (`after/verify.txt`), host artifacts (`/root/crucible/...`), other repositories, served routes and URLs. Grading those as missing would be hundreds of false defects, so they are COUNTED and never graded. What is left is small enough for a reader: every MISSING path at the revision the ledger was last reviewed against is in `tools/path_claim_baseline.tsv` with the class that explains it, and `--fail-on-missing` fails on any the ledger does not name -- so a revision that moves the corpus reds the gate until a reader has looked. The census print is what says which revision that was. This docstring deliberately carries no census figures. The first draft did, and the leading-slash fix in the matcher moved every one of them (1,024 distinct tokens became 1,036) -- the defect class this archive grades as a stale claim. The tool prints its own census on every run; that print is the only place those numbers live. WHAT IT MEASURES -- one verdict per DISTINCT path (unit: distinct paths) EXISTS the path is in the tree at --rev. MISSING the path's first component IS a tracked top-level entry, so the prose is naming something inside this repository, and the tree at --rev does not hold it. This is the graded class. EXTERNAL the first component is not tracked here (scratch tree, host path, URL, other repo). OUT OF SCOPE: the path may be perfectly true and this tool cannot see it. Counted, never graded -- the same vocabulary `citation_resolve_check.py` uses for EXTERNAL_PATH. TEMPLATE the path carries a placeholder (`YYYY`, ``, `{n}`), so it is a pattern rather than a claim about one file. Occurrences are counted beside distinct paths, because one wrong path cited 16 times is one defect and 16 chances for a reader to trip on it. A FENCED BLOCK IS QUOTED MATERIAL, NOT THIS DOCUMENT'S PROSE -- the rule `queue_index.py`, `checkin_check.py` and `citation_resolve_check.py` already apply to headings and citations. A fenced listing that quotes a path is not the containing document asserting that path exists. Skipped lines are printed on every run, never silently dropped. THE BASELINE, AND WHY THE FIRST CENSUS IS ITS LEDGER `\\t\\t`, one row per reviewed missing path, class from a closed set (PLANTED, NOT_ADDED, RENAMED, SHORT_NAME, FOREIGN_TREE, EXAMPLE). A row is a stated reading, not an exemption: `--fail-on-missing` fails only on a MISSING path the ledger does not name, and every tolerated row is still PRINTED with its class on every run, so a reader sees the tolerated rows rather than trusting that there are none. A ledger row whose path now exists is reported CLEARED, and a row naming a path the census no longer produces is reported as a dead row -- the ledger cannot rot invisibly. EXIT CODES 0 the census ran. Without --fail-on-missing a MISSING path is not a fault. 1 --fail-on-missing and at least one MISSING path is not in the baseline, or names a class the ledger does not tolerate. 2 could not grade: not a git work tree, a revision that does not resolve, a corpus with no markdown, a NAMED baseline that cannot be read or names an unknown class. Never 0, and never a silent empty census. A repo that carries no ledger of its own is not this: `--baseline` defaults to tools/path_claim_baseline.tsv resolved against --repo, and a tree without that file is graded with no ledger after one note on stderr. USAGE python3 path_claim_check.py [--repo DIR] [--rev HEAD] [--corpus PATH...] [--json OUT] [--only missing|exists|external|template|all] [--context] [--fail-on-missing] [--baseline PATH] --baseline defaults to tools/path_claim_baseline.tsv, resolved against --repo, so the bare form already grades against this archive's ledger. python3 path_claim_check.py --selftest # 20 cells python3 path_claim_check.py --repo . --fail-on-missing """ import argparse import collections import json import os import re import subprocess import sys DEFAULT_REPO = "/root/troupe-standup" # The ledger this archive carries, REPO-relative: resolved against --repo, not # against the caller's cwd, so a bare run against a clone reads that clone's # ledger. Until 2026-10-21 this was advertised in USAGE and never applied -- # `--baseline` had no default, so a bare invocation graded against an empty # ledger and, under --fail-on-missing, returned rc 1 for an archive whose own # rows excuse seven of the ten MISSING paths. A default a reader can cite and # the code does not apply is the worst kind: it makes the tool lie in the # direction of a defect. DEFAULT_BASELINE = "tools/path_claim_baseline.tsv" # The extensions this archive actually names. A closed list, for the reason # citation_resolve_check.py gives: an open `\\.[a-z]+` swallows clock times, # decimals and domain names. EXT = (r"md|markdown|py|json|txt|js|mjs|cjs|sh|bash|yml|yaml|html|css|csv|tsv|" r"jsonl|sql|db|sqlite|age|wav|mp3|mp4|gz|tgz|toml|cfg|ini|log|out|bak") # At least one '/' is REQUIRED: a bare filename ("index.html") is not a claim # about a location, and the archive names dozens of those for pages it does not # hold. The trailing guard stops `x.md` from matching the front of `x.md5`. PATH = re.compile(r"(?P/?(?:[A-Za-z0-9_.@+-]+/)+[A-Za-z0-9_.@+-]+\.(?:%s))" r"(?![A-Za-z0-9])" % EXT) FENCE = re.compile(r"^\s*(```|~~~)") HOST = re.compile(r"^(?:/|~/|\.\./)") TEMPLATE = re.compile(r"YYYY|<[^>]*>|\{[^}]*\}|NNN") STRIP = "`*'\"()[]<>,;:." # The closed class set a baseline row may name. Each is a reason a MISSING path # is not a live defect; adding one is a deliberate edit to this tuple. # EXAMPLE was added after the tool's own construction record landed and the next # census found 3 new MISSING paths -- two quoted metasyntactic illustrations # ("recoveries/foo.md", "the record docs/bar.md") and one red-probe filename # that was planted in a detached worktree and never ref'd. None of the other # classes was true of them, and the closed set is the place that says so. BASELINE_CLASSES = ("PLANTED", "NOT_ADDED", "RENAMED", "SHORT_NAME", "FOREIGN_TREE", "EXAMPLE") VERDICTS = ("EXISTS", "MISSING", "EXTERNAL", "TEMPLATE") class CannotGrade(Exception): """An input this tool refuses to grade: the caller turns it into rc 2.""" def git(repo, *args): return subprocess.run(["git", "-C", repo, *args], capture_output=True) def precondition(repo, rev): """None when `repo` is a git work tree carrying `rev`, else why not. Every reader below swallows git's status, so an unreadable repo or a mistyped revision would come back as an empty tree and print a clean census of nothing -- the silent-pass class selftest_sweep.py's malformed axis exists to catch. """ if not os.path.isdir(repo): return "cannot read repo %s: no such directory" % repo p = git(repo, "rev-parse", "--is-inside-work-tree") if p.returncode != 0 or p.stdout.strip() != b"true": return "cannot read repo %s: not a git work tree" % repo q = git(repo, "rev-parse", "--verify", rev + "^{commit}") if q.returncode != 0: why = q.stderr.decode("utf-8", "replace").strip().splitlines() return "cannot read %s at %s: %s" % (repo, rev, why[-1] if why else "git rev-parse failed") return None def clean(token): """Strip the punctuation a path wears when it sits in prose or in a span.""" p = token.strip(STRIP) while p.startswith("./"): p = p[2:].strip(STRIP) return p def classify(path, tracked, tops): if TEMPLATE.search(path): return "TEMPLATE" if HOST.match(path) or path.split("/")[0] not in tops: return "EXTERNAL" return "EXISTS" if path in tracked else "MISSING" def fence_mask(text): """1-based line numbers that are a fence marker or sit inside a fence.""" out, in_fence = set(), False for i, raw in enumerate(text.split("\n"), 1): if FENCE.match(raw): out.add(i) in_fence = not in_fence continue if in_fence: out.add(i) return out def scan_text(text, where, tracked, tops): """Rows for one document -> ({path: row}, counter). `where` is the document's name, used only to build the first-cite handle. Line numbers are the document's own, so a row can be read back at --rev. """ rows, stats = {}, collections.Counter() mask = fence_mask(text) stats["fenced_lines"] = len(mask) for i, line in enumerate(text.split("\n"), 1): if i in mask: continue for m in PATH.finditer(line): p = clean(m.group("path")) if not p: continue stats["occurrences"] += 1 row = rows.get(p) if row is None: row = rows[p] = {"path": p, "mentions": 0, "first": "%s:%d" % (where, i), "context": line.strip()[:180], "verdict": classify(p, tracked, tops)} row["mentions"] += 1 return rows, stats def scan(repo, rev, corpus, store): all_rows, stats = {}, collections.Counter() for f in corpus: text = store.read(f).decode("utf-8", "replace") rows, st = scan_text(text, f, store.tree, store.tops) stats.update(st) for p, row in rows.items(): if p in all_rows: all_rows[p]["mentions"] += row["mentions"] else: all_rows[p] = row return all_rows, stats class Store: def __init__(self, repo, rev): self.repo, self.rev = repo, rev self.tree = set(git(repo, "ls-tree", "-r", "--name-only", rev) .stdout.decode("utf-8", "replace").splitlines()) self.tops = {p.split("/")[0] for p in self.tree} def read(self, path): p = git(self.repo, "show", "%s:%s" % (self.rev, path)) if p.returncode != 0: raise CannotGrade("cannot read %s at %s" % (path, self.rev)) return p.stdout def load_baseline(path): """`\\t\\t` -> {path: (class, reason)}. Unreadable is rc 2.""" try: with open(path, "r", encoding="utf-8") as fh: text = fh.read() except OSError as exc: raise CannotGrade("cannot read baseline %s: %s" % (path, exc)) except UnicodeDecodeError as exc: raise CannotGrade("cannot read baseline %s: not UTF-8: %s" % (path, exc)) out = {} for n, line in enumerate(text.splitlines(), 1): s = line.rstrip("\r") if not s.strip() or s.lstrip().startswith("#"): continue parts = s.split("\t") if len(parts) != 3: raise CannotGrade("cannot read baseline %s:%d: not " "\\t\\t: %r" % (path, n, s)) name, cls, reason = (x.strip() for x in parts) if not name or not reason: raise CannotGrade("cannot read baseline %s:%d: empty path or reason: %r" % (path, n, s)) if cls not in BASELINE_CLASSES: raise CannotGrade("cannot read baseline %s:%d: unknown class %r " "(known: %s)" % (path, n, cls, ", ".join(BASELINE_CLASSES))) out[name] = (cls, reason) return out def apply_baseline(rows, ledger): """Attach the ledger to each MISSING row; report what moved on both sides.""" missing = {p: r for p, r in rows.items() if r["verdict"] == "MISSING"} for p, row in missing.items(): if p in ledger: row["baseline"] = {"known": True, "class": ledger[p][0], "reason": ledger[p][1], "new": False} else: row["baseline"] = {"known": False, "class": None, "reason": None, "new": True} cleared = sorted(p for p in ledger if p in rows and rows[p]["verdict"] == "EXISTS") dead = sorted(p for p in ledger if p not in rows) return {"entries": len(ledger), "matched": sum(1 for r in missing.values() if r["baseline"]["known"]), "new": sorted(p for p, r in missing.items() if r["baseline"]["new"]), "cleared": cleared, "dead": dead, "tolerated": collections.Counter(r["baseline"]["class"] for r in missing.values() if r["baseline"]["known"])} def report(rows, stats, rev, sha, corpus, ledger, path, only, context): counts = collections.Counter(r["verdict"] for r in rows.values()) print("path_claim_check: %s at %s (%s)" % ("repo", rev, sha[:7])) print("matcher: /?(?:[A-Za-z0-9_.@+-]+/)+[A-Za-z0-9_.@+-]+\\.(closed ext list), " "at least one '/' required") print(" graded only when the first component is a tracked top-level entry; " "fenced blocks skipped") print("corpus: %d markdown file(s)" % len(corpus)) print("paths: %d distinct / %d occurrence(s) (unit: distinct paths; " "occurrences beside)" % (len(rows), stats["occurrences"])) for v in VERDICTS: print(" %-9s %4d distinct" % (v, counts[v])) print("fenced skipped: %d line(s)" % stats["fenced_lines"]) if ledger is not None: print("baseline %s: %d entries, %d matched, %d new, %d cleared, %d dead row(s)" % (path, ledger["entries"], ledger["matched"], len(ledger["new"]), len(ledger["cleared"]), len(ledger["dead"]))) for cls, n in sorted(ledger["tolerated"].items()): print(" tolerated %-13s %d" % (cls, n)) for p in ledger["cleared"]: print(" CLEARED %s -- ledger row, now in the tree; the row can go" % p) for p in ledger["dead"]: print(" DEAD_ROW %s -- ledger row, no longer in the census; the row can go" % p) sel = {"missing": lambda r: r["verdict"] == "MISSING", "exists": lambda r: r["verdict"] == "EXISTS", "external": lambda r: r["verdict"] == "EXTERNAL", "template": lambda r: r["verdict"] == "TEMPLATE", "all": lambda r: True}[only] show = sorted((r for r in rows.values() if sel(r)), key=lambda r: (-r["mentions"], r["path"])) print("--- %s: %d distinct path(s) (showing first 300) ---" % (only, len(show))) for r in show[:300]: tag = "" if r["verdict"] == "MISSING" and r.get("baseline"): b = r["baseline"] tag = (" [%s: %s]" % (b["class"], b["reason"])) if b["known"] \ else " [NEW -- not in the baseline]" print(" %3dx %-58s first %s%s" % (r["mentions"], r["path"], r["first"], tag)) if context: print(" ...%s..." % r["context"]) def resolve_baseline(repo, path): """The ledger to grade against, or None when this repo carries none. `path` is repo-relative by contract (DEFAULT_BASELINE), so it is joined onto `repo` before the existence test: a bare run from another cwd must read the tree it is grading, never a file beside the caller. An absolute path is taken as given. A caller who NAMED a ledger that is not there gets the path back unchanged, so load_baseline refuses it with rc 2 -- a mistyped ledger must never read as "no ledger". A repo that simply carries none (a fixture tree) is a stated reading: one note on stderr, no ledger, and the census still prints, because the count is the finding. """ if not path: return None cand = path if os.path.isabs(path) else os.path.join(repo, path) if os.path.exists(cand): return cand if path == DEFAULT_BASELINE: print("note: no ledger at %s -- every MISSING path counts as new" % cand, file=sys.stderr) return None return cand def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--repo", default=DEFAULT_REPO) ap.add_argument("--rev", default="HEAD") ap.add_argument("--corpus", nargs="*") ap.add_argument("--json") ap.add_argument("--only", choices=["missing", "exists", "external", "template", "all"], default="missing") ap.add_argument("--context", action="store_true") ap.add_argument("--fail-on-missing", action="store_true") ap.add_argument("--baseline", default=DEFAULT_BASELINE, help="ledger to grade MISSING rows against (default: " "%s, resolved against --repo)" % DEFAULT_BASELINE) ap.add_argument("--selftest", action="store_true") a = ap.parse_args() if a.selftest: return selftest() problem = precondition(a.repo, a.rev) if problem: print(problem, file=sys.stderr) return 2 try: store = Store(a.repo, a.rev) sha = git(a.repo, "rev-parse", a.rev).stdout.decode().strip() corpus = a.corpus if a.corpus is not None else \ sorted(p for p in store.tree if p.endswith((".md", ".markdown"))) if not corpus: raise CannotGrade("cannot grade: no markdown corpus at %s in %s" % (a.rev, a.repo)) ledger = resolve_baseline(a.repo, a.baseline) ledger = load_baseline(ledger) if ledger else None rows, stats = scan(a.repo, a.rev, corpus, store) except CannotGrade as exc: print(str(exc), file=sys.stderr) return 2 led = apply_baseline(rows, ledger) if ledger is not None else None if led is None: # No ledger: every MISSING path is new, and the count is the finding. led_view = None else: led_view = led report(rows, stats, a.rev, sha, corpus, led_view, a.baseline, a.only, a.context) missing = [r for r in rows.values() if r["verdict"] == "MISSING"] new = len(missing) if led is None else len(led["new"]) print("\npath_claim_check: %d distinct paths, %d missing, %d new against the baseline" % (len(rows), len(missing), new)) if a.json: try: with open(a.json, "w") as fh: json.dump({"repo": a.repo, "rev": a.rev, "sha": sha, "corpus": corpus, "stats": dict(stats), "baseline": led, "paths": sorted(rows.values(), key=lambda r: r["path"])}, fh, indent=1) except OSError as exc: print("cannot write --json " + a.json + ": " + str(exc), file=sys.stderr) return 2 print("wrote %s" % a.json) if a.fail_on_missing and new: print("FAIL: %d missing path(s) the baseline does not name" % new) return 1 return 0 # --------------------------------------------------------------- self-test # The fixtures are graded by the same scan_text() the corpus is, so a cell that # passes here is a statement about the shipped matcher, not about a copy of it. TRACKED = {"docs/real.md", "tools/t.py", "notes/n.md", "WORK-QUEUE.md"} TOPS = {"docs", "tools", "notes"} def _verdicts(text): rows, _ = scan_text(text, "fixture.md", TRACKED, TOPS) return {p: r["verdict"] for p, r in rows.items()} def _rows(text): return scan_text(text, "fixture.md", TRACKED, TOPS)[0] def selftest(): n = bad = 0 def cell(name, ok, detail=""): nonlocal n, bad n += 1 bad += 0 if ok else 1 print("%s %s%s" % ("PASS" if ok else "FAIL", name, "" if ok else " <- " + detail)) cell("c1 a tracked path is EXISTS", _verdicts("see docs/real.md for the table") == {"docs/real.md": "EXISTS"}, str(_verdicts("see docs/real.md for the table"))) cell("c2 an in-namespace path not in the tree is MISSING", _verdicts("the record `docs/absent.md` was written") == {"docs/absent.md": "MISSING"}, str(_verdicts("the record `docs/absent.md` was written"))) cell("c3 a component this repo does not track is EXTERNAL, not MISSING", _verdicts("kept at crucible/q51/helm-selftest-q51.py") == {"crucible/q51/helm-selftest-q51.py": "EXTERNAL"}, str(_verdicts("kept at crucible/q51/helm-selftest-q51.py"))) cell("c4 an absolute host path is EXTERNAL", _verdicts("under /root/crucible/q39/gate.py today") == {"/root/crucible/q39/gate.py": "EXTERNAL"}, str(_verdicts("under /root/crucible/q39/gate.py today"))) cell("c5 a placeholder path is TEMPLATE, not MISSING", _verdicts("one file per day: checkins/YYYY-MM-DD-volt.md") == {"checkins/YYYY-MM-DD-volt.md": "TEMPLATE"}, str(_verdicts("one file per day: checkins/YYYY-MM-DD-volt.md"))) cell("c6 a fenced path is quoted material, not a claim", _verdicts("```\ndocs/absent.md\n```") == {}, str(_verdicts("```\ndocs/absent.md\n```"))) cell("c7 the fence closes: the same path in prose is graded again", _verdicts("```\ndocs/absent.md\n```\nsee docs/absent.md") == {"docs/absent.md": "MISSING"}, str(_verdicts("```\ndocs/absent.md\n```\nsee docs/absent.md"))) cell("c8 a bare filename with no directory is not a path claim", _verdicts("the page index.html and blocks/index.html") == {"blocks/index.html": "EXISTS" if "blocks/index.html" in TRACKED else "EXTERNAL"}, str(_verdicts("the page index.html and blocks/index.html"))) cell("c9 trailing punctuation and backticks are stripped", list(_verdicts("`docs/absent.md`.").keys()) == ["docs/absent.md"], str(_verdicts("`docs/absent.md`."))) r = _rows("docs/absent.md once, then docs/absent.md again") cell("c10 occurrences aggregate onto one row with its first cite", len(r) == 1 and r["docs/absent.md"]["mentions"] == 2 and r["docs/absent.md"]["first"] == "fixture.md:1", str(r)) # --- the CLI contract, on a real fixture repo ------------------------- import tempfile td = tempfile.mkdtemp(prefix="pathclaim-") repo = os.path.join(td, "repo") os.makedirs(os.path.join(repo, "docs")) os.makedirs(os.path.join(repo, "tools")) with open(os.path.join(repo, "docs", "real.md"), "w") as fh: fh.write("real\n") with open(os.path.join(repo, "tools", "t.py"), "w") as fh: fh.write("# t\n") with open(os.path.join(repo, "WORK-QUEUE.md"), "w") as fh: fh.write("the record `docs/absent.md` and the real docs/real.md\n") subprocess.run(["git", "init", "-q", repo], capture_output=True) subprocess.run(["git", "-C", repo, "add", "-A"], capture_output=True) subprocess.run(["git", "-C", repo, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "fixture"], capture_output=True) def run(*args): p = subprocess.run([sys.executable, os.path.abspath(__file__), *args], capture_output=True, text=True) return p.returncode, p.stdout + p.stderr rc, out = run("--repo", repo) cell("c11 the census alone is rc 0 even with a missing path", rc == 0 and "1 missing" in out, "rc=%d %s" % (rc, out[-200:])) rc, out = run("--repo", repo, "--fail-on-missing") cell("c12 --fail-on-missing is rc 1 on an unbaselined missing path", rc == 1 and "FAIL: 1 missing path" in out, "rc=%d %s" % (rc, out[-200:])) bl = os.path.join(td, "baseline.tsv") with open(bl, "w") as fh: fh.write("# fixture\n" + "docs/absent.md\tPLANTED\tfixture row\n") rc, out = run("--repo", repo, "--fail-on-missing", "--baseline", bl) cell("c13 a baselined missing path is tolerated, and still printed", rc == 0 and "PLANTED" in out and "1 matched" in out, "rc=%d %s" % (rc, out[-300:])) with open(os.path.join(td, "cleared.tsv"), "w") as fh: fh.write("docs/real.md\tRENAMED\tfixture: the row is now in the tree\n") rc, out = run("--repo", repo, "--baseline", os.path.join(td, "cleared.tsv")) cell("c14 a ledger row whose path now exists is CLEARED and named", rc == 0 and "CLEARED" in out and "docs/real.md" in out, "rc=%d %s" % (rc, out[-300:])) rc, out = run("--repo", td) cell("c15 a directory that is not a git work tree is rc 2", rc == 2 and "not a git work tree" in out, "rc=%d %s" % (rc, out[-200:])) rc, out = run("--repo", repo, "--rev", "nosuchrev") cell("c16 a revision that does not resolve is rc 2", rc == 2 and "cannot read" in out, "rc=%d %s" % (rc, out[-200:])) empty = os.path.join(td, "empty") os.makedirs(empty) with open(os.path.join(empty, "a.txt"), "w") as fh: fh.write("no markdown here\n") subprocess.run(["git", "init", "-q", empty], capture_output=True) subprocess.run(["git", "-C", empty, "add", "-A"], capture_output=True) subprocess.run(["git", "-C", empty, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "fixture"], capture_output=True) rc, out = run("--repo", empty) cell("c17 a corpus with no markdown is rc 2, never an empty clean census", rc == 2 and "no markdown corpus" in out, "rc=%d %s" % (rc, out[-200:])) # --- the ledger's closed set is enforced, not decorative ---------------- badcls = os.path.join(td, "badclass.tsv") with open(badcls, "w") as fh: fh.write("docs/absent.md\tMADE_UP\tfixture: a class the tool does not know\n") rc, out = run("--repo", repo, "--fail-on-missing", "--baseline", badcls) cell("c18 a ledger row naming a class outside the closed set is rc 2", rc == 2 and "unknown class" in out and "MADE_UP" in out, "rc=%d %s" % (rc, out[-300:])) excls = os.path.join(td, "example.tsv") with open(excls, "w") as fh: fh.write("docs/absent.md\tEXAMPLE\tfixture: a metasyntactic illustration\n") rc, out = run("--repo", repo, "--fail-on-missing", "--baseline", excls) cell("c19 the EXAMPLE class is accepted and its row still printed", rc == 0 and "tolerated EXAMPLE" in out and "1 matched" in out, "rc=%d %s" % (rc, out[-300:])) # --- the default ledger is the one the tree carries --------------------- # Until 2026-10-21 `--baseline` had NO default while USAGE advertised one: # a bare run graded against an empty ledger, so `--fail-on-missing` returned # rc 1 for a tree whose own rows excused the path. This pins bare == named. with open(os.path.join(repo, "tools", "path_claim_baseline.tsv"), "w") as fh: fh.write("# fixture: the ledger this tree carries\n" "docs/absent.md\tPLANTED\tfixture row, carried by the tree\n") subprocess.run(["git", "-C", repo, "add", "-A"], capture_output=True) subprocess.run(["git", "-C", repo, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "ledger"], capture_output=True) def summary(text): return [ln for ln in text.splitlines() if ln.startswith("path_claim_check:")] bare_rc, bare_out = run("--repo", repo, "--fail-on-missing") named_rc, named_out = run("--repo", repo, "--fail-on-missing", "--baseline", os.path.join(repo, "tools", "path_claim_baseline.tsv")) cell("c20 the repo's own ledger is the default: bare run == named run", bare_rc == named_rc == 0 and summary(bare_out) == summary(named_out) and "1 matched" in bare_out, "bare rc=%d %r | named rc=%d %r" % (bare_rc, summary(bare_out), named_rc, summary(named_out))) print("selftest: %d cells, %d passed, %d failed" % (n, n - bad, bad)) return 0 if bad == 0 else 1 if __name__ == "__main__": sys.exit(main())