Files
Maven/docs/architecture/build_evidence.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

291 lines
12 KiB
Python

#!/usr/bin/env python3
"""Write architecture-evidence.txt: the reviewer's index into the pack.
It resolves a named symbol list against this checkout and prints where each one
is, or says plainly that it does not exist. A requested name that is absent is
evidence too, so nothing here is silently dropped or silently corrected.
Every contradiction is re-checked at generation time by running its own probe,
so the claim and the grep that supports it cannot drift apart in the pack.
python3 docs/architecture/build_evidence.py
"""
import os
import re
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
TREES = ["cmd", "internal"]
# The reviewer's list, verbatim on the left. Where a name does not exist in this
# repo, the right side is what it appears to mean. Resolution below reports both
# so a wrong name is visible rather than quietly fixed.
REQUESTED = [
("auth.TierFor", "auth.MaxLayer"),
("auth.Surface", None),
("auth.Layer", None),
("tool.Executor.Exec", None),
("tool.PolicyFor", None),
("tool.RiskOf", None),
("tool.RiskSafe", "tool.TierSafe"),
("tool.RiskDestructive", "tool.TierDestructive"),
("tool.RiskIrreversible", "tool.TierIrreversible"),
("router.ClaimOf", None),
("reactiveHandler", None),
("tickLoop", None),
]
# Added because the authorization function the reviewer wants to reconstruct
# runs through these and the list above does not reach them.
ALSO = [
"auth.Gate", "auth.Can", "auth.Requirement", "auth.Authority",
"auth.NewFloorEnrollment", "auth.StaticEnrollment", "auth.Scope",
"tool.Policy", "tool.RiskOfCapability", "tool.irreversibleVerbs",
"tool.ErrNeedsConfirm", "tool.ErrNeedsAuthedSurface", "tool.ErrNotEnabled",
"ipc.Server.Check", "ipc.CheckFunc",
"voice.PushToTalkReq", "voice.Sessions.Add", "voice.Session",
"PolicyFor", "RiskOf", "Executor.Exec",
"pendingAct", "resolveConfirm", "actionAct", "runTurn", "applyAction",
"querySources", "queryWalk", "StageZeroGrammars", "Router.Route",
]
PKG_DIR = {
"auth": "internal/auth", "tool": "internal/tool", "claim": "internal/claim",
"modes": "internal/modes", "router": "internal/router", "voice": "internal/voice",
"ipc": "internal/ipc", "store": "internal/store",
}
def go_files(rel):
full = os.path.join(ROOT, rel)
out = []
for base, _, names in os.walk(full):
for n in sorted(names):
if n.endswith(".go"):
out.append(os.path.relpath(os.path.join(base, n), ROOT))
return sorted(out)
def all_go():
out = []
for t in TREES:
out.extend(go_files(t))
return out
_CACHE = {}
def lines_of(rel):
"""Read once. resolve() sweeps every file per pattern per symbol, and
re-reading cmd/ and internal/ that many times took minutes."""
if rel not in _CACHE:
try:
_CACHE[rel] = open(os.path.join(ROOT, rel), errors="replace").read().splitlines()
except OSError:
_CACHE[rel] = []
return _CACHE[rel]
def resolve(sym):
"""Find the declaration of sym. Returns (path, line, text) or (None,)*3."""
tail = sym.split(".")[-1]
recv = sym.split(".")[-2] if sym.count(".") >= 1 else None
pats = [
re.compile(r"^func\s+\(\w+\s+\*?" + re.escape(recv or "\x00") + r"\)\s+" + re.escape(tail) + r"\b"),
re.compile(r"^func\s+" + re.escape(tail) + r"\b"),
re.compile(r"^type\s+" + re.escape(tail) + r"\b"),
re.compile(r"^\s*" + re.escape(tail) + r"\s+\w+\s*=\s"), # typed const
re.compile(r"^\s*" + re.escape(tail) + r"\s*=\s"),
re.compile(r"^(var|const)\s+" + re.escape(tail) + r"\b"),
re.compile(r"^\s*" + re.escape(tail) + r"\s+\w"), # struct field
]
pkg = sym.split(".")[0]
files = go_files(PKG_DIR[pkg]) if pkg in PKG_DIR else all_go()
files = [f for f in files if not f.endswith("_test.go")]
for pat in pats:
for rel in files:
for i, line in enumerate(lines_of(rel), 1):
if pat.match(line):
return rel, i, line.strip()
return None, None, None
def exported(pkg_rel):
"""Every exported declaration in a package, for the `claim.*` / `modes.*` asks."""
out = []
pat = re.compile(r"^(func|type|const|var)\s+\(?[^)]*\)?\s*([A-Z]\w*)")
fn = re.compile(r"^func\s+(\([^)]*\)\s*)?([A-Z]\w*)")
for rel in go_files(pkg_rel):
if rel.endswith("_test.go"):
continue
for i, line in enumerate(lines_of(rel), 1):
m = fn.match(line) or pat.match(line)
if m:
name = m.group(m.lastindex)
if name and name[0].isupper():
out.append((name, f"{rel}:{i}", line.strip()))
return out
def sh(cmd):
return subprocess.run(cmd, shell=True, cwd=ROOT, capture_output=True,
text=True).stdout.strip()
# Each probe is a shell command whose output IS the evidence. Re-run at pack
# time so the pack cannot claim something the checkout no longer shows.
CONTRADICTIONS = [
("auth surface/layer documented as control, not consumed on turn path",
# px.Surface is the Praxis lifecycle verb, an unrelated name collision, and
# Surfaced* are the read-out-item helpers. Excluded by name so the absence
# this probe reports is the auth Surface and not a filtering accident.
"grep -rnE 'req\\.Surface|sess\\.Surface|Session\\.Surface|auth\\.Surface|voice\\.Surface' cmd/mavend/*.go "
"| grep -v _test | grep -vE 'px\\.Surface|Surfaced' "
"|| echo '(no match: no file in cmd/mavend reads the auth Surface of a request or a session)'"),
("auth.Can runs only behind the IPC boundary",
"grep -rn 'auth\\.' cmd/ internal/ --include='*.go' | grep -v _test "
"| grep -v '^internal/auth/' | grep -vE ':[0-9]+:\\s*(//|\\*)'"),
("tool risk policy live on execution path",
"sed -n '176,190p' internal/tool/tool.go"),
("tool executor receives no reach/surface",
"grep -n 'func (e \\*Executor) Exec' internal/tool/tool.go"),
("voice server normalizes incoming surface to pc-client",
"grep -n 'SurfacePCClient' internal/voice/server.go"),
("claim abstraction exists but Route does not consume it",
"grep -rn 'ClaimOf' --include='*.go' cmd internal | grep -v _test || echo '(only the definition; no caller)'"),
("modes package is imported by nothing",
"grep -rn 'internal/modes' --include='*.go' cmd internal | grep -v '^internal/modes/' || echo '(no importer)'"),
("voice server DEFAULTS an empty surface, it does not overwrite a sent one",
"sed -n '193,201p' internal/voice/server.go"),
("session surface is hardcoded, independently of the request field",
"sed -n '146,149p' internal/voice/server.go"),
("HandlePushToTalk never reads req.Surface",
"sed -n '200,203p' cmd/mavend/voice.go"),
("hexis reuses the same risk policy",
"grep -n 'RiskOfCapability\\|PolicyFor' cmd/mavend/ecosystem_acts.go"),
("praxis lifecycle mutations bypass the risk policy entirely",
"sed -n '156,172p' cmd/mavend/ecosystem_acts.go"),
("Exec trusts a confirmed bool it cannot verify was bound",
"grep -n 'func (e \\*Executor) Exec' internal/tool/tool.go; grep -rn 'tools.Exec(' cmd/mavend/*.go | grep -v _test"),
("FloorEnrollment maps every same-uid caller to one surface",
"sed -n '63,72p' internal/auth/enrollment.go"),
("PasskeySession ignores Scope in both methods",
"grep -n 'func (s \\*PasskeySession) CurrentLayer\\|func (s \\*PasskeySession) Assert' internal/webauthn/session.go"),
("Claim.Coverage can be 1.0 with nothing extracted",
"grep -n 'func claimSpans' -A 4 internal/router/claim.go; grep -n 'd.Slots.Text = ex.Text' -B 2 internal/router/router.go; grep -n 'func (c Claim) Coverage' -A 7 internal/claim/claim.go"),
("claim_test asserts Band only, and every case sets Text == Utterance",
"grep -n 'Utterance:\\|Text:\\|want:' internal/router/claim_test.go | head -20"),
("systemctl reboot is destructive, not irreversible",
"grep -n 'irreversibleVerbs = map' -A 8 internal/tool/risk.go; grep -n 'reboot' deploy/mavend.json"),
]
def main() -> int:
out = []
w = out.append
w("architecture evidence pack")
w("=" * 72)
w("")
w("commit: " + sh("git rev-parse HEAD"))
w("date: " + sh("git log -1 --format=%cd --date=short"))
w("branch: " + sh("git rev-parse --abbrev-ref HEAD"))
w("")
w("working tree at pack time (git status --short):")
for line in (sh("git status --short") or "(clean)").splitlines():
w(" " + line)
w("")
w("The pack is built from the WORKING TREE, not from the commit. The lines")
w("above are the difference. deploy/mavend.json in particular is modified:")
w("phraser.model_path points at maven-instruct-b2, the committed value was")
w("Qwen3-1.7B-UD-Q4_K_XL. Sixteen 'configured-off' claims read this file.")
w("")
w("requested symbols")
w("-" * 72)
for name, actual in REQUESTED:
rel, line, text = resolve(name)
if rel:
w(f"- {name}")
w(f" {rel}:{line} {text}")
elif actual:
arel, aline, atext = resolve(actual)
w(f"- {name} -> DOES NOT EXIST in this repo")
if arel:
w(f" the name appears to be {actual}")
w(f" {arel}:{aline} {atext}")
else:
w(f" and neither does {actual}")
else:
w(f"- {name} -> NOT FOUND")
w("")
for pkg, rel in (("claim.*", "internal/claim"), ("modes.*", "internal/modes")):
w(f"{pkg} ({rel})")
w("-" * 72)
for name, anchor, text in exported(rel):
w(f"- {name}")
w(f" {anchor} {text}")
w("")
w("additional symbols on the authorization path")
w("-" * 72)
for name in ALSO:
rel, line, text = resolve(name)
w(f"- {name}")
w(f" {rel}:{line} {text}" if rel else " NOT FOUND")
w("")
w("known contradictions, each re-checked at pack time")
w("=" * 72)
w("The command under each claim was run against this checkout just now.")
w("Its output is what follows. Nothing here is transcribed by hand.")
w("")
for claim, cmd in CONTRADICTIONS:
w("- " + claim)
w(" $ " + cmd)
res = sh(cmd)
for line in (res or "(no output)").splitlines():
w(" " + line)
w("")
w("what the pack does NOT contain, and why")
w("=" * 72)
w("- .git, so no history and no gitignored working files travel with it.")
w("- deploy/telegram.env and deploy/db_key.env. The second holds the")
w(" database key. Both are gitignored and present in the working tree.")
w("- docker-compose.yml verbatim. It carries one live-looking credential on")
w(" line 132 (an uptime-kuma API key). The pack ships")
w(" docker-compose.redacted.yml with that one value replaced and nothing")
w(" else changed, so the mavcaldav and mavmaild claims stay checkable.")
w("- models/, deps/, *.db, *.onnx, *.gguf, certs, logs, node_modules.")
w("- internal/session, internal/db and tests/ from the requested list: none")
w(" of the three exists. Sessions live in internal/voice/session.go, the")
w(" store is internal/store, and tests sit beside their code as *_test.go.")
w("")
w("included test files, since the ask named them by subject:")
for pat, label in (
("internal/router", "routing and arbitration"),
("internal/tool", "risk and confirmation"),
("internal/auth", "authorization"),
("internal/claim", "claim"),
("cmd/mavend", "turn path, confirm gate, query chain"),
):
n = sh(f"find {pat} -name '*_test.go' | wc -l")
w(f" {label}: {n} *_test.go under {pat}/")
w("")
w("fixtures are synthetic, not captured speech: internal/router/eval/*.json")
w("and cmd/mavend/testdata/**.json are hand-written contracts. Named here")
w("because they are Russian utterances and look like personal data.")
path = os.path.join(HERE, "architecture-evidence.txt")
open(path, "w").write("\n".join(out) + "\n")
print(f"architecture-evidence.txt: {os.path.getsize(path)} bytes, {len(out)} lines")
return 0
if __name__ == "__main__":
sys.exit(main())