diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 5a5a3be..a181abb 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -10586,3 +10586,21 @@ Verification, read at `6cd2114` (the revision this record was written against; a 5. **What is not repaired, named rather than smoothed.** The reply's own line 3 reads *"Mistakes of mine first: none in this lane — this is the first null reply"*, and that sentence is **false**: `replies/2026-10-20-to-null-01.md` and `replies/2026-10-20-to-null-02.md` are two earlier replies to null, both on disk and both in the census. It is another writer's prose, this lane's repair channel is an append and not an edit, so the sentence stands and this line is its correction. Separately, `path_claim_check --fail-on-missing` reads **2 new MISSING rows** — `tools/build_digest.py`, `tools/html_to_text.py`, cited at `wizard-work/sphinx/longcontext-archive-2026-10-21/digest.md:3` and `:125` — and both files do exist at that artifact's own `tools/` directory, so the rows are the tool resolving artifact-relative tokens against the repo root, recorded here and left to that lane. This commit also carries the pending Workhorse artifact-claims record already at the foot of this file (measured before this append, unchanged by it) and its index re-render; it carries **nothing** of forge's four uncommitted `tools/` refactors, which stay in the worktree as their lane left them. — Volt + +**Path-citation base 2026-10-22 (volt, Workhorse) — the census graded every token at the repo root, so an artifact naming its own scripts read `MISSING` while the files sat beside it: a second base that the root base always wins, six rows resolved and printed with the base they used, and the one real rename ledgered.** No item opened or closed; the open set is unchanged (`Q37`'s pipe span, the career-lane `Q53` decision held for Alexander, `Q64`'s design question Alexander's). Appended at the foot; `docs/queue-index.md` re-rendered in the same commit. + +1. **The red, measured at `40f1095` before any edit.** `path_claim_check.py --repo . --fail-on-missing` → **rc 1**, `1,148 distinct paths, 19 missing, 7 new against the baseline`. Six of the seven were a **base mismatch, not a broken citation**: `wizard-work/sphinx/longctx-needle-grounding-2026-10-22/REPORT.md:49-52` names four scripts of its own artifact's `tools/` directory (`fetch.py`, `build_index.py`, `run_retrieval.py`, `build_negative_control.py`), and the 2026-10-21 archive's `digest.md:3`/`:125` name `build_digest.py` and `html_to_text.py` of its own; `git ls-files` on both artifacts' `tools/` directories lists every one of those files at HEAD (`wizard-work/sphinx/longctx-needle-grounding-2026-10-22/tools/fetch.py`, `.../tools/build_index.py`, `.../tools/run_retrieval.py`, `.../tools/build_negative_control.py`, `wizard-work/sphinx/longcontext-archive-2026-10-21/tools/build_digest.py`, `.../tools/html_to_text.py`). The seventh was a real rename: `replies/2026-10-22-to-null-01.md` → `-03` at `7a472a4` (added at `d659219`), cited by `WORK-QUEUE.md:10582` in the record that describes the fix. + +2. **The rule, and why the root base keeps the last word.** `doc_relative()` joins the token onto the citing document's own directory, normalises it, refuses a candidate that climbs out of the tree (`../`), and accepts it only when the result is tracked. The root base is tried **first, always**: a repo-relative citation is never rewritten by the second base, and a document at the repo root has no second base at all. `MISSING` now means neither base holds the token. The two edges are cells, not prose: `c24` pins a root hit staying `EXISTS` when a same-named file also sits beside the document, `c23` pins a root document getting no rescue. + +3. **A distinct verdict, because the base is part of the claim.** `DOCREL` is a resolution, printed on every run as `[DOCREL: resolves at , from :; N cite(s) did not resolve at their own base]`, selectable with `--only docrel`. A path cited from two documents keeps the resolving base and still counts the cites that failed at their own base — which is exactly the `WORK-QUEUE.md:10586` pair: both tokens resolve in the archive's `digest.md`, and the row says `1 cite(s) did not resolve at their own base` rather than hiding the queue's phrasing. A ledger row the second base now resolves is printed `CLEARED_DOCREL` with the base that resolved it, so `apply_baseline()` returns it beside `cleared` instead of leaving it neither cleared nor dead. + +4. **Measured after, same tree.** `1,148 distinct / 4,714 occurrences` — `EXISTS 348, DOCREL 6, MISSING 13, EXTERNAL 771, TEMPLATE 10`, total unchanged — and `13 missing, 0 new against the baseline`, **rc 0**. Six rows moved `MISSING`→`DOCREL` by the second base; the seventh became a ledger row (`replies/2026-10-22-to-null-01.md`, class `RENAMED`, naming `7a472a4`, `d659219` and `WORK-QUEUE.md:10582`). Nothing was silenced: the ledger still prints all 13 tolerated rows with their classes on every run. + +5. **Red first, on the shipped matcher rather than on a copy of it.** `--selftest` grew **21 → 27 cells**; `c22`–`c27` grade the second base and its two refusals. The mutation proof: copy the file, replace `doc_relative()`'s body with `return None`, run the same selftest → `FAIL c22`, `FAIL c26`, `FAIL c27`, `selftest: 27 cells, 24 passed, 3 failed`, rc 1 (`wizard-outputs/volt/2026-10-22-docrel-base/transcript-mutation.txt`). The three cells that stay green under the mutation are the ones asserting the *absence* of a rescue, which is the property the fix must not lose. + +6. **Verification at the commit carrying this record.** `path_claim_check.py --selftest` **27/27 rc 0**; `--repo . --fail-on-missing` **13 missing, 0 new, rc 0**; `verify.py --repo .` **6 legs, 6 pass, 0 fault, 0 ungradable, 0 skipped, rc 0**; `doc_command_check.py --repo . --fail` **65 commands over 19 oracles, OK 65**; `doc_census_check.py --repo . --tools tools/path_claim_check.py --fail` **0 INTERNAL SPLIT, 0 STALE**; `tool_map.py --repo . --check docs/TOOLS.md` rc 0, so no derived cell in `docs/TOOLS.md` moved. Artifact `wizard-outputs/volt/2026-10-22-docrel-base/` — `REPORT.md`, `check.sh`, four transcripts, and a `files.sha256` over its own six files (`sh check.sh` → `CHECK: PASS`, rc 0). + +7. **What it does not claim.** `DOCREL` is a fallback, so a genuinely broken root-relative citation is still `MISSING` unless a same-named tracked file happens to sit beside the citing document; where that coincidence occurs the resolved path is printed, and the tool does not adjudicate intent. The second base does not reach a queue line that quotes *another artifact's* relative path from the repo root — that stays visible as an unresolved cite inside a `DOCREL` row, and the prose repair would be an append, not an edit to another lane's text. `path_claim_check.py` is not a `verify.py` leg, so no gate changed meaning here and no seventh leg was added. No Q-item state changed, and nothing here decides anything Alexander holds. + +— Volt diff --git a/docs/queue-index.md b/docs/queue-index.md index a78716c..e82ea3a 100644 --- a/docs/queue-index.md +++ b/docs/queue-index.md @@ -56,7 +56,7 @@ | 46 | Q44 | 6162-6705 | 88496 | opening | The pin class: redirect every constant composed from the live data root, not only `SNAPSHOT_PATH` *(Helm lane; opened 2026-10-12 by the pipe, per Q43's own closing sentence — "extend the redirect … | DONE 2026-10-12 — `recoveries/helm-root-redirect.md` (Merlin, Helm lane); the closure block is at the foot | | 47 | Q48 | 6706-6825 | 15123 | opening | The runtime-resolution redirect: close the class Q45's literals cannot see | DONE 2026-10-13 — `recoveries/helm-runtime-redirect.md` (Merlin, Helm lane), scratch tree | | 48 | Q49 | 6826-7593 | 92050 | opening | The runtime-resolved root's live write path: name it, reproduce it, then close it | open and unclaimed. Opened by the pipe 2026-10-13 | -| 49 | Q45 | 7594-10588 | 541373 | opening | §9 follow-on — the home-directory channel *(Helm lane; Merlin)* | OPEN — awaiting Alexander's zoom/whole-shop answer; build is the pipe's. | +| 49 | Q45 | 7594-10606 | 547023 | opening | §9 follow-on — the home-directory channel *(Helm lane; Merlin)* | OPEN — awaiting Alexander's zoom/whole-shop answer; build is the pipe's. | ## Ids with no `## ` heading @@ -71,9 +71,9 @@ Opened as records inside another section. `line` is the record's own opening lin | Q50 | 6990 | 6990 | 46 | Q49 | BULLET+BOLD | BOLD-LEAD | | Q51 | 7106 | 7106 | 37 | Q49 | BULLET+BOLD | BOLD-LEAD | | Q52 | 7167 | 7167 | 90 | Q49 | BULLET+BOLD | BOLD-LEAD | -| Q53 | 7292 | 7246 | 153 | Q49 | BULLET+BOLD | BOLD-LEAD | +| Q53 | 7292 | 7246 | 154 | Q49 | BULLET+BOLD | BOLD-LEAD | | Q63 | 9918 | 9909 | 17 | Q45 | BULLET+BOLD | BOLD-LEAD | -| Q64 | 10308 | 10308 | 16 | Q45 | BULLET+BOLD | BOLD-LEAD | +| Q64 | 10308 | 10308 | 17 | Q45 | BULLET+BOLD | BOLD-LEAD | **Not in this document.** Ids the prose corpus names that the queue has never mentioned (`checkins/`, `replies/`, `recoveries/`, `audits/`, `docs/`, the top-level documents; `tools/` excluded). That set moves with the prose, not with the queue, so embedding it here would make `--check` go RED for a reason that has nothing to do with the queue being current. It is reported on demand: `python3 tools/queue_index.py --outside`. @@ -218,7 +218,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q53", "line": 7292, - "refs": 153, + "refs": 154, "section": "Q49", "shape": "BULLET+BOLD" }, @@ -236,7 +236,7 @@ The block below is the parse this document renders, byte for byte. `--check` com "form": "BOLD-LEAD", "id": "Q64", "line": 10308, - "refs": 16, + "refs": 17, "section": "Q45", "shape": "BULLET+BOLD" } @@ -246,9 +246,9 @@ The block below is the parse this document renders, byte for byte. `--check` com "end_line": 8, "line": 1 }, - "queue_bytes": 1433634, - "queue_lines": 10588, - "queue_sha256": "3319eec97cb43cde4f7eeefec6e17e322201458f6417e23c4e266f240485d5a4", + "queue_bytes": 1439284, + "queue_lines": 10606, + "queue_sha256": "8c12492b6459ff61955aabd5f7c6cb2db623514d01da06161dd157f70a6c8591", "sections": [ { "bytes": 3139, @@ -827,11 +827,11 @@ The block below is the parse this document renders, byte for byte. `--check` com "title": "The runtime-resolved root's live write path: name it, reproduce it, then close it" }, { - "bytes": 541373, - "end_line": 10588, + "bytes": 547023, + "end_line": 10606, "id": "Q45", "line": 7594, - "lines": 2995, + "lines": 3013, "role": "opening", "role_detail": "", "status": "OPEN \u2014 awaiting Alexander's zoom/whole-shop answer; build is the pipe's.", diff --git a/tools/path_claim_baseline.tsv b/tools/path_claim_baseline.tsv index 4a91961..adee301 100644 --- a/tools/path_claim_baseline.tsv +++ b/tools/path_claim_baseline.tsv @@ -26,3 +26,4 @@ docs/bar.md EXAMPLE a metasyntactic illustration in the sentence about the gap, recoveries/foo.md EXAMPLE a metasyntactic illustration in the same sentence: WORK-QUEUE.md:10362 quotes "recoveries/foo.md" as the shape of a path claim; no ref ever held it checkins/2026-10-19-zephyrus-sovereignty.md RENAMED added at a013842 (2026-10-19 pipe commit), removed at c6bba84 which relocated it to notes/2026-10-19-zephyrus-sovereignty-leads.md (BAD_NAME+DUPLICATE fix per reply 77); WORK-QUEUE.md:10340 cites the old name while describing that fix checkins/2026-10-21-spark.md TARGET owed, not cited as existing: replies/2026-10-21-to-spark-06.md:26 says Post `checkins/2026-10-21-spark.md` (Did / Blocked / Next / Ask); git log --all --diff-filter=A -> 0 commits, so no ref holds it and only spark posting it clears this row +replies/2026-10-22-to-null-01.md RENAMED renamed to replies/2026-10-22-to-null-03.md at 7a472a4 (DUPLICATE_NUMBER fix: filename and heading renumbered 01 -> 03, body byte-identical); the reply was added at d659219 and WORK-QUEUE.md:10582 cites the old name while describing that fix diff --git a/tools/path_claim_check.py b/tools/path_claim_check.py index 57a1d48..95e0aa8 100644 --- a/tools/path_claim_check.py +++ b/tools/path_claim_check.py @@ -32,9 +32,16 @@ WHY THIS EXISTS WHAT IT MEASURES -- one verdict per DISTINCT path (unit: distinct paths) EXISTS the path is in the tree at --rev. + DOCREL the path is NOT in the tree at the repo root, but it resolves + relative to the document that cites it -- an artifact's own + `tools/fetch.py` named beside its `REPORT.md`. Printed with the + path it resolves to and the cite that resolved it, so a reader can + go and look at the base the tool used. Counted as resolved, not as + a defect: the file it names exists. 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. + prose is naming something inside this repository, and neither base + holds it -- not the tree at --rev, and not the citing document's + own directory. 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 @@ -45,6 +52,21 @@ WHAT IT MEASURES -- one verdict per DISTINCT path (unit: distinct paths) 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. +WHY THERE ARE TWO BASES, AND WHY THE ROOT ONE WINS + The corpus writes most paths relative to the repository root, so the root base + is the contract and it is tried first, always. But an artifact inside + `wizard-work/` writes its own inventory the way a reader sitting in that + directory would: `tools/fetch.py` beside `REPORT.md` is the artifact's fetch + script. Graded at the root alone, exactly those citations read MISSING while + the files sat in that artifact's own `tools/` directory (the live case: + `wizard-work/sphinx/longctx-needle-grounding-2026-10-22/REPORT.md:49-52`) -- + a tool reporting a base mismatch as a defect teaches writers to stop naming + files, which is the worse failure. So the second base exists, it is used ONLY + when the root base missed, and the verdict it produces is a different word + (DOCREL) that carries the resolved path and the resolving cite. A root- + relative citation is never rewritten by it, and a token that resolves under + neither base is still MISSING. + 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 @@ -87,17 +109,19 @@ EXIT CODES 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] + [--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 # 21 cells + python3 path_claim_check.py --selftest # 27 cells python3 path_claim_check.py --repo . --fail-on-missing + python3 path_claim_check.py --repo . --only docrel --context """ import argparse import collections import json import os +import posixpath import re import subprocess import sys @@ -144,7 +168,14 @@ STRIP = "`*'\"()[]<>,;:." # citation; it is a deliverable somebody owes, which is a different thing. BASELINE_CLASSES = ("PLANTED", "NOT_ADDED", "RENAMED", "SHORT_NAME", "FOREIGN_TREE", "EXAMPLE", "TARGET") -VERDICTS = ("EXISTS", "MISSING", "EXTERNAL", "TEMPLATE") +# DOCREL sits between the two graded outcomes on purpose: it is a resolution, +# reported beside EXISTS rather than folded into it, because the base it used is +# part of the claim and a reader has to be able to see that. +VERDICTS = ("EXISTS", "DOCREL", "MISSING", "EXTERNAL", "TEMPLATE") +# Which resolution wins when one path is cited from more than one document: a +# root hit beats a document-relative hit beats no hit. EXTERNAL/TEMPLATE never +# meet the others (they are a property of the token, not of the tree). +RESOLUTION_RANK = {"MISSING": 0, "DOCREL": 1, "EXISTS": 2} class CannotGrade(Exception): @@ -184,6 +215,7 @@ def clean(token): def classify(path, tracked, tops): + """The verdict at the repo-root base alone. DOCREL needs a second base.""" if TEMPLATE.search(path): return "TEMPLATE" if HOST.match(path) or path.split("/")[0] not in tops: @@ -191,6 +223,22 @@ def classify(path, tracked, tops): return "EXISTS" if path in tracked else "MISSING" +def doc_relative(path, where, tracked): + """The tracked path this document's own directory resolves `path` to, or None. + + `where` is the citing document's repo-relative name; a document at the root + has no second base distinct from the first, so it returns None and root + documents keep the single-base contract. A candidate that climbs out of the + tree is not a base this tool will stand on. + """ + if not where or "/" not in where: + return None + cand = posixpath.normpath(posixpath.join(posixpath.dirname(where), path)) + if cand == ".." or cand.startswith("../"): + return None + return cand if cand in tracked else None + + def fence_mask(text): """1-based line numbers that are a fence marker or sit inside a fence.""" out, in_fence = set(), False @@ -207,8 +255,10 @@ def fence_mask(text): 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. + `where` is the document's name, used for the first-cite handle and as the + second base: a token the root base misses is tried beside the document + (`doc_relative`) before it is called MISSING. Line numbers are the + document's own, so a row can be read back at --rev. """ rows, stats = {}, collections.Counter() mask = fence_mask(text) @@ -221,17 +271,39 @@ def scan_text(text, where, tracked, tops): if not p: continue stats["occurrences"] += 1 + verdict = classify(p, tracked, tops) + resolved = None + if verdict == "MISSING": + resolved = doc_relative(p, where, tracked) + if resolved: + verdict = "DOCREL" 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)} + "verdict": verdict, + "resolved": resolved, + "resolved_from": ("%s:%d" % (where, i) + if resolved else None), + "unresolved": 0} row["mentions"] += 1 + if verdict == "MISSING": + # One resolving cite does not erase the cites that did not + # resolve at their own base; the count travels with the row. + row["unresolved"] += 1 + if (RESOLUTION_RANK.get(verdict, -1) > + RESOLUTION_RANK.get(row["verdict"], -1)): + row["verdict"] = verdict + if resolved: + row["resolved"] = resolved + row["resolved_from"] = "%s:%d" % (where, i) return rows, stats def scan(repo, rev, corpus, store): + """Every document's rows, merged by path -- the same scan_text() the fixtures + are graded by, so no second matcher exists to drift from this one.""" all_rows, stats = {}, collections.Counter() for f in corpus: text = store.read(f).decode("utf-8", "replace") @@ -239,7 +311,14 @@ def scan(repo, rev, corpus, store): stats.update(st) for p, row in rows.items(): if p in all_rows: - all_rows[p]["mentions"] += row["mentions"] + cur = all_rows[p] + cur["mentions"] += row["mentions"] + cur["unresolved"] += row["unresolved"] + if (RESOLUTION_RANK.get(row["verdict"], -1) > + RESOLUTION_RANK.get(cur["verdict"], -1)): + cur["verdict"] = row["verdict"] + cur["resolved"] = row.get("resolved") + cur["resolved_from"] = row.get("resolved_from") else: all_rows[p] = row return all_rows, stats @@ -298,11 +377,16 @@ def apply_baseline(rows, ledger): 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") + # A row the second base now resolves is cleared too, but it says so: the + # path is true at the citing document's directory, not at the repo root, and + # a reader who deletes the row should know which base made it go. + cleared_docrel = sorted(p for p in ledger + if p in rows and rows[p]["verdict"] == "DOCREL") 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, + "cleared": cleared, "cleared_docrel": cleared_docrel, "dead": dead, "tolerated": collections.Counter(r["baseline"]["class"] for r in missing.values() if r["baseline"]["known"])} @@ -329,11 +413,16 @@ def report(rows, stats, rev, sha, corpus, ledger, path, only, context): 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["cleared_docrel"]: + row = rows[p] + print(" CLEARED_DOCREL %s -- ledger row, now resolves at %s from %s; " + "the row can go" % (p, row.get("resolved"), row.get("resolved_from"))) 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", + "docrel": lambda r: r["verdict"] == "DOCREL", "external": lambda r: r["verdict"] == "EXTERNAL", "template": lambda r: r["verdict"] == "TEMPLATE", "all": lambda r: True}[only] @@ -346,6 +435,10 @@ def report(rows, stats, rev, sha, corpus, ledger, path, only, context): b = r["baseline"] tag = (" [%s: %s]" % (b["class"], b["reason"])) if b["known"] \ else " [NEW -- not in the baseline]" + elif r["verdict"] == "DOCREL": + tag = (" [DOCREL: resolves at %s, from %s; %d cite(s) did not resolve " + "at their own base]" + % (r.get("resolved"), r.get("resolved_from"), r.get("unresolved", 0))) print(" %3dx %-58s first %s%s" % (r["mentions"], r["path"], r["first"], tag)) if context: print(" ...%s..." % r["context"]) @@ -382,8 +475,8 @@ def main(): 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("--only", choices=["missing", "exists", "docrel", "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, @@ -610,6 +703,69 @@ def selftest(): "bare rc=%d %r | named rc=%d %r" % (bare_rc, summary(bare_out), named_rc, summary(named_out))) + # --- the second base: DOCREL, and the root base still winning ------------- + # These are statements about the shipped scan_text()/scan(), not about a + # copy: the merge is exercised through the same function the corpus uses. + ART = "wizard-work/art/REPORT.md" + art_tracked = {"docs/real.md", "tools/t.py", "wizard-work/art/tools/fetch.py"} + + def _doc(text, where, tracked): + rows, _ = scan_text(text, where, tracked, + {"docs", "tools", "notes", "wizard-work"}) + return rows + + r = _doc("inventory: `tools/fetch.py` beside this report", ART, art_tracked) + cell("c22 a path the root base misses but the document's own directory holds is DOCREL", + r.get("tools/fetch.py", {}).get("verdict") == "DOCREL" + and r["tools/fetch.py"].get("resolved") == "wizard-work/art/tools/fetch.py" + and r["tools/fetch.py"].get("resolved_from") == ART + ":1", str(r)) + r = _doc("inventory: `tools/fetch.py` at the record", "WORK-QUEUE.md", art_tracked) + cell("c23 the second base is not available to a document at the repo root", + r.get("tools/fetch.py", {}).get("verdict") == "MISSING", str(r)) + both = art_tracked | {"tools/fetch.py"} + r = _doc("inventory: `tools/fetch.py`", ART, both) + cell("c24 a root-relative hit is never rewritten by the second base", + r.get("tools/fetch.py", {}).get("verdict") == "EXISTS" + and r["tools/fetch.py"].get("resolved") is None, str(r)) + r = _doc("inventory: `tools/nope.py`", ART, art_tracked) + cell("c25 a candidate that resolves under neither base is still MISSING", + r.get("tools/nope.py", {}).get("verdict") == "MISSING", str(r)) + + class _FakeStore: + def __init__(self, tree, docs): + self.tree, self.docs = set(tree), docs + self.tops = {p.split("/")[0] for p in self.tree} + + def read(self, path): + return self.docs[path].encode() + + merged, _ = scan(".", "HEAD", ["WORK-QUEUE.md", ART], + _FakeStore(art_tracked, + {"WORK-QUEUE.md": "quotes `tools/fetch.py`\n", + ART: "inventory: `tools/fetch.py`\n"})) + row = merged.get("tools/fetch.py", {}) + cell("c26 one path cited from two documents keeps the resolving base and counts both", + row.get("verdict") == "DOCREL" and row.get("mentions") == 2 + and row.get("unresolved") == 1 and row.get("resolved_from") == ART + ":1", + str(row)) + + # --- DOCREL end to end, through the CLI ---------------------------------- + os.makedirs(os.path.join(repo, "wizard-work", "art", "tools")) + with open(os.path.join(repo, "wizard-work", "art", "tools", "fetch.py"), "w") as fh: + fh.write("# fetch\n") + with open(os.path.join(repo, "wizard-work", "art", "REPORT.md"), "w") as fh: + fh.write("## Evidence chain\n\n- `tools/fetch.py` -- fetch + normalise\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", "artifact"], capture_output=True) + rc, out = run("--repo", repo, "--fail-on-missing") + rc2, out2 = run("--repo", repo, "--only", "docrel") + cell("c27 the CLI reports a document-relative citation as DOCREL and does not fail on it", + rc == 0 and "DOCREL 1 distinct" in out + and "0 new against the baseline" in out + and rc2 == 0 and "1 distinct path(s)" in out2 and "tools/fetch.py" in out2, + "rc=%d %r | only rc=%d %r" % (rc, out[-300:], rc2, out2[-300:])) + print("selftest: %d cells, %d passed, %d failed" % (n, n - bad, bad)) return 0 if bad == 0 else 1