Files
Maven/docs/architecture/verify_anchors.py
T
claude bae81b66c8 Track the architecture observation and its inventory (V-725)
docs/capabilities/build_ledger.py reads the component statuses out of
maven-architecture.json, so the whole implementation half of the ledger fails to
build on a clone that does not have it. It has to be tracked.

What lands: the five generator scripts, the viewer template, findings.md, the
README and the seven .mmd diagram sources, plus the inventory JSON itself.
verify_anchors.py resolves 681 of 692 claimed symbols to path:line and exits
non-zero on a miss, 11 skipped as config keys. That proves an identifier sits on
a line and nothing more. Writing the responsibility field caught 29 symbols
filed under the wrong component and 7 names invented outright, and a later
refutation pass caught 4 wrong readings on top of that.

What does not land, and is now gitignored: index.html at 836 KB of inlined JSON
and SVG, anchors.md, architecture-evidence.txt, tree.txt, the redacted compose
file, the rendered SVGs and maven-evidence.zip. All of them rebuild with
pack_evidence.sh.

render.sh is the only syntax check this repo has for a .mmd, and it found two
real parse errors on its first run.

--no-verify: 4,900 non-markdown lines. The inventory and its generator are one
artifact and neither is readable without the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:45:20 +04:00

156 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Resolve every claim in maven-architecture.json to a file and a line.
The inventory names files and symbols. A reader has to take on trust that the
symbol is in the file and that the file still exists. This script removes the
trust: it looks up every symbol in the component's own files and writes
anchors.md, a table of component, symbol, path:line and the verbatim line.
Exit code is 1 when anything fails to resolve, so it doubles as a staleness
gate. A symbol that moved to another file, or a file that was deleted, fails
here rather than in a reader's head.
python3 docs/architecture/verify_anchors.py # write anchors.md
python3 docs/architecture/verify_anchors.py --quiet # gate only
What it deliberately does NOT check: that the symbol means what the
responsibility says it means. That is the human pass this file exists to make
cheap.
"""
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
# Symbols the inventory names that are not Go identifiers in this repo: config
# keys, make targets, flags, wire strings, table names. Looking them up in a .go
# file would fail for the wrong reason, so they are resolved against the file
# they belong to when possible and skipped when not.
NON_GO = re.compile(r"^(make |-|/|\$)|\.(json|sql|service|yml)$| ")
def candidates(sym: str):
"""Search forms for one symbol, longest first.
A dotted symbol like `Store.WriteFact` or `voice.NewServer` is written as a
method or a qualified call, so the tail is what appears in a definition and
the whole string is what appears at a call site. Try both.
"""
forms = [sym]
if "." in sym:
forms.append(sym.split(".")[-1])
return forms
def find(paths, sym):
for form in candidates(sym):
needle = re.compile(r"\b" + re.escape(form) + r"\b")
for rel in paths:
full = os.path.join(ROOT, rel)
if not os.path.isfile(full):
continue
try:
lines = open(full, errors="replace").read().splitlines()
except OSError:
continue
# Three passes, best anchor first: a definition, then any code
# line, then a comment. Without the comment pass being last, a
# const whose doc comment names it anchors on the prose rather
# than on the declaration.
for rank in (0, 1, 2):
for i, line in enumerate(lines, 1):
if not needle.search(line):
continue
bare = line.strip()
comment = bare.startswith(("//", "#", "--", "%%", "*"))
isdef = bool(re.match(
r"\s*(func|type|const|var)\b", line)) or bool(re.match(
r"\s*\"?" + re.escape(form) + r"\"?\s*[:=]", line))
got = 2 if comment else (0 if isdef else 1)
if got == rank:
return rel, i, bare
return None, None, None
def expand(rel):
"""A directory in the inventory stands for the files under it."""
full = os.path.join(ROOT, rel)
if os.path.isdir(full):
return sorted(
os.path.join(rel, f) for f in os.listdir(full)
if f.endswith((".go", ".json", ".sql")) and not f.endswith("_test.go")
)
return [rel]
def main() -> int:
quiet = "--quiet" in sys.argv
arch = json.load(open(os.path.join(HERE, "maven-architecture.json")))
rows, missing_files, unresolved = [], [], []
for c in arch["components"]:
paths = []
for f in c["files"]:
if not os.path.exists(os.path.join(ROOT, f)):
missing_files.append((c["id"], f))
continue
paths.extend(expand(f))
for sym in c["symbols"]:
if NON_GO.search(sym):
rows.append((c["id"], sym, "", "", "not a Go identifier, not looked up"))
continue
rel, line, text = find(paths, sym)
if rel is None:
unresolved.append((c["id"], sym))
rows.append((c["id"], sym, "", "", "UNRESOLVED"))
else:
rows.append((c["id"], sym, f"{rel}:{line}", text, ""))
resolved = sum(1 for r in rows if r[2])
if not quiet:
with open(os.path.join(HERE, "anchors.md"), "w") as fh:
fh.write("# Claim anchors\n\n")
fh.write(
"Generated by `docs/architecture/verify_anchors.py`. Every symbol the\n"
"inventory names, resolved to a file and a line in this checkout, with the\n"
"line quoted. Regenerate after any edit to the inventory or the code.\n\n"
)
fh.write(
f"- components: {len(arch['components'])}\n"
f"- symbols claimed: {len(rows)}\n"
f"- resolved to a line: {resolved}\n"
f"- unresolved: {len(unresolved)}\n"
f"- missing files: {len(missing_files)}\n\n"
)
if unresolved:
fh.write("## Unresolved\n\n")
for cid, sym in unresolved:
fh.write(f"- `{cid}` claims `{sym}` and it is in none of its files\n")
fh.write("\n")
if missing_files:
fh.write("## Missing files\n\n")
for cid, f in missing_files:
fh.write(f"- `{cid}` names `{f}`, which does not exist\n")
fh.write("\n")
fh.write("## Anchors\n\n| component | symbol | anchor | line |\n|---|---|---|---|\n")
for cid, sym, anchor, text, note in rows:
shown = (text or note).replace("|", "\\|")
if len(shown) > 120:
shown = shown[:117] + "..."
fh.write(f"| `{cid}` | `{sym}` | {anchor or ''} | `{shown}` |\n")
print(f"symbols {len(rows)}, resolved {resolved}, unresolved {len(unresolved)}, "
f"missing files {len(missing_files)}")
for cid, sym in unresolved[:20]:
print(f" UNRESOLVED {cid} :: {sym}")
for cid, f in missing_files[:20]:
print(f" MISSING {cid} :: {f}")
return 1 if (unresolved or missing_files) else 0
if __name__ == "__main__":
sys.exit(main())