Files
volt c3d70567a6 volt: verify gains a seventh leg -- doc-commands, graded at WORKTREE
The gate had six legs and none of them read a docstring's own command line.
doc_command_check.py existed since 838750c but only as a tool: nothing on the
gate ran it, so a documented flag its parser never learned could land and
nobody read it. This is that leg.

  doc-commands   tools/doc_command_check.py --rev WORKTREE --fail
                 WORKTREE, not HEAD: the commit window is where a docstring is
                 edited. Measured at the worktree this lands in: 67 commands
                 over 19 oracle tools, 67 OK, rc 0.

doc_command_check.py learns --rev WORKTREE: tracked paths from `git ls-files`
(an untracked tools/x.py is not part of the commit this mode grades), bytes
from disk. Outside a work tree it is rc 2, not an empty clean run. Selftest
15 -> 18 cells: one document edited on disk but not committed is rc 0 at HEAD
and rc 1 at WORKTREE, and the flag it names is the edited one.

Three tools stopped defaulting to a fixed path on this host. verify.py,
selftest_sweep.py, write_probe.py, citation_resolve_check.py,
path_claim_check.py and doc_census_check.py now resolve DEFAULT_REPO from the
file's own location, so a bare run in a clone grades the clone.

Three selftests refuse unreadable input at rc 2 instead of reporting it as
failed content: tool_map (not a work tree), write_probe (no table), and
citation_resolve_check (no corpus). Each cell below them graded `repo`, so an
absent tree read as "your table is wrong".

Evidence, all measured on this tree before the commit:
  verify.py --selftest              55 cells, 55 passed   (docstring says 55)
  doc_command_check.py --selftest   18 cells, 18 passed   (baseline pinned 18)
  tool_map.py --repo . --rev WORKTREE --check docs/TOOLS.md   rc 0
  verify.py --repo .                7 legs, 6 pass, 1 fault -- selftest-sweep,
                                    tool_map's drift cell, which renders HEAD
                                    and so cannot pass inside a commit window
tools/selftest_baseline.tsv re-pins doc_command_check.py 15 -> 18 cells.
2026-10-22 22:05:55 +00:00

643 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""tool_map.py -- the instruments get a map, and the map is generated from the tree.
WHY THIS EXISTS
`tools/` holds one script per instrument and there is no document that says
what they are. A reader meets them one at a time -- in a `verify.py` leg, in a
queue block, in a reply -- and nothing states the whole set: which tool grades
what, which flags each one actually accepts, which of them the two probe axes
cover, and which leg of the operator's gate a failure shows up in. The closest
thing is `verify.py`'s `LEGS` table, which names six of them.
A hand-written map would be a fourth notation of the defect this suite keeps
grading: prose about the tree, one step from the thing it describes, with no
reader. So this document is not written, it is DERIVED -- every cell comes
from the artefact it describes, at a stated revision:
what it grades the module docstring's first line, after `<name> -- `
flags the tool's own `--help`, run from a temporary copy of the
blob at `--rev` (the only statement of an interface that
cannot drift from the interface)
probes `selftest_sweep.py`'s READ_PROBES / NOT_UTF8_PROBES /
MALFORMED_PROBES tables, read with `ast.literal_eval`
gate leg `verify.py`'s `LEGS` table, likewise
Nothing is inferred and nothing is typed by hand, so the document cannot say
something the tree does not: `--check` re-derives it and compares bytes.
WHAT IT MEASURES -- one verdict per invocation
CURRENT the document on disk is byte-identical to the render
DRIFT the document differs; the first differing line is printed. A
FAULT: the map is a claim about the tree and it is no longer true.
UNREADABLE the document does not exist, or the revision will not
materialise, or a tool's `--help` outlived its timeout. rc 2,
never rc 1 -- an ungradable map is not a stale one.
`--selftest` ends with the currency cell: it re-renders `docs/TOOLS.md` at
HEAD and compares. That cell is the reason the map stays true -- the tool
sweep runs every tool's `--selftest`, so a commit that changes a tool's
docstring, flags or probe rows without re-rendering the map turns the sweep
RED instead of leaving a stale document that reads as current.
THE PRE-COMMIT BLIND SPOT, AND THE MODE THAT CLOSES IT
Every cell above is derived from a REVISION, not from the working tree:
`collect()` reads blobs with `git show <rev>:<path>` and runs `--help` from a
copy of that blob. That is right for a document that makes a claim about a
commit, and wrong for the ten minutes before a commit exists.
Measured on this repository, 2026-10-21. `4602bd5` is drifted: its commit
message states "TOOLS.md re-rendered, byte-identical" and the map it ships is
not the render of its own tree. Two runs, both from inside a worktree checked
out at that commit: `--repo . --check docs/TOOLS.md` reads **rc 1, DRIFT at
line 49** (the `write-axis` cell, whose description moved in `verify.py`),
and `--repo . --selftest` reads **15/16, rc 1**, the one failure being the
currency cell below. Before the commit existed both commands read rc 0 and
16/16, because the document on disk and the tools at `HEAD` (`ea8fe24`) were
still the pre-edit pair -- the edit lived only in the worktree.
The gate did not catch it either. `selftest_sweep.py --repo .` at `4602bd5`
reads **21 tools, 21 OK, 0 fatal**, because the sweep runs each tool as
`[python, path, "--selftest"]` with no `--repo` (`selftest_sweep.py:449`) and
this tool's `DEFAULT_REPO` was an absolute path, so its selftest graded
/root/troupe-standup -- a different, clean tree. That was a second defect:
the sweep's selftest axis could grade the wrong tree, and it did.
FIXED 2026-10-22 (volt). `DEFAULT_REPO` is now derived from this file's own
location -- `dirname(dirname(abspath(__file__)))` -- so the default target is
the checkout the tool lives in: the same path in place, a different path in a
copy. Measured on a clone at `c3058d5`, whose `docs/TOOLS.md` is drifted
(`--check` reads **rc 1, DRIFT at line 30**):
- before the fix, `--selftest` with no `--repo` -- the sweep's own
invocation, `cwd` = the clone -- read **20/20, rc 0**: it graded
/root/troupe-standup, the clean tree, and called the clone's drift clean;
- the same clone under `--selftest --repo .` read **19/20, rc 1**, the one
failure being the currency cell below;
- after the fix, the repo-less run reads **19/20, rc 1** -- the same
verdict as `--repo .`, because the default now names the copy.
`selftest_sweep.py` needed no edit: `cwd` was already the tree under sweep,
so the fix alone is enough for the sweep's axis to grade what it walks.
`--rev WORKTREE` derives the map from the files on disk, discovered through
`git ls-files -- tools/` -- the index, which is what a commit of tracked
files will carry. Measured on a clean worktree at `190d1aa`: rc 0, and
byte-identical to what HEAD renders; with one uncommitted line appended to
`verify.py`'s `write-axis` description, `--rev HEAD` still reads rc 0 and
`--rev WORKTREE` reads **rc 1, DRIFT at line 49**. The currency cell stays
HEAD-scoped by design: a worktree mid-edit is not a fault, and a sweep that
fails while a docstring is being typed teaches nothing. The pre-commit guard
is that `--check` is run in WORKTREE mode before the commit; four cells below
measure that it separates the two trees and that a clean worktree renders
byte-for-byte what HEAD renders, so the mode cannot invent a drift of its own.
READ-ONLY except for the one path it is told to write. The only process it
starts is `<tool> --help`, which does no work by construction; it never runs a
documented command, so it cannot write, post, or deploy anything.
EXIT CODES (the tools/ contract)
0 the map is current (or was written)
1 the map has drifted from the tree
2 could not grade: not a git work tree, a revision that will not
materialise, or a document that cannot be read
USAGE
python3 tool_map.py --repo . --write docs/TOOLS.md # render
python3 tool_map.py --repo . --check docs/TOOLS.md # grade drift, rc 1 on drift
python3 tool_map.py --repo . --rev WORKTREE --check docs/TOOLS.md # ...as it will be committed
python3 tool_map.py --repo . --json out.json
python3 tool_map.py --selftest # 20 cells
"""
from __future__ import annotations
import argparse
import ast
import json
import os
import re
import subprocess
import sys
import tempfile
DEFAULT_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT_DOC = "docs/TOOLS.md"
HELP_TIMEOUT = 30
# The one value of `--rev` that does not name a revision: the files git would
# commit from this worktree (the index lists them, the disk supplies them).
WORKTREE = "WORKTREE"
# A long option, or a short one, as argparse prints them in the usage block.
# `-h`/`--help` are dropped: every tool has them, and a column of sixteen
# identical pairs is noise, not information.
OPT_RE = re.compile(r"(?<![\w-])(--[a-z0-9][a-z0-9-]*|-[A-Za-z])(?![\w-])")
SKIP_OPTS = {"-h", "--help"}
def git(repo, *args, inp=None):
r = subprocess.run(["git", "-C", repo, *args], input=inp,
capture_output=True, text=True)
return r.returncode, r.stdout, r.stderr
def read_blob(repo, rev, path):
"""The file's text at `rev`; from disk when `rev` is WORKTREE."""
if rev == WORKTREE:
try:
with open(os.path.join(repo, path), encoding="utf-8") as fh:
return fh.read()
except OSError:
return None
rc, out, err = git(repo, "show", f"{rev}:{path}")
return out if rc == 0 else None
def tracked_tools(repo, rev):
"""Sorted `tools/*.py` basenames tracked at `rev`.
WORKTREE is not a revision and has no tree object: the set is the INDEX
(`git ls-files`), which is what a commit of tracked files will carry, and
the contents come from disk. Reading the index rather than a reflog of the
worktree keeps one rule -- "the file git would commit" -- for both halves.
"""
if rev == WORKTREE:
rc, out, _ = git(repo, "ls-files", "--", "tools/")
else:
rc, out, _ = git(repo, "ls-tree", "-r", "--name-only", rev, "--", "tools/")
if rc != 0:
return None
names = [p.split("/")[-1] for p in out.split("\n")
if p.startswith("tools/") and p.endswith(".py")]
return sorted(names)
def literal_assignments(repo, rev, path, names):
"""{name: value} for the module-level assignments whose value is a literal."""
src = read_blob(repo, rev, path)
if src is None:
return {}
try:
tree = ast.parse(src)
except SyntaxError:
return {}
found = {}
for node in tree.body:
if not isinstance(node, ast.Assign) or len(node.targets) != 1:
continue
tgt = node.targets[0]
if not isinstance(tgt, ast.Name) or tgt.id not in names:
continue
try:
found[tgt.id] = ast.literal_eval(node.value)
except (ValueError, SyntaxError):
continue
return found
def usage_block(text):
"""Only the `usage:` paragraph of a `--help` page.
argparse prints the module docstring as the description ABOVE the usage
line, and those docstrings discuss flags in prose -- `citation_hash_class.py`
mentions `--batch-all-objects` while explaining why it does not use it. A
flag that prose mentions is not a flag the tool accepts, so the oracle is
cut to the usage paragraph and stop: it ends at the first blank line after
the line that opens with `usage:`. No usage line means no oracle.
"""
lines = text.splitlines()
start = None
for i, line in enumerate(lines):
if line.lower().startswith("usage:"):
start = i
break
if start is None:
return ""
block = []
for line in lines[start:]:
if not line.strip():
break
block.append(line)
return "\n".join(block)
def purpose_of(src):
"""The module docstring's first sentence as a one-clause summary.
The house shape is `<name> -- <what it grades>.`; documents written without
the separator fall back to the docstring's opening sentence. The whole first
PARAGRAPH is flattened before the sentence cut, because the sentence that
states a tool's purpose routinely wraps across two or three source lines --
a line-local cut is what this suite grades as a defect everywhere else.
Returns "" when there is no module docstring, which the row prints as `-`.
"""
if not src:
return ""
try:
doc = ast.get_docstring(ast.parse(src)) or ""
except SyntaxError:
return ""
first_para = re.split(r"\n\s*\n", doc.strip())[0] if doc.strip() else ""
flat = " ".join(line.strip() for line in first_para.splitlines()).strip()
if not flat:
return ""
flat = re.sub(r"^[a-z0-9_]+\.[a-z]+ *(?:--|—|–|-) *", "", flat).strip()
m = re.search(r"^(.+?[.?!])(?:\s|$)", flat)
out = (m.group(1) if m else flat).rstrip()
if len(out) > 180:
out = out[:177].rstrip() + "..."
return out
def flags_of(repo, rev, name):
"""The tool's accepted options, from its own `--help`.
The blob is written to a temporary directory and run there so a tool is
adjudicated at `--rev` against the parser it will actually use, never
against a worktree copy. Returns (flags, note) where note is "" when the
oracle answered and a short reason when it did not.
"""
src = read_blob(repo, rev, f"tools/{name}")
if src is None:
return [], "no such file at rev"
with tempfile.TemporaryDirectory() as td:
path = os.path.join(td, name)
with open(path, "w", encoding="utf-8") as fh:
fh.write(src)
try:
proc = subprocess.run([sys.executable, path, "--help"],
capture_output=True, text=True,
timeout=HELP_TIMEOUT, cwd=td)
except subprocess.TimeoutExpired:
return [], "help timed out"
text = (proc.stdout or "") + (proc.stderr or "")
usage = usage_block(text)
seen, out = set(), []
for m in OPT_RE.finditer(usage):
opt = m.group(1)
if opt in SKIP_OPTS or opt in seen:
continue
seen.add(opt)
out.append(opt)
if not out:
return [], "no --help oracle"
return out, ""
def _count_by_tool(entries):
"""Count probes per tool name (first element of each entry tuple).
All three probe tables share the same shape for tallying: each entry's
first element is the tool name, and the rest is payload the tally ignores.
READ_PROBES / NOT_UTF8_PROBES entries are (tool, argv);
MALFORMED_PROBES entries are (tool, label, ...). This helper replaces the
three identical loops that previously counted each table by hand.
"""
counts = {}
for entry in entries:
name = entry[0]
counts[name] = counts.get(name, 0) + 1
return counts
def collect(repo, rev):
"""The whole map as data: one dict per tool plus the gate's legs."""
names = tracked_tools(repo, rev)
if names is None:
raise SystemExit(f"tool_map: cannot read repo: {repo} at {rev}")
sweep = literal_assignments(repo, rev, "tools/selftest_sweep.py",
{"READ_PROBES", "NOT_UTF8_PROBES",
"MALFORMED_PROBES"})
read_n = _count_by_tool(sweep.get("READ_PROBES", ()))
utf8_n = _count_by_tool(sweep.get("NOT_UTF8_PROBES", ()))
mal_n = _count_by_tool(sweep.get("MALFORMED_PROBES", ()))
legs = literal_assignments(repo, rev, "tools/verify.py", {"LEGS"}).get("LEGS", ())
leg_of = {}
for leg in legs:
if isinstance(leg, dict) and "tool" in leg and "key" in leg:
leg_of.setdefault(leg["tool"], []).append(leg["key"])
rows = []
for name in names:
src = read_blob(repo, rev, f"tools/{name}")
flags, note = flags_of(repo, rev, name)
rows.append({
"tool": name,
"purpose": purpose_of(src),
"flags": flags,
"no_oracle": note,
"read": read_n.get(name, 0),
"not_utf8": utf8_n.get(name, 0),
"malformed": mal_n.get(name, 0),
"legs": leg_of.get(name, []),
})
return {"rev": rev, "rows": rows,
"legs": [{"key": l.get("key"), "tool": l.get("tool"),
"slow": bool(l.get("slow")), "what": l.get("what", "")}
for l in legs if isinstance(l, dict)]}
def render(data):
"""The document, byte-deterministic for a given tree."""
rows, legs = data["rows"], data["legs"]
with_oracle = sum(1 for r in rows if not r["no_oracle"])
lines = [
"# The instruments",
"",
"Generated by `tools/tool_map.py` from the tree it is asked to read. Do not",
"edit by hand: `python3 tools/tool_map.py --repo . --write docs/TOOLS.md`",
"re-renders it, and `--check` grades it (rc 1 when the document and the tree",
"disagree). Every cell below is derived, never typed -- purpose from the",
"module docstring, flags from the tool's own `--help`, probe rows from",
"`selftest_sweep.py`'s tables, the gate leg from `verify.py`'s `LEGS`.",
"",
f"**{len(rows)} tools, {with_oracle} with a `--help` oracle.** "
f"Probes are read as `read-refusal / not-utf8 / malformed-input` rows; a "
f"`-` means that axis does not name the tool.",
"",
"| tool | what it grades | flags | probes | gate leg |",
"|---|---|---|---|---|",
]
for r in rows:
purpose = r["purpose"] or "-"
if r["no_oracle"]:
flags = f"- *({r['no_oracle']})*"
else:
flags = " ".join(f"`{f}`" for f in r["flags"]) or "-"
probes = (f"{r['read']} / {r['not_utf8']} / {r['malformed']}"
if (r["read"] or r["not_utf8"] or r["malformed"]) else "-")
gate = ", ".join(f"`{k}`" for k in r["legs"]) or "-"
lines.append(f"| `{r['tool']}` | {purpose} | {flags} | {probes} | {gate} |")
lines += [
"",
"## The gate",
"",
"`python3 tools/verify.py --repo .` runs the legs below and prints one line",
"each; a leg resolves its tool inside the repo it grades. The `slow` legs are",
"dropped by `--fast`.",
"",
"| leg | tool | slow | what it grades |",
"|---|---|---|---|",
]
for leg in legs:
lines.append(f"| `{leg['key']}` | `{leg['tool']}` | "
f"{'yes' if leg['slow'] else 'no'} | {leg['what']} |")
lines += [
"",
"A tool that appears in no leg is still graded by `selftest-sweep`, which runs",
"every tracked tool's `--selftest` and both read-refusal axes -- so the",
"`probes` column above is also the sweep's cover of that tool.",
"",
]
return "\n".join(lines)
def selftest(repo):
cells = []
# The currency cell below grades `repo`; if `repo` is not a work tree that
# cell can only fail, which reads as "the document drifted" when the truth
# is "no tree was there". Refuse at rc 2, as main() already does for the
# same input.
rc, _, _ = git(repo, "rev-parse", "--show-toplevel")
if rc != 0:
print(f"tool_map: cannot read repo: {repo} is not a git work tree",
file=sys.stderr)
return 2
def cell(label, ok, detail=""):
cells.append({"label": label, "ok": bool(ok), "detail": detail})
# -- the derivation, on fixtures -------------------------------------
cell("purpose: house `<name> -- ...` shape is stripped",
purpose_of('"""fake_tool.py -- grades the fake contract, at one line.\n\n'
'More prose here."""\n') ==
"grades the fake contract, at one line.")
cell("purpose: no separator falls back to the first sentence",
purpose_of('"""A map of the tree. Second sentence follows."""\n') ==
"A map of the tree.")
cell("purpose: absent docstring is empty, not invented",
purpose_of("import os\n") == "")
cell("flags: `-h`/`--help` are dropped, real options kept",
[o for o in OPT_RE.findall("usage: t.py [-h] [--repo R] [--rev V]")
if o not in SKIP_OPTS] == ["--repo", "--rev"])
cell("flags: a positional-looking token is not an option",
OPT_RE.findall("usage: t.py [-h] [--repo REPO] filename") ==
["-h", "--repo"])
cell("flags: a flag the description only MENTIONS is not an oracle",
usage_block("desc mentions --nope\n\noptions:\n") == "")
with tempfile.TemporaryDirectory() as td:
fixture = os.path.join(td, "fx")
os.makedirs(os.path.join(fixture, "tools"))
os.makedirs(os.path.join(fixture, "docs"))
env = dict(os.environ, GIT_AUTHOR_NAME="t", GIT_AUTHOR_EMAIL="t@e",
GIT_COMMITTER_NAME="t", GIT_COMMITTER_EMAIL="t@e")
subprocess.run(["git", "init", "-q", fixture], check=True, env=env)
open(os.path.join(fixture, "tools", "fake_tool.py"), "w").write(
'#!/usr/bin/env python3\n'
'"""fake_tool.py -- grades the fake contract, which nothing measured.\n\n'
'Prose."""\n'
'import argparse\n'
'ap = argparse.ArgumentParser()\n'
'ap.add_argument("--repo")\n'
'ap.add_argument("--fail", action="store_true")\n'
'if "--help" in __import__("sys").argv:\n'
' ap.print_help()\n')
open(os.path.join(fixture, "tools", "quiet_tool.py"), "w").write(
'#!/usr/bin/env python3\n'
'"""quiet_tool.py -- parses argv by hand, so it prints no usage."""\n'
'import sys\n'
'print("ran")\n')
open(os.path.join(fixture, "tools", "selftest_sweep.py"), "w").write(
'READ_PROBES = (("fake_tool.py", ["--repo", "{nope}"]),)\n'
'NOT_UTF8_PROBES = ()\n'
'MALFORMED_PROBES = (("fake_tool.py", "not-a-repo",\n'
' ["--repo", "{notrepo}"], 2, "cannot read"),)\n')
open(os.path.join(fixture, "tools", "verify.py"), "w").write(
'LEGS = ({"key": "fake", "tool": "fake_tool.py", "slow": False,\n'
' "what": "grade the fake contract"},)\n')
def commit(msg):
subprocess.run(["git", "-C", fixture, "add", "-A"], check=True, env=env)
subprocess.run(["git", "-C", fixture, "commit", "-qm", msg],
check=True, env=env)
commit("fixture")
data = collect(fixture, "HEAD")
by = {r["tool"]: r for r in data["rows"]}
cell("collect: every tracked .py is a row, oracle or not",
len(data["rows"]) == 4 and by["fake_tool.py"]["no_oracle"] == ""
and by["quiet_tool.py"]["no_oracle"] == "no --help oracle",
str([(r["tool"], r["no_oracle"]) for r in data["rows"]]))
cell("collect: probe rows and the gate leg are joined by name",
by["fake_tool.py"]["read"] == 1
and by["fake_tool.py"]["malformed"] == 1
and by["fake_tool.py"]["legs"] == ["fake"],
str({k: (v["read"], v["malformed"], v["legs"]) for k, v in by.items()}))
cell("collect: only printed flags are listed, and --help is not",
by["fake_tool.py"]["flags"] == ["--repo", "--fail"],
str(by["fake_tool.py"]["flags"]))
doc = os.path.join(fixture, "docs", "TOOLS.md")
text = render(data)
open(doc, "w").write(text)
cell("render: deterministic (two renders of one tree are identical)",
text == render(collect(fixture, "HEAD")))
cell("render: the tool without an oracle is marked, not accused",
"quiet_tool.py" in text and "no --help oracle" in text)
rc_ok = compare(doc, text)
cell("--check: rc 0 on the document the same tree renders", rc_ok == 0,
f"rc {rc_ok}")
open(doc, "a").write("stale line\n")
rc_drift = compare(doc, render(collect(fixture, "HEAD")), quiet=True)
cell("--check: rc 1 once a byte drifts",
rc_drift == 1, f"rc {rc_drift}")
open(doc, "w").write(text)
rc_missing = compare(os.path.join(fixture, "docs", "nope.md"), text,
quiet=True)
cell("--check: rc 2, not rc 1, when the document does not exist",
rc_missing == 2, f"rc {rc_missing}")
rc_norepo = graderc(td)
cell("--repo a directory that is not a git work tree: rc 2",
rc_norepo == 2, f"rc {rc_norepo}")
# -- WORKTREE: the tree you are about to commit, not the last commit --
# The map is derived from a revision by construction, so a docstring
# edited after the last commit is invisible to both `--check` and the
# currency cell below. That is measured here, not described: the same
# document is current against HEAD and drifted against the worktree.
cell("WORKTREE: a clean worktree renders exactly what HEAD renders",
render(collect(fixture, WORKTREE)) == text)
fake = os.path.join(fixture, "tools", "fake_tool.py")
with open(fake, encoding="utf-8") as fh:
src = fh.read()
with open(fake, "w", encoding="utf-8") as fh:
fh.write(src.replace("which nothing measured",
"which nothing has measured yet"))
wt = collect(fixture, WORKTREE)
hd = collect(fixture, "HEAD")
purpose_wt = {r["tool"]: r["purpose"] for r in wt["rows"]}["fake_tool.py"]
purpose_hd = {r["tool"]: r["purpose"] for r in hd["rows"]}["fake_tool.py"]
cell("WORKTREE: reads the file on disk, not the blob at HEAD",
purpose_wt != purpose_hd and "has measured yet" in purpose_wt,
f"worktree={purpose_wt!r} head={purpose_hd!r}")
rc_head = compare(doc, render(hd), quiet=True)
rc_wt = compare(doc, render(wt), quiet=True)
cell("blind spot: an uncommitted docstring edit is rc 0 at HEAD and rc 1 at WORKTREE",
rc_head == 0 and rc_wt == 1, f"HEAD rc {rc_head}, WORKTREE rc {rc_wt}")
rc_wt_norepo = graderc(td, rev=WORKTREE)
cell("WORKTREE outside a git work tree: rc 2, not a crash",
rc_wt_norepo == 2, f"rc {rc_wt_norepo}")
# -- the currency cell: the reason this document stays true ----------
here = os.path.join(repo, "docs", "TOOLS.md")
try:
want = render(collect(repo, "HEAD"))
rc = compare(here, want, quiet=True)
except SystemExit as exc:
rc = 2
cell("HEAD: the shipped map renders", False, str(exc))
cell("HEAD: docs/TOOLS.md is the render of this tree (the drift guard)",
rc == 0, f"rc {rc}")
passed = sum(1 for c in cells if c["ok"])
for c in cells:
print(f" {'ok ' if c['ok'] else 'FAIL'} {c['label']}"
+ (f" [{c['detail']}]" if c["detail"] and not c["ok"] else ""))
print(f"selftest: {len(cells)} cells, {passed} passed, {len(cells) - passed} failed")
return 0 if passed == len(cells) else 1
def graderc(td, rev=None):
"""rc of a full `--check` run against a directory that is not a repo."""
cmd = [sys.executable, os.path.abspath(__file__), "--repo", td]
if rev:
cmd += ["--rev", rev]
cmd += ["--check", os.path.join(td, "x.md")]
proc = subprocess.run(cmd, capture_output=True, text=True)
return proc.returncode
def compare(path, want, quiet=False):
"""0 current, 1 drift (first differing line printed), 2 unreadable."""
try:
with open(path, encoding="utf-8") as fh:
have = fh.read()
except OSError as exc:
if not quiet:
print(f"tool_map: cannot read document: {exc}")
return 2
if have == want:
return 0
if not quiet:
a, b = have.splitlines(), want.splitlines()
for i in range(max(len(a), len(b))):
got = a[i] if i < len(a) else "<end of document>"
exp = b[i] if i < len(b) else "<end of document>"
if got != exp:
print(f"DRIFT at line {i + 1}:")
print(f" document: {got}")
print(f" tree: {exp}")
break
return 1
def main():
ap = argparse.ArgumentParser(
description="Render the map of tools/ from the tree, and grade its drift.")
ap.add_argument("--repo", default=DEFAULT_REPO)
ap.add_argument("--rev", default="HEAD")
ap.add_argument("--write", metavar="PATH",
help=f"render the map to PATH (default target {DEFAULT_DOC})")
ap.add_argument("--check", metavar="PATH",
help="grade PATH against a fresh render; rc 1 on drift")
ap.add_argument("--json", metavar="PATH", help="write the collected data as JSON")
ap.add_argument("--selftest", action="store_true")
args = ap.parse_args()
if args.selftest:
return selftest(args.repo)
rc, _, _ = git(args.repo, "rev-parse", "--show-toplevel")
if rc != 0:
print(f"tool_map: cannot read repo: {args.repo} is not a git work tree")
return 2
if args.rev != WORKTREE:
rc, _, err = git(args.repo, "rev-parse", "--verify", f"{args.rev}^{{commit}}")
if rc != 0:
print(f"tool_map: cannot read rev {args.rev}: {err.strip().splitlines()[0] if err.strip() else 'no such object'}")
return 2
data = collect(args.repo, args.rev)
text = render(data)
if args.json:
try:
with open(args.json, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2, sort_keys=True)
fh.write("\n")
except OSError as exc:
print("cannot write --json " + args.json + ": " + str(exc), file=sys.stderr)
return 2
print(f"tool_map: wrote {args.json}")
if args.write:
try:
with open(args.write, "w", encoding="utf-8") as fh:
fh.write(text)
except OSError as exc:
print("cannot write --write " + args.write + ": " + str(exc), file=sys.stderr)
return 2
print(f"tool_map: wrote {args.write} ({len(text)} B, "
f"{len(data['rows'])} tools, {len(data['legs'])} gate legs)")
return 0
if args.check:
return compare(args.check, text)
print(text, end="")
return 0
if __name__ == "__main__":
sys.exit(main())