volt: path_claim_check — grade every in-namespace path the archive names against the tree at a revision
This commit is contained in:
1 parent
4d18f779b0
commit
5f892d7311
3 files changed
+519
No files matched your search
@@ -0,0 +1,19 @@
|
||||
# path_claim baseline -- the MISSING paths this archive already carried when
|
||||
# path_claim_check.py first censused it. One row: <path>\t<CLASS>\t<reason>.
|
||||
# Regenerate the census (not the reasons): path_claim_check.py --repo . --context
|
||||
# A row is a stated reading, not an exemption: --fail-on-missing fails on any
|
||||
# MISSING path this ledger does not name, and every row here is printed with its
|
||||
# class on every run, so a reader sees the exceptions instead of trusting that
|
||||
# there are none. A row whose path later appears in the tree is reported CLEARED
|
||||
# and can be deleted; a row the census no longer produces is reported DEAD.
|
||||
# Classes: PLANTED (a file planted in a clone to measure a gate), NOT_ADDED (the
|
||||
# prose states the file was never added on any ref), RENAMED (the artifact exists
|
||||
# under a later name), SHORT_NAME (the prose cites an abbreviated form of a real
|
||||
# artifact), FOREIGN_TREE (a path relative to another project's tree).
|
||||
career-lane/ats-gate-census-draft.md RENAMED history under that name at f29425a..e1426e9; the artifact at HEAD is career-lane/ats-gate-census.md
|
||||
inbox/pending.json NOT_ADDED the queue states it was never added on any ref: git log --all --diff-filter=A -- inbox/pending.json -> no output
|
||||
recoveries/summary-fidelity-full.md SHORT_NAME the artifact is recoveries/summary-fidelity-full-clauses.md; the queue cites the short form
|
||||
checkins/2026-10-19-volt-02.md PLANTED planted in a clone of 3890c12 to measure the gate hole; the citing sentence says so
|
||||
checkins/2026-10-02-volt-b.md PLANTED planted in a clone of e98a34b as the backdated-name example; the citing sentence says so
|
||||
docs/release-engine-spec.md FOREIGN_TREE relative to another project's tree (src/, tests/): neither is tracked here
|
||||
replies/2026-10-19-to-volt-99.md PLANTED planted in a clone of 5defac9 to red the reply leg; the citing sentence says so
|
||||
|
@@ -0,0 +1,495 @@
|
||||
#!/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 `<path>:<line>` 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 at HEAD, 1,024 distinct path tokens appear, and only 317 of them are
|
||||
even candidates for "this archive holds that file". The other 707 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 707 false defects, so they are COUNTED
|
||||
and never graded. What is left is small enough to read: 8 missing paths, each
|
||||
of which is in the reviewed ledger with the class that explains it.
|
||||
|
||||
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`, `<name>`, `{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
|
||||
`<path>\\t<CLASS>\\t<reason>`, one row per reviewed missing path, class from a
|
||||
closed set (PLANTED, NOT_ADDED, HOST_ROUTE, RENAMED, SHORT_NAME,
|
||||
FOREIGN_TREE). 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 eight
|
||||
exceptions 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 baseline that cannot be read or names an
|
||||
unknown class. Never 0, and never a silent empty census.
|
||||
|
||||
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]
|
||||
python3 path_claim_check.py --selftest # 17 cells
|
||||
python3 path_claim_check.py --repo . --fail-on-missing \\
|
||||
--baseline tools/path_claim_baseline.tsv
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
DEFAULT_REPO = "/root/troupe-standup"
|
||||
|
||||
# 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<path>/?(?:[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.
|
||||
BASELINE_CLASSES = ("PLANTED", "NOT_ADDED", "RENAMED", "SHORT_NAME",
|
||||
"FOREIGN_TREE")
|
||||
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):
|
||||
"""`<path>\\t<CLASS>\\t<reason>` -> {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 "
|
||||
"<path>\\t<CLASS>\\t<reason>: %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 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")
|
||||
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 = load_baseline(a.baseline) if a.baseline 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:
|
||||
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)
|
||||
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:]))
|
||||
|
||||
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())
|
||||
@@ -165,6 +165,7 @@ READ_PROBES = (
|
||||
("doc_census_check.py", ["--repo", "{nope}", "--no-run"]),
|
||||
("gate_blame.py", ["--repo", "{nope}"]),
|
||||
("landing_check.py", ["--repo", "{nope}"]),
|
||||
("path_claim_check.py", ["--repo", "{nope}"]),
|
||||
("queue_claim_check.py", ["--queue", "{nope}/q.md"]),
|
||||
("queue_index.py", ["--queue", "{nope}/q.md", "--check", "{nope}/i.md"]),
|
||||
("queue_append_audit.py", ["--repo", "{nope}"]),
|
||||
@@ -229,6 +230,10 @@ MALFORMED_PROBES = (
|
||||
2, "not a git work tree"),
|
||||
("landing_check.py", "not-a-repo", ["--repo", "{notrepo}"],
|
||||
2, "not a git work tree"),
|
||||
# path_claim_check.py's input is a git work tree whose markdown names paths:
|
||||
# a directory that is not one must be named, not graded as an empty census.
|
||||
("path_claim_check.py", "not-a-repo", ["--repo", "{notrepo}"],
|
||||
2, "not a git work tree"),
|
||||
("queue_claim_check.py", "junk-queue", ["--queue", "{junkqueue}"],
|
||||
0, "0 opening records"),
|
||||
("queue_index.py", "junk-index", ["--queue", "{junkqueue}", "--check", "{junkindex}"],
|
||||
|
||||
Reference in new issue
Block a user