From 9732094dfd4caf6db153f1b6b3d41ffd83969d25 Mon Sep 17 00:00:00 2001 From: Volt Date: Sat, 24 Oct 2026 02:06:26 +0000 Subject: [PATCH] volt: path_claim_check learns --rev WORKTREE (index inventory, disk bytes; cells 27->31); verify gains leg 2 path-claims (8 legs, cells 55->59) --- tools/path_claim_check.py | 118 ++++++++++++++++++++++++++++++++++-- tools/selftest_baseline.tsv | 4 +- tools/verify.py | 80 ++++++++++++++++++++---- 3 files changed, 182 insertions(+), 20 deletions(-) diff --git a/tools/path_claim_check.py b/tools/path_claim_check.py index fd5d032..3230bf2 100644 --- a/tools/path_claim_check.py +++ b/tools/path_claim_check.py @@ -96,6 +96,30 @@ THE BASELINE, AND WHY THE FIRST CENSUS IS ITS LEDGER check rather than a word that closes a red. A claim that something EXISTS is still MISSING until the file does. +REVISIONS -- AND WHY `WORKTREE` IS NOT ONE OF THEM + `--rev` selects the tree the corpus is read from AND the tree existence is + judged against, so the claim and its oracle come from the same revision. The + default is `HEAD`. + + `--rev WORKTREE` reads the files on disk and lists them from the INDEX, so it + grades the tree you are about to commit. It is not a revision and has no tree + object: `git rev-parse --verify WORKTREE^{commit}` fails, which is why this + flag could not be spelled until now. The two modes were measured apart in the + selftest before this paragraph existed, on one fixture document: a path claim + that lives only in the staged tree is invisible at `HEAD` (rc 0, no MISSING + row) and is a graded MISSING row under `WORKTREE` (rc 1 under + --fail-on-missing). That window -- the tick between writing prose and + committing it -- is exactly where the archive writes every claim it makes, so + a grader that could only read HEAD could not grade a lane's own tick. The + convention is `doc_command_check.py`'s and `tool_map.py`'s, and the reason is + the same in all three: grade the tree you are about to commit, not the last + one. + + A path that exists on disk but is in neither the index nor HEAD is still + MISSING under `WORKTREE`: the inventory is the index, so `git add` before the + gate is the discipline, not a workaround. Untracked debris under a scratch + tree therefore cannot make a claim read EXISTS. + 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 @@ -108,13 +132,14 @@ EXIT CODES that file is graded with no ledger after one note on stderr. USAGE - python3 path_claim_check.py [--repo DIR] [--rev HEAD] [--corpus PATH...] + python3 path_claim_check.py [--repo DIR] [--rev HEAD|WORKTREE] [--corpus PATH...] [--json OUT] [--only missing|exists|docrel|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 # 27 cells + python3 path_claim_check.py --selftest # 31 cells python3 path_claim_check.py --repo . --fail-on-missing + python3 path_claim_check.py --repo . --rev WORKTREE --fail-on-missing python3 path_claim_check.py --repo . --only docrel --context """ import argparse @@ -138,6 +163,11 @@ DEFAULT_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # direction of a defect. DEFAULT_BASELINE = "tools/path_claim_baseline.tsv" +# The pseudo-revision `doc_command_check.py` and `tool_map.py` already carry. +# Spelled once, here, so a reader greps one name and finds every branch that +# treats it as a state rather than as a commit. +WORKTREE = "WORKTREE" + # 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. @@ -199,6 +229,11 @@ def precondition(repo, rev): 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 + if rev == WORKTREE: + # WORKTREE is a state, not a revision: it has no commit to resolve to. + # Everything below reads the index (inventory) and the disk (bytes), so + # the work-tree test just made is the whole precondition. + return None q = git(repo, "rev-parse", "--verify", rev + "^{commit}") if q.returncode != 0: why = q.stderr.decode("utf-8", "replace").strip().splitlines() @@ -325,13 +360,38 @@ def scan(repo, rev, corpus, store): class Store: + """The inventory and the bytes, at one revision or at the staged work tree. + + WORKTREE is two reads, not one: the INVENTORY is the index (`ls-files`), so + only what will actually be committed can make a claim read EXISTS, and the + BYTES are the disk, so an edit that is not staged yet is still graded as the + text a reader will see. A path the index lists but the disk has lost falls + back to the index blob -- that is byte-for-byte what the commit would carry, + and it is the only honest answer to "does this exist in the tree I am about + to commit". + """ + 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()) + if rev == WORKTREE: + raw = git(repo, "ls-files", "-z").stdout.decode("utf-8", "replace") + names = [p for p in raw.split("\0") if p] + else: + names = git(repo, "ls-tree", "-r", "--name-only", rev) \ + .stdout.decode("utf-8", "replace").splitlines() + self.tree = set(names) self.tops = {p.split("/")[0] for p in self.tree} def read(self, path): + if self.rev == WORKTREE: + try: + with open(os.path.join(self.repo, path), "rb") as fh: + return fh.read() + except OSError: + p = git(self.repo, "show", ":" + path) + if p.returncode != 0: + raise CannotGrade("cannot read %s at WORKTREE" % path) + return p.stdout p = git(self.repo, "show", "%s:%s" % (self.rev, path)) if p.returncode != 0: raise CannotGrade("cannot read %s at %s" % (path, self.rev)) @@ -394,7 +454,10 @@ def apply_baseline(rows, ledger): 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])) + # WORKTREE has no commit of its own, so the sha printed is the HEAD it sits + # on, labelled as such: a reader must not mistake it for the graded tree. + where = ("HEAD " + sha[:7] + ", worktree graded") if rev == WORKTREE else sha[:7] + print("path_claim_check: %s at %s (%s)" % ("repo", rev, where)) 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; " @@ -494,7 +557,8 @@ def main(): return 2 try: store = Store(a.repo, a.rev) - sha = git(a.repo, "rev-parse", a.rev).stdout.decode().strip() + sha = git(a.repo, "rev-parse", + "HEAD" if a.rev == WORKTREE else 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: @@ -657,6 +721,48 @@ def selftest(): 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:])) + # --- WORKTREE: the tree you are about to commit, not the last commit ---- + # One fixture document in three states: the committed claim (HEAD), an + # unstaged edit on disk, and a claim that lives only in the index. Each cell + # names which of the two reads it measures -- the inventory or the bytes. + q = os.path.join(repo, "WORK-QUEUE.md") + with open(q) as fh: + committed_txt = fh.read() + with open(q, "w") as fh: + fh.write("the real docs/real.md, with the committed claim gone\n") + _, out_h = run("--repo", repo) + _, out_w = run("--repo", repo, "--rev", "WORKTREE") + cell("c17a WORKTREE reads the bytes on disk; HEAD reads the blob", + "1 missing" in out_h and "0 missing" in out_w, + "HEAD %r | WORKTREE %r" % (out_h[-140:], out_w[-140:])) + with open(os.path.join(repo, "docs", "new.md"), "w") as fh: + fh.write("new\n") + with open(q, "w") as fh: + fh.write("the record now cites docs/new.md\n") + subprocess.run(["git", "-C", repo, "add", "-A"], capture_output=True) + _, out_h = run("--repo", repo, "--only", "exists") + _, out_w = run("--repo", repo, "--rev", "WORKTREE", "--only", "exists") + cell("c17b a path staged but never committed is invisible at HEAD, EXISTS under WORKTREE", + "docs/new.md" not in out_h and "docs/new.md" in out_w, + "HEAD %r | WORKTREE %r" % (out_h[-200:], out_w[-200:])) + with open(q, "w") as fh: + fh.write("the record cites docs/ghost.md\n") + subprocess.run(["git", "-C", repo, "add", "-A"], capture_output=True) + rc_h, out_h = run("--repo", repo, "--fail-on-missing", "--baseline", bl) + rc_w, out_w = run("--repo", repo, "--rev", "WORKTREE", + "--fail-on-missing", "--baseline", bl) + cell("c17c a claim only the staged tree makes: rc 0 at HEAD, rc 1 under WORKTREE", + rc_h == 0 and rc_w == 1 and "FAIL: 1 missing path" in out_w, + "HEAD rc=%d | WORKTREE rc=%d %s" % (rc_h, rc_w, out_w[-200:])) + rc, out = run("--repo", td, "--rev", "WORKTREE") + cell("c17d WORKTREE outside a git work tree is rc 2, not a crash", + rc == 2 and "not a git work tree" in out, "rc=%d %s" % (rc, out[-200:])) + # Put the fixture back the way the committed cells below expect it. + with open(q, "w") as fh: + fh.write(committed_txt) + os.remove(os.path.join(repo, "docs", "new.md")) + subprocess.run(["git", "-C", repo, "add", "-A"], capture_output=True) + # --- the ledger's closed set is enforced, not decorative ---------------- badcls = os.path.join(td, "badclass.tsv") with open(badcls, "w") as fh: diff --git a/tools/selftest_baseline.tsv b/tools/selftest_baseline.tsv index acd12a2..5168032 100644 --- a/tools/selftest_baseline.tsv +++ b/tools/selftest_baseline.tsv @@ -20,7 +20,7 @@ doc_census_check.py 104 cells doc_command_check.py 18 cells gate_blame.py 21 cells landing_check.py 26 cells -path_claim_check.py 27 cells +path_claim_check.py 31 cells queue_append_audit.py 25 cells queue_claim_check.py 24 fixtures queue_index.py 58 cells @@ -28,5 +28,5 @@ reply_check.py 49 cells selftest_sweep.py 39 cells served_drift_check.py 25 cells tool_map.py 20 cells -verify.py 55 cells +verify.py 59 cells write_probe.py 15 cells diff --git a/tools/verify.py b/tools/verify.py index 45ab8a6..57dc956 100644 --- a/tools/verify.py +++ b/tools/verify.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""verify.py -- the HOME the index gate did not have: one command, seven legs, +"""verify.py -- the HOME the index gate did not have: one command, eight legs, one exit code, and a row per leg so a partial run cannot look like a clean one. WHY THIS EXISTS @@ -45,6 +45,18 @@ WHAT IT RUNS -- one row per leg, cheapest first, never in parallel unknown handles, dates, emptiness), where the ledger states the faults the archive already carried; only a class it does not name is a fault. `--checkin-day DATE` adds a `--since` floor + path-claims tools/path_claim_check.py --rev WORKTREE --fail-on-missing + every path this archive's prose names, at the tree about to be + committed: the INVENTORY is the index, the BYTES are the disk, + so a claim written this tick and not yet committed is graded + rather than invisible. `tools/path_claim_baseline.tsv` states + the readings already reviewed; only a MISSING path the ledger + does not name is a fault, and every tolerated row is still + printed. WORKTREE, not HEAD, for the reason doc-commands gives + below -- HEAD cannot see the tick a lane is writing in -- and + the reason the leg exists at all: the instrument shipped + 2026-10-20 could only read a commit, so it could not grade the + only tree that matters before the commit was made reply-headings tools/reply_check.py --baseline tools/reply_baseline.tsv every reply's heading against its own filename (addressee and number), on the same ledger rule: `replies/` is the second @@ -146,7 +158,7 @@ TOOLS ARE RESOLVED FROM --repo, NOT FROM THIS FILE'S DIRECTORY The path must still be the ROOT: a subdirectory is rc 2 naming the root, because widening `--repo` silently would grade a tree nobody named. -`--selftest` fixture corpus, 55 cells +`--selftest` fixture corpus, 59 cells """ import argparse @@ -170,6 +182,10 @@ LEGS = ( {"key": "checkin", "tool": "checkin_check.py", "slow": False, "what": "grade the newest day's check-ins against the naming and " "duplicate rules"}, + {"key": "path-claims", "tool": "path_claim_check.py", "slow": False, + "what": "every path the archive's prose names, graded at the tree about to " + "be committed (index inventory, disk bytes), against the ledger " + "that states the readings already reviewed"}, {"key": "reply-headings", "tool": "reply_check.py", "slow": False, "what": "grade every reply's heading against its own filename"}, {"key": "index-current", "tool": "queue_index.py", "slow": False, @@ -345,6 +361,17 @@ def leg_argv(key, repo, queue, index, checkin_day=None): if checkin_day: argv += ["--since", checkin_day] return argv + if key == "path-claims": + # WORKTREE, and the ledger from the tree being graded: a revision is + # graded against the tolerance it committed, and a lane is graded + # against the tree it is about to commit. HEAD would grade the last + # commit while the tick's own prose went unread -- the blind spot this + # leg exists to close. + return [sys.executable, os.path.join(repo, "tools", + "path_claim_check.py"), + "--repo", repo, "--rev", "WORKTREE", "--fail-on-missing", + "--baseline", os.path.join(repo, "tools", + "path_claim_baseline.tsv")] if key == "reply-headings": # The ledger comes from the tree being graded, for the same reason the # check-in ledger does: a revision is graded with the tolerance it @@ -624,7 +651,8 @@ def _verify(*argv): return proc.returncode, (proc.stdout or "") + (proc.stderr or "") -GREEN_ALL = {"checkin_check.py": 0, "reply_check.py": 0, "queue_index.py": 0, +GREEN_ALL = {"checkin_check.py": 0, "path_claim_check.py": 0, + "reply_check.py": 0, "queue_index.py": 0, "queue_append_audit.py": 0, "doc_command_check.py": 0, "selftest_sweep.py": 0, "write_probe.py": 0} @@ -643,11 +671,11 @@ def selftest(): failures.append("%s\n got: %r\n want: %r" % (label, got, want)) with tempfile.TemporaryDirectory(prefix="verify-selftest-") as td: - # 1. seven green stubs -> rc 0 + # 1. eight green stubs -> rc 0 repo = _fixture(os.path.join(td, "c1"), GREEN_ALL) rc, out = _verify("--repo", repo) - check("1 all seven legs green -> rc 0", rc, 0) - check("1b summary counts the seven legs", "verify: 7 legs, 7 pass, 0 fault, " + check("1 all eight legs green -> rc 0", rc, 0) + check("1b summary counts the eight legs", "verify: 8 legs, 8 pass, 0 fault, " "0 ungradable, 0 skipped" in out, True) # 2. a stale index is a FAULT, and its own line reaches the row @@ -702,8 +730,8 @@ def selftest(): rc, out = _verify("--repo", repo, "--json", jpath) obj = json.load(open(jpath)) if os.path.exists(jpath) else {} check("7 --json writes the rows", [r["key"] for r in obj.get("rows", [])], - ["checkin", "reply-headings", "index-current", "append-audit", - "doc-commands", "selftest-sweep", "write-axis"]) + ["checkin", "path-claims", "reply-headings", "index-current", + "append-audit", "doc-commands", "selftest-sweep", "write-axis"]) check("7b --json carries the same rc the process returned", obj.get("rc"), rc) @@ -732,9 +760,10 @@ def selftest(): dict(GREEN_ALL, **{"queue_index.py": 1}), outs={"queue_index.py": "STALE: line 1\n"}) rc, out = _verify("--repo", repo) - order = [out.find(k) for k in ("checkin", "reply-headings", "index-current", - "append-audit", "doc-commands", - "selftest-sweep", "write-axis")] + order = [out.find(k) for k in ("checkin", "path-claims", "reply-headings", + "index-current", "append-audit", + "doc-commands", "selftest-sweep", + "write-axis")] check("11 legs print in fixed order", order == sorted(order) and -1 not in order, True) @@ -746,7 +775,7 @@ def selftest(): check("12 skipped leg is its own count in --json", obj.get("counts", {}).get("SKIP"), 1) check("12b and the skipped leg is not counted as a pass", - obj.get("counts", {}).get(PASS), 6) + obj.get("counts", {}).get(PASS), 7) # 13. the detail line must not contradict the verdict: a row graded # FAULT beside "0 fatal" is the SILENT failure this gate exists to catch. @@ -945,6 +974,33 @@ def selftest(): check("29b the fault row is named doc-commands and carries its reason", "fault: doc-commands" in out and "FLAG_NOT_ACCEPTED" in out, True) + # 30. THE PATH-CLAIMS LEG, and the tree it must read. The tool is called + # with `--rev WORKTREE --fail-on-missing` and the ledger taken from the + # tree being graded. HEAD cannot see the tick a lane writes in, so a leg + # reading HEAD would grade the last commit while this tick's new prose + # went unread -- measured apart from the mode itself in + # path_claim_check.py's c17a-c17c. + av = os.path.join(td, "c30-argv") + repo = _fixture(os.path.join(td, "c30"), GREEN_ALL, + argv_markers={"path_claim_check.py": av}) + rc, out = _verify("--repo", repo) + seen = open(av).read() if os.path.exists(av) else "" + check("30 the path-claims leg runs --rev WORKTREE --fail-on-missing", + "--rev WORKTREE" in seen and "--fail-on-missing" in seen, True) + check("30b and it grades against the ledger inside the tree it grades", + os.path.join(repo, "tools", "path_claim_baseline.tsv") in seen, True) + + # 31. a path claim the ledger does not name is a FAULT on the gate, named + # on the row -- the class that was a report nobody ran until this leg. + repo = _fixture(os.path.join(td, "c31"), + dict(GREEN_ALL, **{"path_claim_check.py": 1}), + outs={"path_claim_check.py": + "FAIL: 1 missing path(s) the baseline does not name\n"}) + rc, out = _verify("--repo", repo) + check("31 an unledgered missing path -> rc 1", rc, 1) + check("31b the fault row is named path-claims and carries its reason", + "fault: path-claims" in out and "does not name" in out, True) + # 27. the count in this file's own docstring is a CLAIM, so it is graded # against the count that actually ran. On 2026-10-19 the docstring and # the run said 50 while the argparse help string still said 42 and