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>
291 lines
12 KiB
Python
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())
|