#!/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())