bae81b66c8
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>
156 lines
6.3 KiB
Python
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())
|