From bae81b66c8dc91e14ff4708f741b6a0f55e91e2f Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 26 Aug 2026 12:45:20 +0400 Subject: [PATCH] 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 --- .gitignore | 10 + docs/architecture/README.md | 134 + docs/architecture/build_evidence.py | 290 + docs/architecture/build_inventory.py | 1234 ++++ docs/architecture/build_viewer.py | 47 + .../diagrams/01-system-topology.mmd | 120 + .../diagrams/02-core-internals.mmd | 160 + .../diagrams/03a-flow-reminder.mmd | 78 + docs/architecture/diagrams/03b-flow-fact.mmd | 61 + .../diagrams/03c-flow-world-query.mmd | 70 + .../diagrams/04-state-ownership.mmd | 115 + .../diagrams/05-dependency-boundary.mmd | 141 + docs/architecture/findings.md | 699 ++ docs/architecture/maven-architecture.json | 6263 +++++++++++++++++ docs/architecture/pack_evidence.sh | 139 + docs/architecture/render.sh | 44 + docs/architecture/verify_anchors.py | 155 + docs/architecture/viewer.template.html | 494 ++ 18 files changed, 10254 insertions(+) create mode 100644 docs/architecture/README.md create mode 100644 docs/architecture/build_evidence.py create mode 100644 docs/architecture/build_inventory.py create mode 100644 docs/architecture/build_viewer.py create mode 100644 docs/architecture/diagrams/01-system-topology.mmd create mode 100644 docs/architecture/diagrams/02-core-internals.mmd create mode 100644 docs/architecture/diagrams/03a-flow-reminder.mmd create mode 100644 docs/architecture/diagrams/03b-flow-fact.mmd create mode 100644 docs/architecture/diagrams/03c-flow-world-query.mmd create mode 100644 docs/architecture/diagrams/04-state-ownership.mmd create mode 100644 docs/architecture/diagrams/05-dependency-boundary.mmd create mode 100644 docs/architecture/findings.md create mode 100644 docs/architecture/maven-architecture.json create mode 100755 docs/architecture/pack_evidence.sh create mode 100755 docs/architecture/render.sh create mode 100644 docs/architecture/verify_anchors.py create mode 100644 docs/architecture/viewer.template.html diff --git a/.gitignore b/.gitignore index 6082fba..cce35e9 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,13 @@ __pycache__/ # the command in docs/capabilities/README.md, which runs as root in a container # and so cannot share the host cache. Multi-GB, entirely reproducible. /.cache/ + +# docs/architecture/ derived output. The sources, findings.md and the inventory +# JSON are tracked; these rebuild from them with pack_evidence.sh and are large. +/docs/architecture/index.html +/docs/architecture/anchors.md +/docs/architecture/architecture-evidence.txt +/docs/architecture/tree.txt +/docs/architecture/docker-compose.redacted.yml +/docs/architecture/diagrams/*.svg +/maven-evidence.zip diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..9ede86d --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,134 @@ +# docs/architecture + +An observation of Maven as built, read at `5cae33a` on 2026-08-25. It describes +what the code does today. It proposes nothing. + +This directory is a build output plus its sources. `index.html`, +`maven-architecture.json`, `anchors.md` and `diagrams/*.svg` are generated. + +## Read it + +| file | what it is | +|---|---| +| `index.html` | the viewer. Open it from the filesystem, no server needed. Five views, the rendered diagram above each, click a component for its record. | +| `findings.md` | the analysis. Kept apart from the facts on purpose. | +| `maven-architecture.json` | the inventory. 160 components, 204 relations. The factual source for everything else. | +| `anchors.md` | every symbol the inventory names, resolved to `path:line` with the line quoted. | +| `diagrams/*.mmd` | the five views as Mermaid source. `03a`, `03b` and `03c` are the three traced requests. | +| `diagrams/*.svg` | the same, rendered. | + +## Rebuild it + +```sh +python3 docs/architecture/build_inventory.py # → maven-architecture.json +python3 docs/architecture/verify_anchors.py # → anchors.md, exit 1 if stale +sh docs/architecture/render.sh # → diagrams/*.svg, then index.html +python3 docs/architecture/build_viewer.py # → index.html alone +``` + +`render.sh` drives mermaid-cli through the system chromium rather than letting +puppeteer download its own. It is also the only syntax check this repo has for a +`.mmd`. + +## What is verified, and what is not + +**Verified mechanically.** `verify_anchors.py` resolves all 692 claimed symbols +against the files their component names. Current state: 681 resolved to a line +and 0 unresolved, with 0 missing files. The other 11 are config keys and make +targets rather than Go identifiers, so they are skipped. The script exits +non-zero on any failure, which makes it a staleness gate. + +Writing it caught 29 symbols filed under the wrong component and seven names +that were wrong outright. Two examples: `Store.RecordEvent` for what is really +`Store.CreateEvent`, and `media.Keeper` for what is really `media.Store`. + +**Not verified.** That a symbol means what its `responsibility` says. An anchor +proves the identifier is on that line and nothing more. Judgements about +ownership, coupling and enforcement are readings of the code. A reading can be +wrong in a way grep cannot catch. + +**Marked, not resolved.** Relations carry a `confidence` field. `medium` means +the wiring is in the source and the call path was not traced end to end. `low` +means it was inferred from one reference. The viewer can hide both. Four +relations are `medium` and one is `low`. + +**Deployment-specific.** Sixteen components are `configured-off` against +`deploy/mavend.json` as it stood on the day, and that file was dirty in the +working tree. A different config makes different components live. `status` says +which, per component. + +## The one thing to check first + +`findings.md` 6.3 through 6.3d. They say the system has no single point that +decides whether an origin may cause an effect, and that the pieces which look +like that point are each answering a different question. + +Revised on 2026-08-25 after an independent second pass. Four readings changed +and one earlier statement was wrong. Section 6.3 marks the corrections. + +Start at `internal/tool/tool.go:181`, `cmd/mavend/ecosystem_acts.go:158` and +`internal/router/claim.go:38`. + +## The evidence pack + +`sh docs/architecture/pack_evidence.sh` builds `maven-evidence.zip` at the repo +root: this directory, the structural context, and whole source files for the +architectural seams. Whole files, never snippets, because a cut-down file loses +the call path that makes a claim checkable. + +The path list is an allowlist, not an exclusion list. A denylist ships whatever +nobody thought to exclude, and this tree has a database key in it. + +`architecture-evidence.txt` is the reviewer's index. It resolves a named symbol +list against the checkout and says plainly when a requested name does not exist. +It also re-runs the probe under every contradiction, so a claim and its grep +cannot drift apart. + +One file is not verbatim. `docker-compose.yml` carries an uptime-kuma API key, +so a redacted copy ships in its place with that one value replaced. The script +diffs the two and aborts if anything else changed. + +The scan at the end refuses to build on a credential-shaped hit rather than +printing a warning. Both of its first two versions were wrong in instructive +ways. The name filter deleted `internal/router/singletoken.go` for matching +`*token*`. The value scan flagged docker volume lines that name where a secret +would live and contain none. + +## The authorization function as implemented + +The reconstruction, at the one decision point that gates an act +(`internal/tool/tool.go:156`): + +``` +permit(tool, confirmed) = + row.status == "enabled" tool.go:164 + AND tier != irreversible risk.go:74 VoiceMayRun:false + AND (tier == safe OR confirmed) risk.go:72,76 +``` + +`tier` is `RiskOf(row)`. The reach is not an input: `Executor.Exec` takes +`(ctx, name, args, confirmed)` and no surface. + +`confirmed` is unproven at this boundary too. The invariant that a confirmation +binds one capability, one target and an expiry lives in `pendingAct` and +`resolveConfirm`. `Exec` trusts the boolean. + +The expression covers two of the three act paths. Hexis reuses it deliberately +(`cmd/mavend/ecosystem_acts.go:768`). The Praxis lifecycle path has no tier and +no confirm turn: `praxisItemAction.handle` calls straight through at +`ecosystem_acts.go:158`. + +Behind the IPC boundary, `auth.Can(method, scope, params)` runs with +`scope.Surface` always `SurfaceCoreProcess` (`internal/auth/enrollment.go:65`) +and step-up held as one global timestamp that ignores `Scope` +(`internal/webauthn/session.go:38` and `:62`). + +`auth` answers who may carry what authority. `tool` answers what effect a +capability has and what proof it demands. Those are orthogonal, not competing. +The decision combining them does not exist. + +Two representations of reach exist and both are ignored. `server.go:198` only +defaults an empty `p.Surface`, so a client-asserted one survives and nothing +reads it. `server.go:148` hardcodes `Session.Surface`. Since `req.Surface` is +request payload on an unauthenticated wire, it must not become an authorization +input as it stands. diff --git a/docs/architecture/build_evidence.py b/docs/architecture/build_evidence.py new file mode 100644 index 0000000..23d810d --- /dev/null +++ b/docs/architecture/build_evidence.py @@ -0,0 +1,290 @@ +#!/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()) diff --git a/docs/architecture/build_inventory.py b/docs/architecture/build_inventory.py new file mode 100644 index 0000000..b8ae65f --- /dev/null +++ b/docs/architecture/build_inventory.py @@ -0,0 +1,1234 @@ +"""Builds docs/architecture/maven-architecture.json from a hand-verified inventory. + +Every entry here was read out of the repository at commit 5cae33a. Nothing is +inferred from a directory name. +""" +import json, os + +C = [] + + +def c(id, type, group, responsibility, files, symbols, reads=None, writes=None, + calls=None, called_by=None, confidence="high", status="implemented", notes=""): + C.append(dict(id=id, type=type, group=group, responsibility=responsibility, + files=files, symbols=symbols, reads=reads or [], writes=writes or [], + calls=calls or [], called_by=called_by or [], confidence=confidence, + status=status, notes=notes)) + + +E = [] + + +def e(src, dst, kind, label, confidence="high", status="implemented", evidence=""): + E.append(dict(**{"from": src, "to": dst, "kind": kind, "label": label, + "confidence": confidence, "status": status, "evidence": evidence})) + + +CFG = "deploy/mavend.json" + +# ---------------------------------------------------------------- processes +c("proc.mavend", "process", "homesrv", + "The core daemon and the only holder of the database key. Owns the store, the IPC socket, the voice TCP listener, the tick loop, every in-core background worker and the child llama-server.", + ["cmd/mavend/main.go", "cmd/mavend/boot.go", "docker-compose.yml"], + ["main", "run", "daemonLock", "startBackground", "backgroundWorkers", "newDaemonAPI", "depsNow"], + reads=["cfg.mavend"], writes=["state.db_file"], + calls=["core.ipc_server", "core.voice_server", "core.tick_loop", "svc.llama_server"], + notes="compose service `mavend`. Two nets: `default` (module DNS) and `ecosystem`. Publishes 127.0.0.1:9110 -> container 9100.") + +c("proc.mavsttd", "process", "homesrv", + "Speech-to-text module process. Loads whisper.cpp (ggml-small.bin) through cgo and serves transcription on a unix socket.", + ["cmd/mavsttd/main.go", "cmd/mavsttd/whisper_handler.go", "docker-compose.yml"], + ["main", "whisperHandler"], called_by=["core.stt_seam"], + notes="Key-free. /run/maven/stt.sock on the shared `sockets` volume. Needs /dev/dri and the render gid for the Vulkan build.") + +c("proc.mavttsd", "process", "homesrv", + "Text-to-speech module process. Runs piper with ru_RU-irina-medium and returns wav bytes on a unix socket.", + ["cmd/mavttsd/main.go", "cmd/mavttsd/piper_handler.go", "docker-compose.yml"], + ["main", "piperHandler"], called_by=["core.tts_seam"], notes="/run/maven/tts.sock.") + +c("proc.mavweb", "process", "homesrv", + "The HTTP surface: PWA, dashboards, chat page, tool and routine consoles, WebAuthn enrollment and step-up, presence and ambient ingest.", + ["cmd/mavweb/main.go", "cmd/mavweb/pages.go", "cmd/mavweb/chat.go", "cmd/mavweb/ambient.go", + "cmd/mavweb/credentials.go", "cmd/mavweb/models.go", "cmd/mavweb/facts.go", "cmd/mavweb/ecosystem.go", + "cmd/mavweb/notifications.go"], + ["main", "corePage", "gatedPage", "handlePTT", "handleChatPage", "handleModels", + "handleSignal", "handleTools", "handleRoutines", "logUnguardedSurfaces"], + calls=["core.ipc_server", "core.voice_server", "ext.nexus", "ext.praxis", "ext.hexis"], + writes=["state.passkey_file"], + notes="Opens THREE ipc.Client connections to mavend (main, /models, /api/chat) because ipc.Client serialises on one mutex. Published on 127.0.0.1:9201 only.") + +c("proc.mavpoll", "process", "homesrv", + "Environment poller. Reads netdata alarms, uptime-kuma metrics, wireguard handshakes and optionally zenmoney, writing facts(kind=env, source=poll:*) over IPC. Writes only on value change.", + ["cmd/mavpoll/main.go", "internal/zenmoney/"], ["run"], + writes=["state.facts"], calls=["core.ipc_server", "ext.netdata", "ext.uptimekuma", "ext.zenmoney"], + notes="network_mode: host. Holds the third-party credentials so core never sees them; the zenmoney arm is dark because compose mounts no token file.") + +c("proc.mavwaked", "process", "workpc", + "Always-on listening client. Runs arecord, silero VAD and the keyword head, ships one utterance per activation over the voice wire, plays the reply through aplay, and receives proactive pushes on the same conn.", + ["cmd/mavwaked/main.go", "cmd/mavwaked/vad.go", "cmd/mavwaked/silero.go", "cmd/mavwaked/wakeword.go", + "cmd/mavwaked/wakefeatures.go", "cmd/mavwaked/nudge.go", "cmd/mavwaked/playback.go", "cmd/mavwaked/session.go", + "deploy/mavwaked.service"], + ["main", "run"], calls=["core.voice_server"], + notes="systemd USER unit on workpc, never in docker-compose. Requires maven-voice-tunnel.service: it reaches mavend's loopback 9100 over ssh, not the LAN.") + +c("proc.mavgpud", "process", "workpc", + "GPU supervisor on the workstation. Keeps llama-server loaded while the card is free, unloads on idle or contention, supervises the CrisperWhisper2 transcriber, and proxies both behind a bearer token.", + ["cmd/mavgpud/main.go", "cmd/mavgpud/gpu.go", "cmd/mavgpud/runner.go", "cmd/mavgpud/auth.go", + "deploy/mavgpud.service"], + ["main", "config", "requireToken"], called_by=["core.model_seam", "core.stt_seam"], + notes="Deployed separately from every Maven daemon. Maven never asks it to start anything; llm.Pair only reads /health.") + +c("proc.mavcaldav", "process", "homesrv", + "CalDAV reader and renderer. Polls a collection into facts(kind=env, source=poll:caldav) and can publish Maven's own reminders back as iCal.", + ["cmd/mavcaldav/main.go", "cmd/mavcaldav/render.go", "internal/calendar/"], + ["run", "checkRenderTarget"], writes=["state.facts"], calls=["core.ipc_server"], + status="built-not-deployed", + notes="Commented out in docker-compose.yml, and the comment there names the cost: the `calendar` query source and loop.State.CalendarBusy read facts nobody writes.") + +c("proc.mavmaild", "process", "homesrv", + "IMAP reader. Fetches unseen messages and hands each to core on ipc.MethodIngestMail; core extracts task candidates with the resident model.", + ["cmd/mavmaild/main.go", "internal/email/"], ["run"], + calls=["core.mail_intake"], writes=["state.maildata"], status="built-not-deployed", + notes="Commented out in docker-compose.yml. Reads its password from a file so core never sees it.") + +c("proc.mavenclient", "process", "workpc", + "Reference voice client. Ships one wav per invocation over the voice wire and writes the reply wav. No VAD, no keyword.", + ["cmd/mavenclient/main.go"], ["main"], calls=["core.voice_server"], + notes="A reference and test binary, not a deployed service.") + +c("proc.mavseal", "process", "homesrv", + "Recovery CLI. Re-encrypts a live tmpfs working copy back over the ciphertext file when mavend was killed rather than shut down.", + ["cmd/mavseal/main.go"], ["main"], reads=["state.db_tmpfs"], writes=["state.db_file"], + notes="Not part of the daemon. Uses VACUUM INTO, so it is safe against a live database.") + +c("proc.mavupdate", "process", "homesrv", + "Deployment CLI with automatic rollback. The only trigger for the update path.", + ["cmd/mavupdate/main.go", "internal/update/"], ["main", "update.Updater"], + calls=["core.ipc_server"], + notes="Deliberately has no IPC method and no web button: there is no MethodApplyUpdate in internal/ipc, and mavend never constructs an update.Updater.") + +c("proc.e2eprobe", "process", "dev", + "Typed IPC driver written for the 2026-08-15 acceptance session.", + ["cmd/e2eprobe/main.go"], ["main"], calls=["core.ipc_server"], status="temporary", + notes="Its own doc comment says it is removed after the session. It is still in the tree.") + +c("proc.labelgen", "process", "dev", + "Offline labeller. Runs the real stage 0 grammars over an utterance file and prints JSONL training data for the routing heads.", + ["cmd/labelgen/main.go"], ["main"], reads=["router.stage0"], + notes="Omits the wakeword-act grammar, whose allowlist is a deployment's enabled tool names.") + +# ---------------------------------------------------------------- external +c("svc.llama_server", "model", "homesrv", + "The resident model. A llama-server child process mavend starts and owns, serving both routing and phrasing.", + ["internal/phraser/server.go", "internal/phraser/llmphraser.go", "internal/llm/client.go", CFG], + ["phraser.NewLLMPhraser", "phraser.Config", "llm.Client", "llm.Gate"], + called_by=["core.phraser", "core.llm_router", "core.replier"], + notes="deploy/mavend.json currently points model_path at maven-instruct-b2-Q4_K_XL.gguf; the committed value was Qwen3-1.7B-UD-Q4_K_XL. n_ctx 4096, n_gpu_layers 99, cache_ram_mib 512.") + +c("ext.searxng", "external", "homesrv", + "Self-hosted metasearch. The first world source, asked after every source reading his own data.", + ["cmd/mavend/searchwire.go", "internal/websearch/", CFG], + ["wireSearch", "websearch.Client.Search"], called_by=["core.q.search"], + notes="http://searxng:9563. Needs `json` in search.formats. Only the query string leaves the box.") + +c("ext.kiwix", "external", "homesrv", + "Offline ZIM encyclopedia server. The fallback behind SearXNG.", + ["cmd/mavend/kiwixwire.go", "internal/kiwix/", CFG], + ["wireKiwix", "kiwix.Client"], called_by=["core.q.kiwix"], + notes="http://kiwix-server:8080. Books wikipedia_en_all_maxi_2026-02 and wikipedia_ru_all_maxi_2026-02.") + +c("ext.nexus", "external", "ecosystem", + "Identity service. Resolves free text to a canonical entity id; ambiguity asks rather than picks.", + ["cmd/mavend/ecosystem.go", CFG], ["nexusClient", "nexusClient.Resolve", "wireEcosystem"], + called_by=["core.ecosystem", "core.fact_enrichment", "core.daemon_api"], notes="http://nexus:9740.") + +c("ext.praxis", "external", "ecosystem", + "Attention and operational-state service, read over its HTTP tools API and never from its SQLite file.", + ["cmd/mavend/ecosystem.go", "cmd/mavend/attentionq.go", CFG], + ["praxisClient", "praxisClient.ListAttention", "praxisClient.postItemAction"], + called_by=["core.q.attention", "core.ecosystem_acts"], + notes="http://praxis:8989. Surfaced is not acknowledged; acknowledged is not resolved.") + +c("ext.hexis", "external", "ecosystem", + "Capability execution service. The path a mutating act takes when it is not a local allowlisted tool.", + ["cmd/mavend/ecosystem.go", "cmd/mavend/ecosystem_acts.go", CFG], + ["hexisclient.New", "handleHexisAct", "pendingHexisExec"], + called_by=["core.action_act"], + notes="http://hexis:9741. Free text never reaches a mutating call: an entity id is resolved first.") + +c("ext.telegram", "external", "internet", + "Telegram Bot API through a SOCKS relay. Both an away reach and, since V-637, an inbound channel.", + ["internal/delivery/telegramsink/telegramsink.go", "internal/delivery/telegramsink/intake.go", CFG], + ["telegramsink.New", "telegramsink.NewPoller"], + called_by=["core.sink_telegram", "core.telegram_intake"], + notes="proxy socks5://192.168.240.1:10808, intake: true in the deployed config. Long-poll getUpdates, not a webhook.") + +c("ext.ntfy", "external", "internet", + "Push reach. Present in the config and disabled there.", + ["internal/delivery/ntfysink/", "cmd/mavend/main.go", CFG], ["ntfysink.New", "wireNtfySink"], + called_by=["core.sink_ntfy"], status="configured-off", + notes='deploy/mavend.json sets ntfy.disabled = true, so wireNtfySink returns a nil Sink and the dispatcher slot is nil.') + +c("ext.netdata", "external", "homesrv", "Resource alarms, read by mavpoll.", + ["cmd/mavpoll/main.go"], ["run"], called_by=["proc.mavpoll"]) +c("ext.uptimekuma", "external", "homesrv", + "Service up/down, read by mavpoll over /metrics with an API key. The source of truth for service_down.", + ["cmd/mavpoll/main.go", "internal/loop/rules.go"], ["run", "loop.ServiceDownRule"], called_by=["proc.mavpoll"]) +c("ext.zenmoney", "external", "internet", "Spending totals. Dark: compose mounts no token file.", + ["internal/zenmoney/", "cmd/mavpoll/main.go"], ["zenmoney.Client"], + called_by=["proc.mavpoll"], status="configured-off") +c("ext.homeassistant", "external", "lan", + "The house. Discovery proposes one always-destructive tool row per controllable device.", + ["internal/smarthome/", "cmd/mavend/smarthome.go", CFG], ["wireSmartHome", "smarthome.Client", "homeWiring"], + called_by=["core.home_worker", "core.q.home"], status="configured-off", + notes='deploy/mavend.json smarthome.enabled = false.') +c("ext.openmeteo", "external", "internet", "Weather provider.", + ["internal/weather/", "cmd/mavend/voicewire.go"], ["weather.NewOpenMeteoProvider", "weather.NewStubProvider"], + called_by=["core.q.weather"], status="configured-off", + notes="wireVoice picks it only when cfg.Voice.Weather.Provider == 'open-meteo'. The deployed voice block has no `weather` key, so the Stub provider is wired and the weather query source answers from a stub.") +c("ext.vikunja_mcp", "external", "lan", + "MCP server whose tools are PROPOSED into the same act allowlist as everything else.", + ["internal/mcp/", "cmd/mavend/mcp.go", CFG], ["wireMCP", "mcp.Manager", "mcpWiring"], + called_by=["core.mcp_worker"], status="configured-off", notes="mcp.servers[0].enabled = false.") +c("ext.cw2_stt", "external", "workpc", + "CrisperWhisper2 turbo on the workstation, supervised by mavgpud on port 8081. A second service, not a second endpoint.", + ["cmd/mavgpud/main.go", "internal/stt/", "cmd/mavend/voicewire.go", "deploy/cw2/", CFG], ["stt.Pair", "sttSeam"], + called_by=["core.stt_seam"], notes="Silent fallback to mavsttd when the workstation is down; a worse transcript is still a turn.") +c("ext.piper", "external", "homesrv", "The TTS binary mavttsd runs.", + ["cmd/mavttsd/piper_handler.go"], ["piperHandler"], called_by=["proc.mavttsd"]) +c("ext.whispercpp", "external", "homesrv", "whisper.cpp, linked into mavsttd through cgo.", + ["cmd/mavsttd/whisper_handler.go"], ["whisperHandler"], called_by=["proc.mavsttd"]) +c("ext.alsa", "external", "workpc", "arecord and aplay, spawned by mavwaked; the mic is a named ALSA plug device.", + ["cmd/mavwaked/main.go", "deploy/asoundrc", "deploy/mavwaked.service"], ["run"], called_by=["proc.mavwaked"]) + +# ---------------------------------------------------------------- boundaries +c("bnd.ipc", "boundary", "homesrv", + "The core-to-module boundary. Length-prefixed JSON over a unix domain socket; 0700 dir and 0600 socket are the auth floor. Core mediates and never hands back a db handle.", + ["internal/ipc/frame.go", "internal/ipc/wire.go", "internal/ipc/server.go", "internal/ipc/client.go"], + ["writeFrame", "readFrame", "maxFrame", "Method", "ipc.Server", "ipc.Client", "ipc.DialWait"], + called_by=["proc.mavweb", "proc.mavpoll", "proc.mavcaldav", "proc.mavmaild", "proc.mavupdate", "proc.e2eprobe"], + notes="Hand-rolled framing, kept deliberately (Vikunja #410). 64 method constants in internal/ipc/wire.go, 63 plus Ping.") + +c("bnd.voice_tcp", "boundary", "homesrv", + "The client-to-core network surface. Plaintext TCP with no auth of its own; every conn registers a Session and carries both requests and server-initiated pushes.", + ["internal/voice/server.go", "internal/voice/wire.go", "internal/voice/session.go", "docker-compose.yml"], + ["voice.Server", "voice.Sessions", "PushToTalkReq", "PushToTalkResp", "Sessions.PushToMostRecent"], + called_by=["proc.mavwaked", "proc.mavenclient", "proc.mavweb"], + notes="Bound 0.0.0.0:9100 inside the container so mavweb can reach it by name, published only on 127.0.0.1:9110. workpc reaches it over ssh. TWO representations of reach, both ignored: server.go:198 only DEFAULTS an empty p.Surface, so a client-asserted SurfaceVoice survives and HandlePushToTalk never reads it; server.go:148 hardcodes Session.Surface to SurfacePCClient for every conn. req.Surface is request payload on an unauthenticated wire, so any client can claim pc_client. It must not become an authorization input as it stands.") + +c("bnd.worker", "boundary", "homesrv", + "The core-to-stt/tts boundary. One Client, one conn, one mutex; the Transcriber and Synthesizer interfaces are the Stub/Remote swap seam.", + ["internal/worker/client.go", "internal/worker/server.go", "internal/worker/wire.go", + "internal/stt/stt.go", "internal/tts/tts.go"], + ["worker.Client", "worker.Dial", "stt.NewRemote", "tts.NewRemote"], + called_by=["core.stt_seam", "core.tts_seam"]) + +c("bnd.http_web", "boundary", "homesrv", + "The mavweb HTTP surface. Loopback-only by construction; step-up exists only when -webauthn-origin and -webauthn-rpid are set.", + ["cmd/mavweb/main.go"], ["mux", "logUnguardedSurfaces", "mavwebHTTPServer", "requireStepUp"], + notes="Six surfaces are named as unguarded without WebAuthn: POST /tools, /routines, /models, /api/revert, /api/chat, /api/ptt.") + +c("bnd.http_ecosystem", "boundary", "ecosystem", + "The Maven-to-ecosystem HTTP boundary. Every request carries a contract version header, X-Requested-By: maven and a correlation id minted once per action.", + ["cmd/mavend/ecosystem.go"], + ["ecosystemHTTP", "setHeaders", "withCorrelationID", "ecosystemAPIVersion", "mavenRequester", "ecosystemError"]) + +# ---------------------------------------------------------------- core: entry +c("core.ipc_server", "service", "mavend", + "The IPC listener. Accepts module connections, runs the one Check authorization hook, then dispatches to CoreAPI or to one of the bypass function fields.", + ["internal/ipc/server.go", "cmd/mavend/main.go"], + ["ipc.Listen", "Server.Serve", "Server.Check", "Server.SetAPI", "Server.StepUp", "Server.UnlockFn", + "Server.WrapKeyFn", "Server.IngestMailFn", "Server.SwapModelFn", "Server.ModelStatusFn", + "Server.DescribeImageFn", "Server.CaptureStartFn"], + calls=["core.auth_gate", "core.daemon_api"], called_by=["bnd.ipc"], + notes="Eight function fields bypass CoreAPI entirely. Each is nil unless its config block exists, and nil means ErrUnknownMethod on the wire.") + +c("core.auth_gate", "service", "mavend", + "The single authorization guard. Locked, it is a default-deny allowlist of three methods; unlocked, it is auth.Gate over Enrollment and PasskeySession.", + ["internal/auth/gate.go", "internal/auth/policy.go", "internal/auth/tier.go", "internal/auth/enrollment.go", + "cmd/mavend/main.go"], + ["auth.Gate.Check", "auth.Requirement", "auth.Can", "auth.MaxLayer", "auth.Surface", "auth.Layer", + "auth.NewFloorEnrollment", "webauthn.NewPasskeySession", "errLocked"], + called_by=["core.ipc_server"], + notes="Two tier systems exist and this is the one the turn path does NOT read. FloorEnrollment maps every same-uid caller to SurfaceCoreProcess/L3, and no file in cmd/mavend reads req.Surface or Session.Surface. The live act gate is core.risk_policy in internal/tool.") + +c("core.daemon_lock", "service", "mavend", + "Cold-start unlock. When a wrapped key blob exists and no env key is set the daemon boots LOCKED, serves three methods, and wires everything else inside UnlockFn after a passkey assertion.", + ["cmd/mavend/main.go", "cmd/mavend/keyfile.go", "internal/webauthn/"], + ["daemonLock", "daemonLock.unlock", "daemonLock.closeStore", "srv.UnlockFn", "srv.WrapKeyFn", + "webauthn.UnwrapKey", "webauthn.WrapKey", "wrapKeyToFile", "BlobV1"], + reads=["state.wrapped_key"], writes=["state.wrapped_key", "state.db_file"], + notes="The whole daemon is wired twice, in two places, minutes or days apart. boot.go exists because those two lists had already drifted (V-639).") + +c("core.daemon_api", "adapter", "mavend", + "The daemon's CoreAPI: the store adapter plus eight closures over the tick loop, the event bus, the decision ring, the voice handler and the Nexus client.", + ["cmd/mavend/tick_api.go", "cmd/mavend/boot.go"], + ["daemonAPI", "newDaemonAPI", "bootDeps", "daemonAPI.Chat", "daemonAPI.TickTrace", + "daemonAPI.DayPlan", "daemonAPI.ResolveEntity", "daemonAPI.RecentEvents", "daemonAPI.TurnDecisions"], + calls=["core.store_api", "core.tick_loop", "core.reactive_handler", "core.event_bus", "ext.nexus"], + called_by=["core.ipc_server"], + notes="daemonAPI.chatFn is the voice handler; the handler's own api field is back-patched to daemonAPI by upgradeAPI. A deliberate two-way back-patch, documented on both sides.") + +c("core.store_api", "adapter", "mavend", + "The plain CoreAPI over the store. Every module method that is a state operation lands here.", + ["internal/ipc/storeapi.go", "internal/ipc/coreapi.go", "internal/ipc/api.go", "internal/ipc/unimplemented.go"], + ["ipc.NewStoreAPI", "ipc.CoreAPI", "ipc.UnimplementedCoreAPI"], + calls=["state.db"], called_by=["core.daemon_api", "core.intake_api"]) + +c("core.intake_api", "adapter", "mavend", + "A decorator over CoreAPI that publishes one envelope per successful intake write (WriteFact, WriteNote, CaptureTask) into the in-memory journal.", + ["cmd/mavend/intake.go"], ["newIntakeAPI", "intakeAPI", "newEventBus", "intakeEventsFn"], + calls=["core.store_api", "core.event_bus"], called_by=["core.daemon_api"], + notes="cmd/mavend/mail.go reaches past the decorator to st.CaptureTask and publishes by hand. That exception is stated in intake.go.") + +c("core.event_bus", "shared-state", "mavend", + "Bounded in-memory intake journal. Read only by /events and recent_events; nothing Maven says depends on it, and it dispatches nothing.", + ["internal/event/bus.go", "internal/event/event.go"], + ["event.Bus", "Bus.Publish", "Bus.Recent", "Bus.Subscribe", "Event.Normalize"], + called_by=["core.intake_api", "core.daemon_api"]) + +c("core.voice_server", "service", "mavend", + "The voice TCP listener and session registry. One goroutine per conn; the same conn carries request/response and server-initiated pushes, serialised by the per-session mutex.", + ["internal/voice/server.go", "internal/voice/session.go", "cmd/mavend/voicewire.go"], + ["voice.NewServer", "voice.Server.Serve", "voice.Sessions", "voice.Handler"], + calls=["core.reactive_handler"], called_by=["bnd.voice_tcp"]) + +c("core.wiring", "service", "mavend", + "wireVoice: builds the stt and tts seams, the embedder, the routing heads, the tool executor, MCP, the house, the LAN scanner, the weather provider, the model seam, the router cascade, the sessions registry, the voice sink, memory, dialogue, the replier, the handler and the TCP listener.", + ["cmd/mavend/voicewire.go"], + ["wireVoice", "voiceWiring", "buildRouter", "modelSeam", "sttSeam", "pickLLMRouter", + "seedClassifier", "seedTools", "repairFactVectors", "checkStoredEmbedder", "runReembed"], + called_by=["proc.mavend"], + notes="One 270-line function that constructs seventeen subsystems and returns a struct the rest of the daemon reads fields off.") + +# ---------------------------------------------------------------- core: turn +c("core.reactive_handler", "service", "mavend", + "The turn pipeline shared by every reach. Holds stt, tts, the router, the CoreAPI, the tool executor and matcher, the phraser, the replier, recall, the crawler, search, Kiwix, the house, the LAN scanner, the weather provider, the raw store, the time parser, dialogue and clarify state, the decision ring, the trace sink, the ecosystem clients and five pieces of per-turn mutable state.", + ["cmd/mavend/voice.go", "cmd/mavend/voicewire.go"], + ["reactiveHandler", "runTurn", "HandlePushToTalk", "handleText", "applyAction", "upgradeAPI", + "reply", "chatHistory", "turnSource", "sourceVoice", "sourceText"], + calls=["core.turn_route", "core.preroute", "core.action_table", "core.replier", "core.stt_seam", "core.tts_seam"], + called_by=["core.voice_server", "core.daemon_api", "core.telegram_intake"], + notes="34 fields. It is the single junction of routing, memory, tools, ecosystem, search, the house, the LAN and dialogue state.") + +c("core.turn_route", "arbitration", "mavend", + "This turn's routing, memoised with sync.Once and carried on the context so the clarify resolver and the action pipeline act on one decision. Also decides an elliptical follow-up from the previous turn instead of routing it.", + ["cmd/mavend/turnroute.go", "cmd/mavend/continuation.go", "cmd/mavend/turnrole.go"], + ["turnRoute", "turnRoute.resolve", "withTurnRoute", "turnRouteFrom", "routeForRole", + "needsRoute", "continuationDecision"], + calls=["router.cascade"], called_by=["core.reactive_handler", "core.preroute"], + notes="Exists because arbitration was inverted in V-560: routing twice cost a second on the resident model and could disagree with itself.") + +c("core.preroute", "arbitration", "mavend", + "The ordered ladder of stateful pre-emptors that may claim a turn before routing: expired-clarify notice, confirm answer, targeted repair, untargeted repair, command prohibition, clarify answer, quiet toggle, snooze, ack, reminder cancellation, ordinal selection.", + ["cmd/mavend/voice.go", "cmd/mavend/confirm.go", "cmd/mavend/repair.go", "cmd/mavend/clarify.go", + "cmd/mavend/command_prohibition.go", "cmd/mavend/quiet_toggle.go", "cmd/mavend/snooze.go", + "cmd/mavend/ack.go", "cmd/mavend/reminder_cancel.go", "cmd/mavend/ordinal.go", "cmd/mavend/decisiontrace.go"], + ["resolveConfirm", "resolveRepair", "resolveUntargetedRepair", "resolveCommandProhibition", + "resolveClarifyAnswer", "resolveQuietToggle", "resolveSnooze", "resolveAck", + "resolveReminderCancellation", "resolveCandidate", "preRouteLadder", "notePreRoute", + "clarifyExpiredNotice", "withNotice", "withResumed"], + calls=["state.clarify_store", "state.pending_act", "state.dialogue_sessions"], + called_by=["core.reactive_handler"], + notes="Eleven rungs, order argued step by step in runTurn. A new rung must also appear in preRouteLadder or it is missing from the decision record.") + +c("core.action_table", "arbitration", "mavend", + "Per-intent dispatch. Seven entries, one per router intent; a handler returning the empty string defers to the replier.", + ["cmd/mavend/actions.go"], ["actionHandlers", "applyAction"], + calls=["core.action_fact", "core.action_reminder", "core.action_act", "core.action_note", + "core.action_chat", "core.action_system", "core.query_chain"], + called_by=["core.reactive_handler"]) + +c("core.action_fact", "handler", "mavend", + "IntentFact: refuse question-shaped and complaint utterances, write the fact, prune and re-insert its recall vector, feed pattern detection.", + ["cmd/mavend/actions_fact.go", "cmd/mavend/factgate.go", "cmd/mavend/patterns.go"], + ["actionFact", "factConfidence", "pruneFactVectors", "store.FactRecallText", "router.IsQuestionShaped", + "router.IsTransientComplaint"], + writes=["state.facts", "state.memory_vectors"], + calls=["core.recall", "core.query_chain", "core.action_chat"], called_by=["core.action_table"], + notes="Re-routes a question-shaped fact into actionQuery and a complaint into actionChat, so a fact handler can return a query answer or a chat answer.") + +c("core.action_reminder", "handler", "mavend", + "IntentReminder: fall back to the time parser for stage-0 matches, create the row, and confirm from the stored fire time rather than from the utterance.", + ["cmd/mavend/actions_reminder.go", "cmd/mavend/reminderwhen.go", "cmd/mavend/reminderbody.go"], + ["actionReminder", "reminderConfirm", "reminderBody", "router.ResolvedTheHour", "refusesCommand"], + writes=["state.reminders"], called_by=["core.action_table"], + notes="Never sets Cron. store.Reminder and ipc.CreateReminder both carry a Cron field and no spoken path writes it.") + +c("core.action_act", "handler", "mavend", + "IntentAct: the enabled-tool allowlist, the destructive confirm gate, the Hexis capability path, and entity resolution in front of any mutation.", + ["cmd/mavend/actions_act.go", "cmd/mavend/confirm.go", "cmd/mavend/ecosystem_acts.go", + "cmd/mavend/entityname.go", "internal/tool/"], + ["actionAct", "pendingAct", "resolveConfirm", "tool.Executor", "tool.Matcher", + "handleHexisAct", "pendingHexisExec", "hexisBeforeClarify"], + reads=["state.tools"], calls=["ext.hexis", "ext.homeassistant", "ext.vikunja_mcp"], + called_by=["core.action_table"], + notes="Three gates, none of them the caller's surface: the enabled allowlist, the risk tier (core.risk_policy) and the confirm turn. A Hexis confirmation binds capability id, target entity, arguments, requester and expiry. Filling in a clarified argument never grants authority.") + +c("core.ecosystem_hexis_gate", "handler", "mavend", + "The Hexis capability act path. Resolves the entity through Nexus, reads the tier Hexis declares rather than deriving a second opinion, and parks a mutating capability for a confirm bound to capability, target and expiry.", + ["cmd/mavend/ecosystem_acts.go"], + ["handleHexisAct", "execHexis", "pendingHexisExec", "hexisBeforeClarify"], + calls=["ext.hexis", "ext.nexus"], called_by=["core.action_act"], + notes="The second act path. It reuses internal/tool's policy deliberately (ecosystem_acts.go:768), so local rows and Hexis capabilities share one tier vocabulary. Praxis does not.") + +c("core.praxis_acts", "handler", "mavend", + "The Praxis item-lifecycle mutations: acknowledge, resolve, ignore, pin. Reached from actionAct before the local executor, resolved against the last read-out list for an ordinal, and executed directly.", + ["cmd/mavend/ecosystem_acts.go"], + ["handlePraxisAct", "praxisItemAction", "praxisItemAction.handle", "praxisCapabilities", + "resolveSurfacedPosition", "rememberSurfaced"], + calls=["ext.praxis"], reads=["state.surfaced_items"], called_by=["core.action_act"], + notes="The third act path, and the one with NO risk tier and NO confirm turn: handle() calls a.call(ctx, px, id) directly (ecosystem_acts.go:158). A remote mutation that runs on first hearing. Reversible on the Praxis side, which is a reason nothing in the code states.") + +c("core.risk_policy", "service", "mavend", + "The tier system that actually gates an act. RiskOf sorts a tool row into safe, destructive or irreversible; PolicyFor maps the tier to a confirm requirement and a VoiceMayRun flag. Unknown shapes default upward to destructive.", + ["internal/tool/risk.go", "internal/tool/tool.go"], + ["Risk", "TierSafe", "TierDestructive", "TierIrreversible", "RiskOf", "RiskOfCapability", + "PolicyFor", "Policy.Confirm", "Policy.VoiceMayRun", "irreversibleVerbs", "Executor.Exec"], + called_by=["core.action_act"], + notes="VoiceMayRun is NOT conditioned on the reach: Executor.Exec takes (ctx, name, args, confirmed) and no surface. It also has no proof the boolean was bound correctly: that invariant lives in pendingAct and resolveConfirm, and Exec trusts it because only two callers exist. Covers two of the three act paths (local rows, and Hexis via RiskOfCapability at ecosystem_acts.go:768); the Praxis lifecycle path bypasses it entirely. `systemctl reboot` is TierDestructive, not TierIrreversible, so it runs after one spoken confirm. internal/tool/risk.go:84 states it is not a sandbox.") + +c("core.action_note", "handler", "mavend", "IntentNote: write the note and index it for recall.", + ["cmd/mavend/actions_note.go", "cmd/mavend/notefragment.go"], ["actionNote"], + writes=["state.notes", "state.memory_vectors"], called_by=["core.action_table"]) + +c("core.action_chat", "handler", "mavend", + "IntentChat: build history from the dialogue session and let the phraser answer from general knowledge plus the context block.", + ["cmd/mavend/actions.go"], ["actionChat", "chatHistory", "phraser.PhraseChat", "phraser.ChatFallback"], + calls=["core.phraser"], called_by=["core.action_table", "core.action_fact"]) + +c("core.action_system", "handler", "mavend", + "IntentSystem: clock, date and system-observable answers, read off the handler's own clock and keyword tests.", + ["cmd/mavend/voice.go", "cmd/mavend/actions.go"], ["actionSystem", "replySystem"], called_by=["core.action_table"]) + +# ---------------------------------------------------------------- router +c("router.cascade", "arbitration", "mavend", + "The routing cascade. Stage 0 grammars win outright at confidence 1.0; then the routing heads; then the LLM router; then the classifier with slot extraction and a confidence gate. Every stage may decline and the next one answers.", + ["internal/router/router.go", "internal/router/intent.go", "internal/router/source.go", + "internal/router/slots.go"], + ["router.Router", "router.New", "Router.Route", "router.Config", "Decision", "Intent", "Source", + "Sources", "ValidSource", "gateLLMDecision", "fillSlots", "CorrectMisroute", "SourceAnchored"], + calls=["router.stage0", "router.heads", "router.llm", "router.classifier", "router.extractor"], + called_by=["core.turn_route"], + notes="A route produces TWO decisions: Intent (one of seven) and Source (one of twelve, read on IntentQuery alone). SourceAnchored is set only by a stage 0 grammar.") + +c("router.stage0", "arbitration", "mavend", + "The deterministic grammar set, in the daemon's order. First match wins. The order is the contract and each rule's comment argues its position.", + ["internal/router/stagezero.go", "internal/router/stage0.go", "internal/router/agendaq.go", + "internal/router/worldquery.go", "internal/router/praxis.go", "internal/router/task.go", + "internal/router/commandframe.go", "internal/router/barecapture.go", "internal/router/possession.go", + "internal/router/list.go", "internal/router/feeds.go", "internal/router/help.go", + "internal/router/implicitquery.go", "internal/router/fragment.go", "internal/router/reminderreport.go", + "internal/router/taskstatus.go", "internal/router/knowledge.go"], + ["StageZeroGrammars", "Grammar", "Grammar.Evaluate", "DefaultGrammars", "CommandProhibitionGrammar", + "AmbiguousFragmentGrammar", "SystemTimeDateGrammars", "AgendaQueryGrammars", "ImplicitElapsedQueryGrammar", + "WorldQueryGrammars", "MavenHelpGrammar", "FeedQueryGrammar", "TaskListGrammar", "ListGrammars", + "ReminderGrammar", "ReminderCancellationReportGrammar", "PraxisGrammars", "TaskStatusGrammar", + "BareCaptureGrammar", "TaskCaptureGrammar", "NarrativeQueryGrammars", "PossessionStatementGrammar", + "StripWakeToken"], + called_by=["router.cascade", "proc.labelgen", "eval.router"], + notes="One list, called by both buildRouter and the eval fixture, because the two copies had already drifted (V-693). PraxisGrammars is the only path to Praxis.") + +c("router.heads", "model", "mavend", + "Routing heads: a softmax over the label set, run on a fine-tuned copy of the e5-small encoder. Runs after stage 0 and before the model. Declines below its own threshold.", + ["internal/router/heads.go", "internal/router/onnxruntime.go", CFG], + ["RouterHeads", "NewRouterHeads", "RouterHeads.Route"], + called_by=["router.cascade"], + notes="Configured at voice.embedder.heads_path. A missing or broken weights file logs and leaves the field nil, which is the pre-V-664 cascade exactly. heads_path must never equal model_path; that is refused at config load.") + +c("router.llm", "model", "mavend", + "The LLM router. A grammar-constrained call to the resident model that names an intent and a destination. Any error or parse failure falls through to the classifier.", + ["internal/router/llmrouter.go", "internal/router/currentversion.go", "cmd/mavend/voicewire.go"], + ["LLMRouter", "LLMRouter.Route", "Completer", "pickLLMRouter"], + calls=["svc.llama_server"], called_by=["router.cascade"], + notes="voice.llm_router = true in the deployed config. gateLLMDecision thins a structurally incomplete route rather than trusting it.") + +c("router.classifier", "model", "mavend", + "The nearest-centroid classifier over the embedder. The floor: it answers when the resident model is off, absent or erroring, and it names no destination.", + ["internal/router/classifier.go", "internal/router/embedder.go", "cmd/mavend/voicewire.go", + "cmd/mavend/seed.go", "models/seeds/"], + ["Classifier", "Classifier.Classify", "seedClassifier", "loadSeedFile", "ErrNoIntents", "seedDir"], + called_by=["router.cascade"], + notes="Seeded from models/seeds at boot. It sets no Source, so a box whose model is down walks the whole query chain in table order.") + +c("router.embedder", "model", "mavend", + "The ONNX multilingual-e5-small encoder, or the HashEmbedder floor when no embedder block is configured. Backs the classifier, recall, topics and the trace encoder id.", + ["internal/router/onnxembedder.go", "internal/router/embedder.go", CFG], + ["NewONNXEmbedder", "NewHashEmbedder", "EmbedQuery", "EmbedPassage", "EmbedderID", "tokenizerRev"], + called_by=["router.classifier", "core.recall", "core.feed_worker", "core.crawl_worker", "core.topics"], + notes="EmbedQuery and EmbedPassage apply the query:/passage: prefixes the model was trained with. Calling plain Embed on a note is a bug.") + +c("router.extractor", "service", "mavend", + "Stage 2 slot extraction: time, act target, fact key and value, ordinals, number words, URLs.", + ["internal/router/slots.go", "internal/router/dateparser.go", "internal/router/acttarget.go", + "internal/router/timementions.go", "internal/router/numwords.go", "internal/router/halfpast.go", + "internal/router/url.go", "internal/router/remindersubject.go"], + ["Extractor", "Extractor.Extract", "DateTimeParser", "NewPythonDateParser", "DefaultFactParser", + "ResolvedTheHour"], + called_by=["router.cascade", "core.preroute"]) + +c("router.claim", "planned", "mavend", + "A comparable unit of evidence for the many claimants that compete for one utterance: consumed span, unexplained remainder, band and veto reason.", + ["internal/claim/claim.go", "internal/router/claim.go", "docs/plans/19-dialogue-arbitration.md"], + ["claim.Claim", "claim.Split", "claim.Band", "router.ClaimOf", "bandOf", "vetoOf", "filledSlots"], + status="planned-unwired", confidence="high", + notes="Nothing calls ClaimOf. Its own doc comment says so. DEFECT: claimSpans includes Slots.Text unconditionally (internal/router/claim.go:38) while Router.fillSlots backfills the raw utterance into Text for note, query and chat (internal/router/router.go:334), so Claim.Coverage returns 1.0 for a claim that extracted nothing, and MoreSpecificThan reads coverage first. filledSlots in the same file guards against exactly this and claimSpans does not. All five cases in claim_test.go set Text == Utterance and assert Band only.") + +c("router.modes", "planned", "mavend", + "An inventory of roughly thirty downstream behaviours mapped back to the seven public intents.", + ["internal/modes/modes.go", "internal/modes/modes_v1.json"], ["modes.Inventory", "modes.Mode"], + status="planned-unwired", confidence="high", + notes="No file outside internal/modes imports it. Only its own test loads the JSON.") + +c("core.query_chain", "arbitration", "mavend", + "The ordered chain of 22 query sources actionQuery walks. First source to claim answers the turn; queryWalk narrows the chain against the destination the cascade named.", + ["cmd/mavend/actions_query.go", "cmd/mavend/querysource.go"], + ["querySources", "querySource", "queryWalk", "actionQuery", "queryTurn", "noteQuerySource", + "querySourceNames", "withQuerySourceSink"], + called_by=["core.action_table", "core.action_fact"], + notes="Table order IS the arbitration. queryWalk takes sources OUT and never moves one. Only sources marked guesses:true are droppable, and the personal boundary only when SourceAnchored was set by a literal stage 0 pattern (V-666).") + +# ---------------------------------------------------------------- query sources +qs = [ + ("core.q.factbykey", "fact-by-key", "Reads the current value for a key straight out of the fact store.", + "cmd/mavend/actions_query.go", "queryFactByKey", "SourceRecall", False, False), + ("core.q.dayplan", "day-plan", "Assembles today's plan from the tick loop, not from a table.", + "cmd/mavend/actions_query.go", "queryDayPlan", "SourceCalendar", False, False), + ("core.q.habits", "habits", "Answers a habit question from the behaviour profile over recorded events.", + "cmd/mavend/actions_query.go", "queryHabits", "SourceCalendar", False, False), + ("core.q.tasks", "tasks", "Answers from the task list.", + "cmd/mavend/actions_task.go", "queryTasks", "SourceTasks", False, False), + ("core.q.attention", "attention", "Answers from what Praxis says needs looking at.", + "cmd/mavend/attentionq.go", "queryAttention", "SourceAttention", True, False), + ("core.q.list", "list", "Answers from the shopping and other named lists.", + "cmd/mavend/actions_list.go", "queryList", "SourceList", True, False), + ("core.q.money", "money", "Answers from the spending facts the poller wrote.", + "cmd/mavend/actions_money.go", "queryMoney", "SourceMoney", False, False), + ("core.q.history", "history", "Answers 'what did I tell you' from the facts he tapped in.", + "cmd/mavend/historyq.go", "queryHistory", "SourceRecall", False, False), + ("core.q.feeds", "feeds", "Answers from the RSS notes the feed worker wrote.", + "cmd/mavend/actions_query.go", "queryFeeds", "SourceFeeds", True, False), + ("core.q.home", "home", "Answers from Home Assistant state.", + "cmd/mavend/actions_query.go", "queryHome", "SourceHome", True, False), + ("core.q.network", "network", "Answers from a bounded LAN scan.", + "cmd/mavend/actions_query.go", "queryNetwork", "SourceNetwork", True, False), + ("core.q.calendar", "calendar", "Answers from calendar events. The only date-aware source, so the only one a continuation may reach.", + "cmd/mavend/actions_query.go", "queryCalendar", "SourceCalendar", False, False), + ("core.q.weather", "weather", "Answers the forecast for a place.", + "cmd/mavend/actions_query.go", "queryWeather", "SourceWeather", True, False), + ("core.q.self", "self", "Answers a question about Maven herself, above the sources that search his data.", + "cmd/mavend/self.go", "querySelf", "SourceSelf", True, False), + ("core.q.embed", "embed", "Embeds the query once and fills the turn scratch for the passes below it.", + "cmd/mavend/actions_query.go", "queryEmbed", "SourceRecall", False, False), + ("core.q.memory", "memory", "Vector search over notes and facts together.", + "cmd/mavend/actions_query.go", "queryMemory", "SourceRecall", False, False), + ("core.q.notes", "notes", "The notes-only recall pass.", + "cmd/mavend/actions_query.go", "queryNotes", "SourceRecall", False, False), + ("core.q.personal", "personal", "THE BOUNDARY. A question about him that got this far has no answer in his data, so the walk stops rather than letting the world guess.", + "cmd/mavend/personalboundary.go", "queryPersonal", "SourceRecall", True, True), + ("core.q.search", "search", "SearXNG, the live web. Only the query string leaves the box.", + "cmd/mavend/actions_query.go", "querySearch", "SourceWorld", False, False), + ("core.q.kiwix", "kiwix", "The offline ZIMs, the fallback behind the live search.", + "cmd/mavend/actions_query.go", "queryKiwix", "SourceWorld", False, False), + ("core.q.web", "web", "Reads a page he named by URL. Claims only when a URL was spoken.", + "cmd/mavend/actions_query.go", "queryWeb", "SourceWorld", False, False), + ("core.q.general", "general-knowledge", "The resident model answering from its own weights. Last.", + "cmd/mavend/actions_query.go", "queryGeneral", "SourceWorld", False, False), +] +for qid, name, resp, f, sym, dest, guesses, boundary in qs: + tags = [] + if guesses: + tags.append("guesses:true — droppable when the cascade names another destination") + if boundary: + tags.append("boundary:true — droppable only by an anchored stage 0 destination (V-666)") + c(qid, "query_source", "query-chain", + resp, [f, "cmd/mavend/actions_query.go"], [sym, "querySources"], + called_by=["core.query_chain"], confidence="high", + notes=("chain name %r, dest %s. " % (name, dest)) + " ".join(tags)) + +# ---------------------------------------------------------------- core services +c("core.recall", "service", "mavend", + "The note-and-fact recall subsystem: the embedder, the vector store, and the two numbers that gate an answer.", + ["cmd/mavend/recall.go", "internal/memory/store.go", "internal/memory/gate.go", CFG], + ["recallWiring", "memory.Store", "memory.Search", "memory.Confident", "memory.ConfidentScores", + "query_min_score", "query_min_margin"], + reads=["state.memory_vectors"], writes=["state.memory_vectors"], + called_by=["core.q.memory", "core.q.notes", "core.q.embed", "core.action_fact", "core.action_note"], + notes="query_min_score 0.80, query_min_margin 0.008 in the deployed config.") + +c("core.phraser", "service", "mavend", + "The phrasing seam. One parser for every model reply; a deterministic Stub when no phraser block is configured.", + ["internal/phraser/phraser.go", "internal/phraser/llmphraser.go", "internal/phraser/parse.go", + "internal/phraser/prompts.go", "internal/phraser/nudge_llm.go", "internal/phraser/query.go", + "internal/phraser/world.go", "internal/phraser/replier.go", "internal/phraser/swap.go"], + ["phraser.Phraser", "phraser.NewStub", "phraser.NewLLMPhraser", "parseResponseMood", + "PhraseNudge", "PhraseReminder", "PhraseChat", "LLMPhraser.UseRemote"], + calls=["svc.llama_server", "core.model_seam"], + called_by=["core.action_chat", "core.tick_loop", "core.query_chain", "core.mail_intake", "core.memory_eval"], + notes="parseResponseMood is the one parser for model text; every phrasing path reaches it.") + +c("core.replier", "service", "mavend", + "Phrases the reply across a router decision when the action handler returned nothing. LLM-backed when a completion seam exists, a deterministic stub otherwise.", + ["cmd/mavend/replier_llm.go", "internal/voice/replier.go"], + ["voice.Replier", "voice.NewStubReplier", "newLLMReplier"], + calls=["core.model_seam"], called_by=["core.reactive_handler"]) + +c("core.model_seam", "service", "mavend", + "The completion seam the hot paths use. Either an llm.Pair preferring the workstation model with a silent fallback to the resident model, or the resident client alone, or nothing.", + ["cmd/mavend/voicewire.go", "cmd/mavend/main.go", "internal/llm/remote.go", "internal/llm/client.go", + "internal/llm/gate.go", CFG], + ["modelSeam", "llm.Pair", "llm.NewPair", "Pair.Available", "Pair.Complete", "llm.Gate", + "llmClientFor", "cacheRAMMiB"], + calls=["svc.llama_server", "proc.mavgpud"], + called_by=["router.llm", "core.replier", "core.phraser"], status="configured-off", + notes='deploy/mavend.json sets workstation.model_disabled = true, so the model arm is dark and only the STT arm of the workstation block is live. Deleting the block restores pre-workstation behaviour.') + +c("core.stt_seam", "service", "mavend", + "Transcription. Remote to mavsttd when a socket is configured, an in-process Stub otherwise, with an optional stt.Pair preferring the workstation CW2 service.", + ["cmd/mavend/voicewire.go", "internal/stt/"], ["sttSeam", "stt.Transcriber", "stt.NewRemote", "stt.NewStub", "stt.Pair"], + calls=["proc.mavsttd", "ext.cw2_stt"], called_by=["core.reactive_handler"]) + +c("core.tts_seam", "service", "mavend", + "Synthesis. Remote to mavttsd when a socket is configured, an in-process Stub otherwise.", + ["cmd/mavend/voicewire.go", "internal/tts/", "internal/ttsnorm/"], + ["tts.Synthesizer", "tts.NewRemote", "tts.NewStub", "ttsnorm"], + calls=["proc.mavttsd"], called_by=["core.reactive_handler", "core.sink_voice"]) + +c("core.ecosystem", "adapter", "mavend", + "The three ecosystem clients and the shared JSON transport. All HTTP; no component reads another's database.", + ["cmd/mavend/ecosystem.go", "cmd/mavend/ecosystem_acts.go"], + ["wireEcosystem", "ecosystemWiring", "nexusClient", "praxisClient", "hexisclient", + "resolveEntityReference", "newCorrelationID"], + calls=["ext.nexus", "ext.praxis", "ext.hexis"], writes=["state.ecosystem_traces"], + called_by=["core.reactive_handler", "core.fact_enrichment", "core.daemon_api"], + notes="All three are nil unless configured and each degrades alone. An outage means a named gap, never a guess.") + +c("core.decision_trace", "service", "mavend", + "The per-turn arbitration record: who claimed the turn, who lost it, who was never asked. Written to a bounded in-memory ring and, since V-629, persisted for fitting the routing heads.", + ["internal/decision/", "cmd/mavend/decisiontrace.go", "cmd/mavend/routingtrace.go"], + ["decision.Record", "decision.Ring", "decision.With", "decision.Note", "decision.Expect", + "decision.Claim", "preRouteLadder", "traceSink", "pruneTracesOnStart", "persistDecision"], + writes=["state.routing_traces", "state.decision_ring"], called_by=["core.reactive_handler", "router.cascade"], + notes="Retained 14 days, enforced on write and again on start.") + +c("core.topics", "service", "mavend", + "Open-set Russian topic matching with the embedder, one of the three sanctioned word-matching mechanisms.", + ["cmd/mavend/topics.go", "cmd/mavend/ruwords.go", "internal/lexicon/", "internal/morph/"], + ["topics", "lexicon", "morph"], called_by=["core.query_chain", "core.preroute"], + notes="Russian words are matched by internal/lexicon (closed classes), internal/morph (grammar) and topics.go with the embedder (open sets), and by no fourth mechanism.") + +# ---------------------------------------------------------------- proactive +c("core.tick_loop", "worker", "mavend", + "The proactive driver. One pass per tick: gather state, pick at most one nudge candidate, queue or phrase and dispatch it, flush the digest, fire operator routines, accepted routines and morning routines, detect patterns, deliver due reminders, and repeat un-acked sev4 telegram alarms.", + ["cmd/mavend/tick.go", "cmd/mavend/tick_digest.go", "cmd/mavend/tick_routines.go", + "cmd/mavend/tick_morning.go", "cmd/mavend/tick_api.go"], + ["tickLoop", "newTickLoop", "tickLoop.run", "tickLoop.tick", "deliverReminder", "savePresence", + "maybeFlush", "flushDigest", "enqueueSuppressedDigest", "fireRoutines", "fireAcceptedRoutines", + "fireMorningRoutines", "detectPatterns", "stopFinishedAlarms", "tune", "cachePhrase"], + reads=["state.facts", "state.reminders", "state.nudges", "state.digest_entries", "state.proposed_routines", + "state.events", "state.presence_state"], + writes=["state.nudges", "state.reminders", "state.presence_state", "state.digest_entries", + "state.facts", "state.proposed_routines", "state.delivery_attempts"], + calls=["core.gatherer", "core.rules", "core.phraser", "core.dispatcher", "core.pattern"], + called_by=["proc.mavend"], + notes="Thirteen distinct jobs in one tick function on one 60s ticker, plus a second slower autotune ticker.") + +c("core.gatherer", "service", "mavend", + "Builds the tick's consistent state snapshot out of the store: facts, presence, quiet hours, calendar busy, due reminders, collapsed reminder groups.", + ["internal/loop/gather.go", "internal/loop/state.go"], + ["loop.Gatherer", "NewGatherer", "GatherState", "SetQuietHours", "loop.State", "State.Since", + "State.FactsUnder", "inQuietWindow", "collapseReminders"], + reads=["state.facts", "state.reminders", "state.nudges", "state.presence_state"], + called_by=["core.tick_loop"]) + +c("core.rules", "service", "mavend", + "The pure nudge rule set and the restraint gate. Rules are data; Gate and ExplainGate are pure functions of state.", + ["internal/loop/rules.go", "internal/loop/loop.go", "internal/loop/explain.go", "internal/loop/feedback.go", + "internal/loop/digest_identity.go"], + ["loop.Rule", "DefaultRules", "RulesExcept", "WaterRule", "MealRule", "BreakRule", "ServiceDownRule", + "NetdataCriticalRule", "loop.Gate", "ExplainGate", "ExplainTick", "Tick", "Severity", + "RemindDecisions", "TuneCooldown", "DigestEligible"], + called_by=["core.tick_loop", "core.gatherer"], + notes="disabled_rules is empty in the deployed config, so all five rules are live.") + +c("core.pattern", "service", "mavend", + "Detects a stable recurring action+object pair in the events table and proposes a routine for it.", + ["internal/pattern/detector.go", "cmd/mavend/patterns.go", "cmd/mavend/tick_routines.go", CFG], + ["pattern.Detect", "pattern.ProposedRoutine", "detectPatterns", "announceProposal"], + reads=["state.events"], writes=["state.proposed_routines"], called_by=["core.tick_loop"], + notes="pattern_proposals.notify = false in the deployed config: detect silently, never announce.") + +c("core.morning", "service", "mavend", + "Daily checklists. An item is done when its fact_key gets a non-voided fact inside the window; the nudge fires once at the end of the window and only for what is still open.", + ["internal/morning/", "cmd/mavend/tick_morning.go", CFG], + ["morning.Routine", "morning.Candidate", "fireMorningRoutines", "gatherMorningFacts", + "morningStatus", "dayPlan"], + reads=["state.facts"], called_by=["core.tick_loop"], + notes="One routine configured: medicine, water, pets, 08:00-11:00, nudge at 10:30.") + +c("core.routines", "service", "mavend", + "Operator-declared and user-accepted scheduled behaviours, fired through the normal delivery routing. Bodies are literal operator text, never model-phrased.", + ["internal/routine/", "cmd/mavend/tick_routines.go"], + ["routine.Routine", "routine.Due", "routinesFromConfig", "fireRoutines", "fireAcceptedRoutines"], + reads=["state.proposed_routines"], called_by=["core.tick_loop"], + notes="The deployed config declares no `routines` block, so only accepted proposals can fire.") + +c("core.dispatcher", "service", "mavend", + "Delivery fan-out. A pure routing table over (severity, presence) picks channels; the dispatcher holds the sinks, the durable outbox and the nudge recorder.", + ["internal/delivery/dispatcher.go", "internal/delivery/channel.go", "internal/delivery/sink.go", + "internal/delivery/ack.go", "internal/delivery/sendable.go", "cmd/mavend/main.go"], + ["delivery.NewDispatcher", "delivery.Config", "ChannelsFor", "Channel", "DispatchNudge", + "RepeatUnacked", "beginOutbox", "completeOutbox", "wireDispatcher", "NudgeRecorder", "Outbox", + "ErrVoiceNoSession", "ErrPermanent"], + writes=["state.nudges", "state.delivery_attempts", "state.ack_sends"], + calls=["core.sink_voice", "core.sink_ntfy", "core.sink_telegram"], called_by=["core.tick_loop"], + notes="Double authority is intentional: the gate decides whether a rule EMITS, delivery decides where it LANDS. Reminders bypass the restraint gate.") + +c("core.sink_voice", "adapter", "mavend", + "Pushes a spoken nudge to the most recently active voice session through the same conn that serves requests.", + ["internal/delivery/voicesink/", "cmd/mavend/voicewire.go"], + ["voicesink.New", "voicesink.Sink", "Sessions.PushToMostRecent"], + calls=["core.tts_seam", "core.voice_server"], called_by=["core.dispatcher"]) + +c("core.sink_ntfy", "adapter", "mavend", "The ntfy push reach.", + ["internal/delivery/ntfysink/"], ["ntfysink.New"], calls=["ext.ntfy"], + called_by=["core.dispatcher"], status="configured-off", + notes="nil in the deployed dispatcher because the config block says disabled.") + +c("core.sink_telegram", "adapter", "mavend", "The telegram away reach, and the sev4 repeat-til-ack channel.", + ["internal/delivery/telegramsink/telegramsink.go", "internal/delivery/telegramsink/botapi.go"], + ["telegramsink.New", "telegramsink.Sink"], calls=["ext.telegram"], called_by=["core.dispatcher"]) + +c("core.telegram_intake", "worker", "mavend", + "Long-polls telegram getUpdates from exactly one chat and runs each message as a text turn, offering the same correction gesture the web does.", + ["cmd/mavend/telegramintake.go", "internal/delivery/telegramsink/intake.go", + "internal/delivery/telegramsink/correction.go"], + ["wireTelegramIntake", "chatTurnFn", "telegramsink.NewPoller", "Poller.Run"], + calls=["core.daemon_api", "core.reactive_handler"], called_by=["proc.mavend"], + notes="Started with wg.Add plus a bare goroutine rather than through goWorker or backgroundWorkers, so it is absent from the assertable worker set.") + +# ---------------------------------------------------------------- background workers +c("core.fact_enrichment", "worker", "mavend", + "Resolves a fact's free-text Subject to a canonical Nexus entity id, with per-fact backoff.", + ["cmd/mavend/factenrichment.go"], ["factEnrichmentWorker", "newFactEnrichmentWorker", "tick", + "resolveOne", "enrichmentBackoff", "forgetDeparted"], + reads=["state.facts"], writes=["state.facts"], calls=["ext.nexus"], called_by=["proc.mavend"]) + +c("core.memory_eval", "worker", "mavend", + "Scores recall quality against a stored fixture using the phraser.", + ["cmd/mavend/memoryeval.go", "internal/memeval/", "internal/memory/recalleval/"], + ["memoryEvalWorker", "newMemoryEvalWorker"], calls=["core.phraser"], called_by=["proc.mavend"], + status="configured-off", notes="nil unless configured; the deployed config has no block for it.") + +c("core.feed_worker", "worker", "mavend", + "Polls RSS sources and writes each item as a note with source rss:, embedding it for recall.", + ["cmd/mavend/feeds.go", "internal/rss/poller.go", CFG], + ["feedWorker", "newFeedWorker", "rss.Poller"], + writes=["state.notes", "state.memory_vectors", "state.facts"], + calls=["router.embedder", "core.intake_api"], called_by=["proc.mavend"], + notes="Two sources configured: lwn and archlinux, 30m interval.") + +c("core.crawl_worker", "worker", "mavend", + "Watches configured pages, and holds the fetcher the on-demand queryWeb source uses.", + ["cmd/mavend/crawls.go", "internal/crawl/", "internal/webfetch/", CFG], + ["crawlWorker", "newCrawlWorker", "crawl.Crawler", "crawl.Watcher", "onDemandCrawler", + "factHashes", "crawlFetcher"], + writes=["state.notes", "state.facts"], called_by=["proc.mavend"], + notes="crawl.on_demand = true and no watches are configured, so the worker runs with nothing to watch while queryWeb is live.") + +c("core.mcp_worker", "worker", "mavend", + "Connects to configured MCP servers, proposes their tools into the act allowlist, and withdraws proposals for tools that vanish.", + ["cmd/mavend/mcp.go", "internal/mcp/", CFG], + ["mcpWiring", "wireMCP", "connect", "propose", "withdrawGone", "status", "caller"], + writes=["state.tools"], calls=["ext.vikunja_mcp"], called_by=["proc.mavend"], status="configured-off") + +c("core.home_worker", "worker", "mavend", + "Refreshes Home Assistant state and proposes one always-destructive tool row per controllable device.", + ["cmd/mavend/smarthome.go", "internal/smarthome/", CFG], + ["homeWiring", "wireSmartHome", "propose", "run", "caller"], + writes=["state.tools"], calls=["ext.homeassistant"], called_by=["proc.mavend"], status="configured-off") + +c("core.netscan", "service", "mavend", + "Bounded LAN scan. A read, so it has no allowlist row; what keeps it safe is that its range comes from config alone.", + ["cmd/mavend/netscan.go", "internal/netscan/", CFG], + ["netWiring", "wireNetScan", "scan", "scanSummary", "writeScanRecord", "isNetworkQuery"], + writes=["state.notes"], called_by=["core.q.network"], + notes="netscan.enabled = true in the deployed config, subnet 192.168.1.0/24.") + +c("core.vision", "service", "mavend", + "Describes one image and stores the blob. Dark without a media block; MethodDescribeImage then answers ErrUnknownMethod.", + ["cmd/mavend/vision.go", "internal/vision/", "internal/media/"], + ["wireVision", "Server.DescribeImageFn", "media.Store"], + writes=["state.notes", "state.media_blobs"], called_by=["core.ipc_server"], status="configured-off", + notes="No `media` block in the deployed config.") + +c("core.capture", "service", "mavend", + "The meeting recorder. Off unless a media block exists AND capture.enabled is true; all four capture methods then answer ErrUnknownMethod.", + ["cmd/mavend/capture.go", "internal/capture/"], ["wireCapture", "Server.CaptureStartFn"], + writes=["state.notes", "state.media_blobs"], called_by=["core.ipc_server"], status="configured-off", + notes="The load-bearing default: on an unconfigured box no wire path begins a recording.") + +c("core.speaker", "service", "mavend", + "Voice identification. Enrolment plumbing only until a speaker-embedding model exists on disk; off entirely without a speaker block.", + ["cmd/mavend/speaker.go", "internal/speaker/"], ["wireSpeaker"], + called_by=["core.ipc_server"], status="configured-off") + +c("core.mail_intake", "service", "mavend", + "Extracts task candidates from one fetched message using the resident model and writes them as candidates he reviews on /tasks.", + ["cmd/mavend/mail.go", "internal/email/"], ["wireMailIntake", "mailIntake.ingest", "Server.IngestMailFn"], + writes=["state.tasks"], calls=["core.phraser"], called_by=["core.ipc_server"], status="configured-off", + notes="Nil unless an email block is configured AND there is a llama-server. Reaches past the intake decorator to st.CaptureTask directly.") + +c("core.modelswap", "service", "mavend", + "Swaps the resident model on the fly and reports model status. Nil unless phraser.swap_models lists at least one model.", + ["cmd/mavend/modelswap.go", "internal/phraser/swap.go"], + ["wireModelSwap", "Server.SwapModelFn", "Server.ModelStatusFn"], + called_by=["core.ipc_server"], status="configured-off", + notes="No swap_models key in the deployed phraser block.") + +# ---------------------------------------------------------------- persistence +c("state.db", "storage", "persistence", + "The one SQLite database. Opened at SetMaxOpenConns(1), so every write is serialised at the database and the IPC server adds no locking of its own.", + ["internal/store/store.go", "internal/store/schema.sql", "internal/store/migrations.go"], + ["store.Store", "store.Open", "store.OpenEncrypted", "Store.Close", "migrations", "schemaSQL"], + called_by=["core.store_api", "core.tick_loop", "core.wiring", "core.reactive_handler"], + notes="Append-only discipline: a wrong fact is superseded, never overwritten. 20 migrations on top of schema.sql.") + +c("state.db_file", "storage", "persistence", + "The at-rest ciphertext. A fixed magic header, a random GCM nonce and AES-256-GCM over the whole sqlite file. Decrypted into tmpfs on Open and re-encrypted atomically on Close.", + ["internal/store/crypt.go", "docker-compose.yml", CFG], + ["OpenEncrypted", "cryptMagic", "encState.closeAndSeal", "atomicWrite"], + notes="/var/lib/maven/maven.db.enc on the dbdata volume. Wrong key or a tampered file fails closed; there is never a plaintext fallback.") + +c("state.db_tmpfs", "storage", "persistence", + "The decrypted working copy, in RAM. Lives and dies with the container.", + ["internal/store/crypt.go", CFG], ["db_tmpfs"], + notes="/dev/shm/maven-plain.db. A daemon killed rather than stopped loses everything since the last clean shutdown; that is what mavseal recovers.") + +c("state.wrapped_key", "storage", "persistence", + "The database key wrapped under a passkey PRF output (HKDF-SHA256 + AES-GCM). Its presence with no env key is what puts the daemon in locked mode.", + ["cmd/mavend/keyfile.go", "internal/webauthn/", "cmd/mavend/main.go"], + ["wrapKeyToFile", "webauthn.WrapKey", "webauthn.UnwrapKey", "BlobV1", "BlobV2"], + notes="A v1 blob is derived from the credential PUBLIC key, which mavweb also writes to its passkey file; unwrapping one logs a SECURITY line.") + +c("state.facts", "table", "persistence", + "Every observation: self, env and config. ts is valid-time. A correction points voids_id at the row it cancels.", + ["internal/store/facts.go", "internal/store/entityfacts.go", "internal/store/schema.sql"], + ["Store.WriteFact", "Store.LatestFact", "Store.RecentFacts", "Store.VoidLatestFact", "Store.CorrectValue", + "FactKind", "FactResolutionState"], + notes="Written by six unrelated components: the voice fact handler, the quiet toggle, mavpoll, mavcaldav, mavweb (/api/signal and /api/ambient), the feed and crawl watermark writers, and the fact enrichment worker.") + +c("state.reminders", "table", "persistence", + "User intent, with a fire time, a durable delivery group, a cached phrase, an attempt count and a bounded backoff.", + ["internal/store/reminders.go", "internal/store/migrations.go"], + ["Store.CreateReminder", "ListPendingReminders", "CompleteReminderDelivery", + "CompleteSuccessfulReminderAttempt", "BlockReminderDelivery", "store.Reminder"], + notes="Carries cron and next_fire_ts columns since migration #2. No spoken path writes them.") + +c("state.nudges", "table", "persistence", + "Every proactive send and its outcome. This table IS the restraint memory and the only input to the feedback tuner.", + ["internal/store/nudges.go", "internal/store/schema.sql"], + ["Store.RecordNudge", "Store.ResolveNudge", "Store.RecentOutcomes", "Store.UnackedTelegramRules", + "Store.SnoozedUntil"], + notes="Recorded AFTER a successful send, so a failed send does not pollute the feedback signal.") + +c("state.notes", "table", "persistence", + "Free text he captured, plus everything the feed, crawl, capture, vision and netscan paths write. Embedding is a little-endian float32 blob, scanned brute force.", + ["internal/store/notes.go", "internal/store/schema.sql"], ["Store.WriteNote", "Store.QueryNotes", "Store.RecentNotes"], + notes="Six writers: the note handler, the RSS poller, the crawl watcher, meeting capture, image description and the LAN scan record.") + +c("state.memory_vectors", "table", "persistence", + "The vector index behind recall, over notes and facts together. Backed by store.VectorMemory or the in-memory floor.", + ["internal/store/memory.go", "internal/store/factvectors.go", "internal/memory/store.go", + "cmd/mavend/actions_fact.go", "cmd/mavend/voicewire.go"], + ["Store.VectorMemory", "MemoryStore.Insert", "MemoryStore.Search", "MemoryStore.DeletePrefix", + "pruneFactVectors", "repairFactVectors", "checkStoredEmbedder", "runReembed"], + notes="Marked with the embedder id. Swapping the embedder or bumping tokenizerRev requires ReembedAll.") + +c("state.tools", "table", "persistence", + "The act allowlist. A `proposed` row drives nothing; the executor only runs `enabled` rows, and enabling is a human act on an authed surface, never the voice path.", + ["internal/store/tools.go", "internal/store/schema.sql", "cmd/mavend/voicewire.go", CFG], + ["Store.ProposeTool", "Store.EnableTool", "Store.LookupTool", "seedTools"], + notes="Three unrelated proposers write here: config seeding, MCP discovery and Home Assistant discovery. Twelve tools are seeded from the deployed config, five of them destructive.") + +c("state.presence_state", "table", "persistence", + "The one stateful bit of presence: the hysteresis bucket, rewritten each tick.", + ["internal/store/presence.go", "internal/store/presence_state.go", "internal/store/schema.sql"], + ["Store.SavePresenceState", "Store.LoadPresenceState", "Store.PresenceProbes", "Bucket"], + notes="A singleton row. Presence is otherwise a pure function.") + +c("state.events", "table", "persistence", "Recorded action+object events; the only input to pattern detection.", + ["internal/store/events.go", "internal/ipc/storeapi.go"], ["Store.CreateEvent", "Store.DistinctEventPairs", + "Store.EventsFor", "storeAPI.SeedEvent"], + notes="SeedEvent is reachable only when mavend runs with -allow-seed, and the flag makes seedStore nil rather than merely refusing.") + +c("state.proposed_routines", "table", "persistence", "Detected patterns awaiting acceptance, and accepted ones with their fire clock.", + ["internal/store/proposed_routines.go"], ["ListProposedRoutines", "AcceptProposedRoutine", "DismissProposedRoutine"]) + +c("state.digest_entries", "table", "persistence", "Candidates the restraint gate BLOCKED, held durably for later resurfacing.", + ["internal/store/digest.go", "cmd/mavend/tick_digest.go"], ["enqueueSuppressedDigest", "maybeDrainDigest", "expireStaleDigest"], + notes="A separate mechanism from the in-memory digestQ, which batches candidates the gate ALLOWED.") + +c("state.delivery_attempts", "table", "persistence", + "The durable delivery ledger. Intent is recorded BEFORE the external send, so a crash leaves a pending row rather than silence.", + ["internal/store/delivery.go", "internal/delivery/dispatcher.go"], + ["BeginDeliveryAttempt", "CompleteDeliveryAttempt", "ReconcileStaleDeliveryAttempts"], + notes="Reconciled to `unknown` at boot before the tick loop resumes, so nothing auto-resends into the ambiguity.") + +c("state.ack_sends", "table", "persistence", "sev4 telegram repeat-til-ack tracking.", + ["internal/store/ack.go"], ["Store.WasAcked", "Store.MarkSent", "Store.MarkAcked", "Store.LastSent"]) + +c("state.dialogue_sessions", "table", "persistence", "The follow-up slot carry-over, persisted so a restart mid-conversation keeps the thread.", + ["internal/store/dialogue.go", "internal/dialogue/"], + ["dialogue.NewPersistentSessionStore", "SessionStore.Get", "SessionStore.Load"], + notes="TTL 2 minutes, pruned on load. The clarify store deliberately does NOT persist: a restart expires the open question.") + +c("state.tasks", "table", "persistence", "Task candidates and tasks, from the voice path, the web form and the mail reader.", + ["internal/store/tasks.go", "internal/tasks/"], ["Store.CaptureTask", "ListTasks", "SetTaskStatus", "EditTask"]) + +c("state.list_items", "table", "persistence", "The shopping and other named lists.", + ["internal/store/listitems.go"], ["Store.AddListItem", "ListItems"]) + +c("state.routing_traces", "table", "persistence", "Persisted turn decision records, for fitting the routing heads.", + ["internal/store/routingtraces.go", "cmd/mavend/routingtrace.go"], ["WriteRoutingTrace", "PruneRoutingTraces"], + notes="Retained 14 days, enforced on write and again on start.") + +c("state.routing_labels", "table", "persistence", "Owner corrections of a turn's route, the label side of the same data.", + ["internal/store/routinglabels.go"], ["Store.CorrectTurn", "Store.RoutingLabels"]) + +c("state.ecosystem_traces", "table", "persistence", "One row per ecosystem call, keyed by correlation id.", + ["internal/store/ecotraces.go"], ["Store.WriteEcosystemTrace", "Store.RecentEcosystemTraces", + "Store.PruneEcosystemTraces"]) + +c("state.meta", "table", "persistence", "Schema version and the embedder marker.", + ["internal/store/meta.go"], ["Store.Meta", "Store.SetMeta"]) + +c("state.passkey_file", "storage", "persistence", + "mavweb's WebAuthn credential store, a JSON file outside the database.", + ["cmd/mavweb/credentials.go", "internal/webauthn/"], ["passkeys.json", "-passkey-file"], + notes="Held by mavweb, not mavend. A v1 wrapped key blob plus this file together recover the database key with no authenticator.") + +c("state.maildata", "storage", "persistence", "mavmaild's seen-UID file, on its own volume so neither side can be restored from the other's backup.", + ["cmd/mavmaild/main.go", "docker-compose.yml"], ["-state"], status="built-not-deployed") + +c("state.media_blobs", "storage", "persistence", "The blob store shared by vision and the meeting recorder, with a retention loop.", + ["internal/media/", "cmd/mavend/vision.go", "cmd/mavend/capture.go"], ["media.Store", "media.Blob"], status="configured-off") + +# ---------------------------------------------------------------- shared mutable state +c("state.decision_ring", "shared-state", "in-memory", + "The last few turns' arbitration records. In memory and bounded, because none of his words belong in a table that outlives the diagnosis.", + ["internal/decision/ring.go"], ["decision.Ring", "Ring.Push", "Ring.Recent"], + called_by=["core.reactive_handler", "core.daemon_api"]) + +c("state.clarify_store", "shared-state", "in-memory", + "The parked request behind an open question, as a per-reach stack. Deliberately not persisted: a restart expires the question.", + ["internal/dialogue/clarify.go"], ["dialogue.ClarifyStore", "PendingQuestion", "Put", "Push", "Pop", + "CompleteTop", "TakeExpired", "Depth"], + called_by=["core.preroute"]) + +c("state.pending_act", "shared-state", "in-memory", + "Three single-slot confirmation registers on the handler, under one mutex: a destructive local act, a routine proposal and a mutating Hexis capability. Last-asked wins.", + ["cmd/mavend/voice.go", "cmd/mavend/confirm.go", "cmd/mavend/ecosystem_acts.go"], + ["reactiveHandler.pending", "pendingRoutine", "pendingHexis", "lastRouted", "confirmTTL"], + called_by=["core.preroute", "core.action_act"], + notes="Single-user box. A second act while one waits overwrites it; each expires after confirmTTL.") + +c("state.surfaced_items", "shared-state", "in-memory", + "The Praxis item ids she last read out, in order, so an ordinal has something to mean. Replaced by the next attention digest, with no TTL.", + ["cmd/mavend/voice.go", "cmd/mavend/ordinal.go", "cmd/mavend/attentionq.go"], + ["reactiveHandler.surfacedItems", "resolveCandidate"], called_by=["core.preroute", "core.q.attention"]) + +c("state.tick_memo", "shared-state", "in-memory", + "The tick loop's own mutable memory: the last phrase per rule, the last trace, the digest queue, per-routine last-fire maps and the proposal cooldown clock.", + ["cmd/mavend/tick.go"], + ["tickLoop.lastPhrase", "tickLoop.lastTrace", "tickLoop.digestQ", "routineLast", "morningLast", + "lastProposalAt"], + called_by=["core.tick_loop", "core.daemon_api"], + notes="lastProposalAt is in memory on purpose: a restart is allowed to permit one more announcement.") + +# ---------------------------------------------------------------- config +c("cfg.mavend", "config", "configuration", + "The single daemon config. Sets sockets, the database paths, the phraser, the embedder and heads, every reach, every world source, the ecosystem URLs, the tool allowlist and every capability's on/off state, with ${VAR} expansion from a gitignored env file.", + [CFG, "internal/config/", "deploy/telegram.env"], + ["config.Load", "config.Config", "DBEncryptionKey", "DefaultWrappedKeyPath", "DefaultRouterThreshold", + "MorningRoutinesFromConfig"], + called_by=["proc.mavend"], + notes="Presence of a block is the on-switch for most capabilities. Absence is silent and, for the calendar, load-bearing.") + +c("cfg.compose", "config", "configuration", + "The deployment shape. Five services run; mavcaldav and mavmaild are present as commented-out blocks with their reasoning.", + ["docker-compose.yml"], ["x-image", "services", "volumes", "networks"], + notes="Count services against compose, not against `make build`. mavwaked and mavgpud are systemd units on workpc.") + +c("cfg.systemd", "config", "configuration", + "The workstation units: the listening client, the GPU supervisor and the ssh tunnel that is the only path to mavend's voice port.", + ["deploy/mavwaked.service", "deploy/mavgpud.service", "deploy/maven-voice-tunnel.service", "deploy/asoundrc"], + ["ExecStart", "Requires=maven-voice-tunnel.service"]) + +# ---------------------------------------------------------------- eval +c("eval.router", "test", "evaluation", + "The routing fixture. Scores the real stage 0 set, the classifier and the resident model on a frozen Russian corpus.", + ["internal/router/eval/eval.go", "internal/router/eval/ru_routing_v1.json", + "internal/router/eval/ru_ecosystem_v1.json", "internal/router/eval/reach.go", "Makefile"], + ["eval", "make eval-router", "make eval-reach"], reads=["router.stage0"], + notes="Calls router.StageZeroGrammars directly, so a grammar change moves the measurement with it.") + +c("eval.phrasing", "test", "evaluation", + "The persona checks: address form, feminine self-reference and the cringe list.", + ["internal/phraser/eval/checks.go", "Makefile"], ["CheckAddress", "CheckFeminine", "CheckCringe", "make eval-phrasing"]) + +c("eval.gates", "test", "evaluation", + "The static gates. staticcheck, deadcode and govulncheck pass against a baseline rather than against zero, and fail on a baseline entry whose finding is gone.", + ["scripts/analyzers/deadcode.baseline", "scripts/analyzers/staticcheck.baseline", "Makefile"], + ["make analyze", "make lint", "make deadcode", "make vuln"], + notes="The deadcode baseline currently accepts 13 unreachable symbols.") + +# =================================================================== edges +def process_edges(): + e("proc.mavwaked", "bnd.voice_tcp", "tcp", "PushToTalk over ssh tunnel", evidence="deploy/mavwaked.service -addr 127.0.0.1:9100; Requires=maven-voice-tunnel.service") + e("proc.mavenclient", "bnd.voice_tcp", "tcp", "one wav per invocation", evidence="cmd/mavenclient/main.go") + e("proc.mavweb", "bnd.voice_tcp", "tcp", "POST /api/ptt", evidence="cmd/mavweb/main.go handlePTT(w, r, *voiceAddr, ...)") + e("bnd.voice_tcp", "core.voice_server", "in-process", "accept, register Session", evidence="internal/voice/server.go") + e("proc.mavweb", "bnd.ipc", "unix", "three ipc.Client connections", evidence="cmd/mavweb/main.go core/swapConn/turnConn") + e("proc.mavpoll", "bnd.ipc", "unix", "WriteFact", evidence="cmd/mavpoll/main.go -socket /run/maven/mavend.sock") + e("proc.mavcaldav", "bnd.ipc", "unix", "WriteFact", status="built-not-deployed", evidence="docker-compose.yml: commented out") + e("proc.mavmaild", "bnd.ipc", "unix", "IngestMail", status="built-not-deployed", evidence="docker-compose.yml: commented out") + e("proc.mavupdate", "bnd.ipc", "unix", "Ping only", evidence="cmd/mavupdate/main.go; there is no MethodApplyUpdate") + e("proc.e2eprobe", "bnd.ipc", "unix", "typed probe", status="temporary", evidence="cmd/e2eprobe/main.go") + e("bnd.ipc", "core.ipc_server", "in-process", "dispatch", evidence="internal/ipc/server.go serveConn") + e("core.stt_seam", "bnd.worker", "unix", "Transcribe", evidence="cmd/mavend/voicewire.go worker.Dial(cfg.Voice.Stt.Socket)") + e("core.tts_seam", "bnd.worker", "unix", "Synthesize", evidence="cmd/mavend/voicewire.go worker.Dial(cfg.Voice.Tts.Socket)") + e("bnd.worker", "proc.mavsttd", "unix", "/run/maven/stt.sock", evidence="docker-compose.yml mavsttd command") + e("bnd.worker", "proc.mavttsd", "unix", "/run/maven/tts.sock", evidence="docker-compose.yml mavttsd command") + e("core.stt_seam", "ext.cw2_stt", "http", "preferred transcriber, silent fallback", evidence="cmd/mavend/voicewire.go sttSeam; mavend.json workstation.stt") + e("core.model_seam", "proc.mavgpud", "http", "/health probe then completion", status="configured-off", evidence="mavend.json workstation.model_disabled = true") + e("proc.mavsttd", "ext.whispercpp", "in-process", "cgo", evidence="cmd/mavsttd/whisper_handler.go") + e("proc.mavttsd", "ext.piper", "subprocess", "piper argv", evidence="docker-compose.yml mavttsd -piper") + e("proc.mavwaked", "ext.alsa", "subprocess", "arecord / aplay", evidence="cmd/mavwaked/main.go") + e("proc.mavend", "svc.llama_server", "subprocess", "child llama-server", evidence="internal/phraser/server.go; mavend.json phraser.bin_path") + e("proc.mavgpud", "svc.llama_server", "subprocess", "supervised child on the workstation card", evidence="cmd/mavgpud/runner.go") + e("proc.mavpoll", "ext.netdata", "http", "alarms", evidence="docker-compose.yml -netdata http://127.0.0.1:19999") + e("proc.mavpoll", "ext.uptimekuma", "http", "/metrics", evidence="docker-compose.yml -kuma") + e("proc.mavpoll", "ext.zenmoney", "http", "spending", status="configured-off", evidence="docker-compose.yml: the token mount is commented out") + e("proc.mavweb", "ext.nexus", "http", "read-only /ecosystem panel", evidence="docker-compose.yml -nexus http://nexus:9740") + e("proc.mavweb", "ext.praxis", "http", "read-only /ecosystem panel", evidence="docker-compose.yml -praxis") + e("proc.mavweb", "ext.hexis", "http", "read-only /ecosystem panel", evidence="docker-compose.yml -hexis") + +def core_edges(): + e("core.ipc_server", "core.auth_gate", "calls", "Check before every dispatch", evidence="internal/ipc/server.go Server.Check") + e("core.ipc_server", "core.daemon_api", "calls", "CoreAPI dispatch", evidence="cmd/mavend/main.go ipc.Listen(cfg.SocketPath, coreAPI)") + e("core.ipc_server", "core.mail_intake", "calls", "IngestMailFn bypass", status="configured-off", evidence="cmd/mavend/main.go wireMailIntake") + e("core.ipc_server", "core.modelswap", "calls", "SwapModelFn / ModelStatusFn bypass", status="configured-off", evidence="cmd/mavend/main.go wireModelSwap") + e("core.ipc_server", "core.vision", "calls", "DescribeImageFn bypass", status="configured-off", evidence="cmd/mavend/main.go wireVision") + e("core.ipc_server", "core.capture", "calls", "four Capture* bypasses", status="configured-off", evidence="cmd/mavend/main.go wireCapture") + e("core.ipc_server", "core.speaker", "calls", "speaker enrolment bypass", status="configured-off", evidence="cmd/mavend/main.go wireSpeaker") + e("core.ipc_server", "core.daemon_lock", "calls", "UnlockFn / WrapKeyFn / StepUp", evidence="cmd/mavend/main.go srv.UnlockFn") + e("core.daemon_api", "core.store_api", "embeds", "embedded ipc.CoreAPI", evidence="cmd/mavend/tick_api.go type daemonAPI struct { ipc.CoreAPI ... }") + e("core.daemon_api", "core.intake_api", "wraps", "coreFor() = newIntakeAPI(ipc.NewStoreAPI(st), evBus, time.Now)", evidence="cmd/mavend/main.go coreFor") + e("core.intake_api", "core.event_bus", "publishes", "one envelope per intake write", evidence="cmd/mavend/intake.go") + e("core.daemon_api", "core.reactive_handler", "calls", "chatFn = handler.handleText", evidence="cmd/mavend/boot.go newDaemonAPI") + e("core.reactive_handler", "core.daemon_api", "calls", "handler.api back-patched by upgradeAPI", evidence="cmd/mavend/voice.go upgradeAPI; boot.go d.voiceW.handler.upgradeAPI(api)") + e("core.daemon_api", "core.tick_loop", "reads", "trace, morningStatus, dayPlan", evidence="cmd/mavend/boot.go getTrace / getDayPlan") + e("core.daemon_api", "ext.nexus", "http", "ResolveEntity", evidence="cmd/mavend/tick_api.go daemonAPI.ResolveEntity") + e("core.voice_server", "core.reactive_handler", "calls", "HandlePushToTalk", evidence="internal/voice/server.go Handler") + e("core.telegram_intake", "core.daemon_api", "calls", "api.Chat per inbound message", evidence="cmd/mavend/telegramintake.go chatTurnFn") + + e("core.reactive_handler", "core.stt_seam", "calls", "step 1 transcribe", evidence="cmd/mavend/voice.go HandlePushToTalk") + e("core.reactive_handler", "core.preroute", "calls", "steps 1-5e, eleven rungs", evidence="cmd/mavend/voice.go runTurn") + e("core.reactive_handler", "core.turn_route", "calls", "step 6 route", evidence="cmd/mavend/voice.go rt.resolve(ctx)") + e("core.reactive_handler", "core.action_table", "calls", "step 9 applyAction", evidence="cmd/mavend/actions.go actionHandlers") + e("core.reactive_handler", "core.replier", "calls", "step 10, only when the handler returned nothing", evidence="cmd/mavend/voice.go h.replier.Reply(ctx, dec)") + e("core.reactive_handler", "core.tts_seam", "calls", "step 6 synthesize", evidence="cmd/mavend/voice.go h.reply") + e("core.reactive_handler", "core.decision_trace", "writes", "one record per turn", evidence="cmd/mavend/voice.go decision.With(ctx, text)") + e("core.preroute", "core.turn_route", "reads", "the clarify resolver reads the routed decision before claiming", evidence="cmd/mavend/turnroute.go routeForRole") + e("core.preroute", "state.clarify_store", "reads", "parked question", evidence="cmd/mavend/clarify.go") + e("core.preroute", "state.pending_act", "reads", "confirm register", evidence="cmd/mavend/confirm.go") + e("core.preroute", "state.surfaced_items", "reads", "ordinal resolution", evidence="cmd/mavend/ordinal.go") + e("core.turn_route", "router.cascade", "calls", "Route", evidence="cmd/mavend/turnroute.go r.h.router.Route") + e("core.turn_route", "state.dialogue_sessions", "reads", "previous turn for a continuation", evidence="cmd/mavend/turnroute.go dialogueSessions.Get") + + e("router.cascade", "router.stage0", "calls", "stage 0, first match wins at 1.0", evidence="internal/router/router.go for i, g := range r.grammars") + e("router.cascade", "router.heads", "calls", "stage 0b, declines below threshold", evidence="internal/router/router.go if r.heads != nil") + e("router.cascade", "router.llm", "calls", "stage 1a, any error falls through", evidence="internal/router/router.go if r.llm != nil") + e("router.cascade", "router.classifier", "calls", "stage 1, the floor", evidence="internal/router/router.go r.classifier.Classify") + e("router.cascade", "router.extractor", "calls", "stage 2 slots", evidence="internal/router/router.go r.extractor.Extract / fillSlots") + e("router.llm", "svc.llama_server", "http", "grammar-constrained completion", evidence="internal/router/llmrouter.go") + e("router.classifier", "router.embedder", "calls", "nearest centroid", evidence="internal/router/classifier.go") + e("router.heads", "router.embedder", "reads", "a fine-tuned COPY of the same encoder", evidence="mavend.json voice.embedder.heads_path; heads_path must never equal model_path") + + e("core.action_table", "core.query_chain", "calls", "IntentQuery", evidence="cmd/mavend/actions.go actionHandlers[IntentQuery]") + e("core.action_table", "core.action_fact", "calls", "IntentFact", evidence="cmd/mavend/actions.go") + e("core.action_table", "core.action_reminder", "calls", "IntentReminder", evidence="cmd/mavend/actions.go") + e("core.action_table", "core.action_act", "calls", "IntentAct", evidence="cmd/mavend/actions.go") + e("core.action_table", "core.action_note", "calls", "IntentNote", evidence="cmd/mavend/actions.go") + e("core.action_table", "core.action_chat", "calls", "IntentChat", evidence="cmd/mavend/actions.go") + e("core.action_table", "core.action_system", "calls", "IntentSystem", evidence="cmd/mavend/actions.go") + e("core.action_fact", "core.query_chain", "calls", "a question-shaped fact is answered as a query", evidence="cmd/mavend/actions_fact.go return h.actionQuery(ctx, q)") + e("core.action_fact", "core.action_chat", "calls", "a complaint is answered as chat", evidence="cmd/mavend/actions_fact.go return h.actionChat(ctx, c)") + e("core.action_fact", "state.facts", "writes", "WriteFact source tap:voice or tap:text", evidence="cmd/mavend/actions_fact.go") + e("core.action_fact", "state.memory_vectors", "writes", "prune then insert one vector per key", evidence="cmd/mavend/actions_fact.go pruneFactVectors") + e("core.action_reminder", "state.reminders", "writes", "CreateReminder", evidence="cmd/mavend/actions_reminder.go h.api.CreateReminder") + e("core.action_act", "state.tools", "reads", "enabled allowlist only", evidence="internal/tool/tool.go:164 t.Status != enabled") + e("core.action_act", "core.risk_policy", "calls", "PolicyFor(RiskOf(t)) before every local exec", evidence="internal/tool/tool.go:181") + e("core.action_act", "core.praxis_acts", "calls", "intercepted BEFORE the local executor", evidence="cmd/mavend/actions_act.go:44 handlePraxisAct") + e("core.praxis_acts", "ext.praxis", "http", "acknowledge / resolve / ignore / pin, no tier and no confirm", evidence="cmd/mavend/ecosystem_acts.go:158 a.call(ctx, px, id)") + e("core.ecosystem_hexis_gate", "core.risk_policy", "calls", "RiskOfCapability then PolicyFor, the same policy reused", evidence="cmd/mavend/ecosystem_acts.go:768") + e("core.risk_policy", "core.action_act", "gates", "ErrNeedsAuthedSurface / ErrNeedsConfirm", evidence="internal/tool/tool.go:182-187") + e("core.action_act", "ext.hexis", "http", "confirmed mutating capability", evidence="cmd/mavend/ecosystem_acts.go handleHexisAct") + e("core.action_note", "state.notes", "writes", "WriteNote", evidence="cmd/mavend/actions_note.go") + e("core.action_chat", "core.phraser", "calls", "PhraseChat with dialogue history", evidence="cmd/mavend/actions.go") + + for qid, name, resp, f, sym, dest, guesses, boundary in qs: + e("core.query_chain", qid, "calls", + "chain position: %s" % name, + status="implemented", evidence="cmd/mavend/actions_query.go querySources") + e("core.q.search", "ext.searxng", "http", "query string only", evidence="cmd/mavend/actions_query.go querySearch") + e("core.q.kiwix", "ext.kiwix", "http", "ZIM search", evidence="cmd/mavend/actions_query.go queryKiwix") + e("core.q.web", "core.crawl_worker", "calls", "on-demand page fetch", evidence="cmd/mavend/crawls.go onDemandCrawler") + e("core.q.general", "core.phraser", "calls", "the model answers from its own weights, last", evidence="cmd/mavend/actions_query.go queryGeneral") + e("core.q.attention", "ext.praxis", "http", "ListAttention", evidence="cmd/mavend/attentionq.go") + e("core.q.attention", "state.surfaced_items", "writes", "the read-out order, for the next ordinal", evidence="cmd/mavend/attentionq.go") + e("core.q.home", "ext.homeassistant", "http", "device state", status="configured-off", evidence="mavend.json smarthome.enabled = false") + e("core.q.network", "core.netscan", "calls", "bounded LAN scan", evidence="cmd/mavend/netscan.go") + e("core.q.weather", "ext.openmeteo", "http", "forecast", status="configured-off", evidence="wireVoice picks NewStubProvider without a voice.weather block; the deployed config has none") + e("core.q.calendar", "state.facts", "reads", "facts(kind=env, source=caldav:*)", status="partially-wired", confidence="high", evidence="docker-compose.yml: mavcaldav is commented out, so nobody writes them") + e("core.q.memory", "state.memory_vectors", "reads", "vector search over notes and facts", evidence="cmd/mavend/actions_query.go queryMemory") + e("core.q.notes", "state.notes", "reads", "notes-only recall pass", evidence="cmd/mavend/actions_query.go queryNotes") + e("core.q.factbykey", "state.facts", "reads", "current value for a key", evidence="cmd/mavend/actions_query.go queryFactByKey") + e("core.q.money", "state.facts", "reads", "facts(kind=env, source=poll:zenmoney)", status="configured-off", evidence="compose does not mount the zenmoney token") + e("core.q.tasks", "state.tasks", "reads", "task list", evidence="cmd/mavend/actions_task.go") + e("core.q.list", "state.list_items", "reads", "named lists", evidence="cmd/mavend/actions_list.go") + e("core.q.feeds", "state.notes", "reads", "notes with source rss:*", evidence="cmd/mavend/actions_query.go queryFeeds") + e("core.q.dayplan", "core.tick_loop", "reads", "the plan is assembled by the tick loop, not read from a table", evidence="cmd/mavend/tick_morning.go dayPlan; voice.go upgradeAPI comment") + e("core.q.habits", "state.events", "reads", "behaviour profile over recorded events", evidence="internal/memory/behavior.go BuildProfile") + e("core.q.history", "state.facts", "reads", "what he tapped in", evidence="cmd/mavend/historyq.go") + +def tick_edges(): + e("core.tick_loop", "core.gatherer", "calls", "GatherState, aborts the tick on error", evidence="cmd/mavend/tick.go") + e("core.gatherer", "state.facts", "reads", "the tick snapshot", evidence="internal/loop/gather.go readFact") + e("core.gatherer", "state.reminders", "reads", "due and collapsed", evidence="internal/loop/gather.go collapseReminders") + e("core.gatherer", "state.nudges", "reads", "restraint memory", evidence="internal/loop/gather.go") + e("core.tick_loop", "core.rules", "calls", "ExplainTick, at most one candidate", evidence="cmd/mavend/tick.go loop.ExplainTick") + e("core.tick_loop", "core.phraser", "calls", "PhraseNudge / PhraseReminder", evidence="cmd/mavend/tick.go") + e("core.tick_loop", "core.dispatcher", "calls", "DispatchNudge, RepeatUnacked", evidence="cmd/mavend/tick.go") + e("core.tick_loop", "core.pattern", "calls", "detectPatterns every tick", evidence="cmd/mavend/tick_routines.go") + e("core.tick_loop", "core.morning", "calls", "fireMorningRoutines", evidence="cmd/mavend/tick_morning.go") + e("core.tick_loop", "core.routines", "calls", "fireRoutines and fireAcceptedRoutines", evidence="cmd/mavend/tick_routines.go") + e("core.tick_loop", "state.presence_state", "writes", "savePresence each tick", evidence="cmd/mavend/tick.go savePresence") + e("core.tick_loop", "state.digest_entries", "writes", "gate-suppressed candidates", evidence="cmd/mavend/tick_digest.go") + e("core.tick_loop", "state.tick_memo", "writes", "lastPhrase, lastTrace, digestQ", evidence="cmd/mavend/tick.go") + e("core.tick_loop", "state.facts", "writes", "the cooldown feedback fact from tune()", evidence="cmd/mavend/tick.go tune; internal/loop/feedback.go FeedbackKey") + e("core.dispatcher", "core.sink_voice", "calls", "present", evidence="internal/delivery/channel.go ChannelsFor") + e("core.dispatcher", "core.sink_ntfy", "calls", "away, sev3+", status="configured-off", evidence="wireNtfySink returns nil when disabled") + e("core.dispatcher", "core.sink_telegram", "calls", "away sev4, repeat til ack", evidence="internal/delivery/channel.go") + e("core.dispatcher", "state.delivery_attempts", "writes", "begin before send, complete after", evidence="internal/delivery/dispatcher.go beginOutbox") + e("core.dispatcher", "state.nudges", "writes", "recorded AFTER a successful send", evidence="internal/delivery/dispatcher.go NudgeRecorder") + e("core.sink_voice", "core.voice_server", "calls", "PushToMostRecent on the request conn", evidence="internal/delivery/voicesink/") + e("core.sink_telegram", "ext.telegram", "http", "sendMessage through the SOCKS relay", evidence="mavend.json telegram.proxy") + e("ext.telegram", "core.telegram_intake", "http", "getUpdates long poll", evidence="internal/delivery/telegramsink/intake.go") + +def worker_edges(): + e("proc.mavend", "core.tick_loop", "spawns", "named worker `tick`", evidence="cmd/mavend/boot.go backgroundWorkers") + e("proc.mavend", "core.fact_enrichment", "spawns", "named worker `fact-enrichment`", evidence="cmd/mavend/boot.go") + e("proc.mavend", "core.memory_eval", "spawns", "named worker `memory-eval`", status="configured-off", evidence="cmd/mavend/boot.go, nil unless configured") + e("proc.mavend", "core.feed_worker", "spawns", "named worker `feed`", evidence="cmd/mavend/boot.go") + e("proc.mavend", "core.crawl_worker", "spawns", "named worker `crawl`", evidence="cmd/mavend/boot.go") + e("proc.mavend", "core.mcp_worker", "spawns", "named worker `mcp`", status="configured-off", evidence="cmd/mavend/boot.go") + e("proc.mavend", "core.home_worker", "spawns", "named worker `home`", status="configured-off", evidence="cmd/mavend/boot.go") + e("proc.mavend", "core.voice_server", "spawns", "named worker `voice`", evidence="cmd/mavend/boot.go") + e("proc.mavend", "core.telegram_intake", "spawns", "bare goroutine, NOT in backgroundWorkers", confidence="high", + evidence="cmd/mavend/telegramintake.go wg.Add(1); go func(){...}()") + e("core.fact_enrichment", "state.facts", "writes", "entity_id and resolution_state", evidence="cmd/mavend/factenrichment.go resolveOne") + e("core.fact_enrichment", "ext.nexus", "http", "Resolve with backoff", evidence="cmd/mavend/factenrichment.go") + e("core.feed_worker", "state.notes", "writes", "one note per item, source rss:", evidence="internal/rss/poller.go") + e("core.feed_worker", "state.facts", "writes", "the feed watermark", evidence="cmd/mavend/feeds.go") + e("core.crawl_worker", "state.facts", "writes", "crawl:hash: watermark", evidence="cmd/mavend/crawls.go hashKey") + e("core.crawl_worker", "state.notes", "writes", "changed page text", evidence="internal/crawl/watch.go") + e("core.mcp_worker", "state.tools", "writes", "proposed rows", status="configured-off", evidence="cmd/mavend/mcp.go propose") + e("core.home_worker", "state.tools", "writes", "proposed destructive rows", status="configured-off", evidence="cmd/mavend/smarthome.go propose") + e("core.netscan", "state.notes", "writes", "one scan record", evidence="cmd/mavend/netscan.go writeScanRecord") + +def web_edges(): + e("proc.mavweb", "state.facts", "writes", "POST /api/signal presence, POST /api/ambient meeting time", evidence="cmd/mavweb/ambient.go, cmd/mavweb/facts.go") + e("proc.mavweb", "state.tools", "writes", "POST /tools enable/disable", evidence="cmd/mavweb/pages.go handleTools") + e("proc.mavweb", "state.proposed_routines", "writes", "POST /routines accept/dismiss", evidence="cmd/mavweb/pages.go handleRoutines") + e("proc.mavweb", "state.routing_labels", "writes", "POST /api/correct", evidence="cmd/mavweb/main.go handleCorrectAPI") + e("proc.mavweb", "state.facts", "writes", "POST /api/revert voids the latest fact for a key", evidence="cmd/mavweb/main.go handleRevert") + e("proc.mavweb", "state.passkey_file", "writes", "WebAuthn credential store", evidence="cmd/mavweb/credentials.go -passkey-file") + e("proc.mavweb", "state.wrapped_key", "writes", "StoreEncryptionKey after an assertion", evidence="cmd/mavend/main.go srv.WrapKeyFn, called by mavweb") + +def storage_edges(): + e("state.db", "state.db_file", "persists", "sealed on Close", evidence="internal/store/crypt.go") + e("state.db", "state.db_tmpfs", "persists", "decrypted working copy in RAM", evidence="internal/store/crypt.go") + e("proc.mavseal", "state.db_file", "writes", "recovery re-seal", evidence="cmd/mavseal/main.go") + for t in ["state.facts", "state.reminders", "state.nudges", "state.notes", "state.memory_vectors", + "state.tools", "state.presence_state", "state.events", "state.proposed_routines", + "state.digest_entries", "state.delivery_attempts", "state.ack_sends", + "state.dialogue_sessions", "state.tasks", "state.list_items", "state.routing_traces", + "state.routing_labels", "state.ecosystem_traces", "state.meta"]: + e("state.db", t, "contains", "table", evidence="internal/store/schema.sql and internal/store/migrations.go") + +def uncertain_edges(): + e("core.action_act", "ext.vikunja_mcp", "http", "an enabled MCP tool executes through tool.Executor.WithMCP", + confidence="medium", status="configured-off", + evidence="cmd/mavend/voicewire.go exec = exec.WithMCP(w.mcp.caller()); no MCP server is enabled, so no such row can exist today") + e("core.action_act", "ext.homeassistant", "http", "an enabled house tool executes through tool.Executor.WithHome", + confidence="medium", status="configured-off", + evidence="cmd/mavend/voicewire.go exec = exec.WithHome(w.home.caller()); smarthome.enabled = false") + e("core.ecosystem", "state.ecosystem_traces", "writes", "one row per ecosystem call", + confidence="medium", evidence="internal/store/ecotraces.go:34 Store.WriteEcosystemTrace; the call site was not read in full") + e("core.memory_eval", "state.notes", "reads", "the recall fixture", + confidence="low", status="configured-off", + evidence="internal/memeval/eval.go references WriteNote; the worker is nil on this deployment so the path was not traced") + e("proc.mavcaldav", "state.reminders", "reads", "the render side publishes pending reminders back as iCal", + confidence="medium", status="built-not-deployed", + evidence="cmd/mavcaldav/render.go; -render-url is off and checkRenderTarget refuses reading its own writes") + e("router.claim", "router.cascade", "calls", "the arbiter that would read claims", + confidence="high", status="planned-unwired", + evidence="internal/router/claim.go: 'Nothing in Route calls this yet.'") + +process_edges() +core_edges() +tick_edges() +worker_edges() +web_edges() +storage_edges() +uncertain_edges() + +doc = { + "schema": 1, + "generated": "2026-08-25", + "repo": "/mnt/server/home/kami/apps/Maven", + "commit": "5cae33a517ebeca55e2875512766c007b4e7457b", + "working_tree": "dirty: deploy/mavend.json modified (phraser.model_path swapped to maven-instruct-b2), docs/evals/CLAUDE.md modified, two untracked files", + "method": "Read from source, config, compose, systemd units, schema and migrations. No component is inferred from a directory name. Every entry cites the files and symbols it was read from.", + "legend": { + "type": ["process", "service", "worker", "handler", "arbitration", "query_source", "adapter", + "boundary", "storage", "table", "shared-state", "model", "external", "config", + "test", "planned"], + "status": { + "implemented": "on the running path of the deployed configuration", + "configured-off": "wired in code, dark because its config block is absent or disabled", + "built-not-deployed": "a complete binary or service that docker-compose.yml does not run", + "partially-wired": "the code path exists and one end of it has no producer or consumer", + "planned-unwired": "written and tested, called by nothing", + "temporary": "explicitly marked in its own source as disposable", + "dead": "unreachable, accepted in scripts/analyzers/deadcode.baseline" + }, + "confidence": { + "high": "read directly from code, config or schema", + "medium": "the wiring is in the source but the full call path was not traced end to end", + "low": "inferred from one reference; treat as uncertain" + } + }, + "components": C, + "edges": E, +} + +out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "maven-architecture.json") +with open(out, "w") as f: + json.dump(doc, f, indent=2, ensure_ascii=False) +print("components:", len(C), "edges:", len(E)) +ids = {x["id"] for x in C} +bad = [(x["from"], x["to"]) for x in E if x["from"] not in ids or x["to"] not in ids] +print("dangling edges:", bad) diff --git a/docs/architecture/build_viewer.py b/docs/architecture/build_viewer.py new file mode 100644 index 0000000..81511e1 --- /dev/null +++ b/docs/architecture/build_viewer.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Assemble index.html from the template, the inventory and the diagrams. + +index.html is self-contained on purpose: it opens from the filesystem with no +server, and a browser at file:// refuses to fetch a sibling JSON. So the +inventory, every .mmd source and every rendered .svg are inlined here rather +than loaded at runtime. + +Run it through docs/architecture/render.sh, which re-renders the SVGs first. +Running it alone rebuilds the viewer against whatever SVGs are already there. +""" +import json +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +DIA = os.path.join(HERE, "diagrams") + + +def main() -> None: + arch = json.load(open(os.path.join(HERE, "maven-architecture.json"))) + mermaid, svg = {}, {} + for name in sorted(os.listdir(DIA)): + path = os.path.join(DIA, name) + if name.endswith(".mmd"): + mermaid[name] = open(path).read() + elif name.endswith(".svg"): + svg[name] = open(path).read() + + payload = ( + "const ARCH = " + json.dumps(arch, ensure_ascii=False) + ";\n" + "const MERMAID = " + json.dumps(mermaid, ensure_ascii=False) + ";\n" + "const SVG = " + json.dumps(svg, ensure_ascii=False) + ";\n" + ) + template = open(os.path.join(HERE, "viewer.template.html")).read() + if "/*__DATA__*/" not in template: + raise SystemExit("viewer.template.html has no /*__DATA__*/ marker") + out = os.path.join(HERE, "index.html") + open(out, "w").write(template.replace("/*__DATA__*/", payload)) + print( + "index.html: %d bytes, %d components, %d relations, %d diagrams, %d rendered" + % (os.path.getsize(out), len(arch["components"]), len(arch["edges"]), + len(mermaid), len(svg)) + ) + + +if __name__ == "__main__": + main() diff --git a/docs/architecture/diagrams/01-system-topology.mmd b/docs/architecture/diagrams/01-system-topology.mmd new file mode 100644 index 0000000..b90f53d --- /dev/null +++ b/docs/architecture/diagrams/01-system-topology.mmd @@ -0,0 +1,120 @@ +%% View 1 — System topology. +%% Runtime processes and external systems, with process boundaries drawn explicitly. +%% mavend is the centre because the code makes it one: it is the only key holder, +%% it owns the store, the IPC socket, the voice listener, the tick loop, eight +%% in-process background workers and the child llama-server. +%% Evidence: docker-compose.yml, cmd/mavend/main.go, cmd/mavend/boot.go, +%% deploy/mavwaked.service, deploy/mavgpud.service. +flowchart LR + + subgraph WORKPC["workpc — systemd user units, never in docker-compose"] + direction TB + MAVWAKED["mavwaked
process
arecord · silero VAD · keyword head"] + MAVGPUD["mavgpud
process
GPU supervisor"] + LLAMA_W["llama-server
model · workstation card"] + CW2["CrisperWhisper2 turbo
model · port 8081"] + ALSA["arecord / aplay
external"] + TUNNEL["maven-voice-tunnel.service
ssh · the only path in"] + end + + subgraph HOMESRV["homesrv — docker compose project `maven`"] + direction TB + + subgraph MAVEND_P["mavend — process boundary · the only key holder"] + direction TB + IPCSRV["IPC server
unix /run/maven/mavend.sock"] + VOICESRV["voice server
TCP 0.0.0.0:9100"] + TURN["reactive handler
the turn pipeline"] + TICK["tick loop
60s"] + WORKERS["8 background workers
tick · fact-enrichment · feed · crawl
voice · mcp · home · memory-eval"] + STORE[("store
sqlite, MaxOpenConns=1")] + end + + LLAMA_H["llama-server
model · resident
child process of mavend"] + MAVSTTD["mavsttd
process
whisper.cpp"] + MAVTTSD["mavttsd
process
piper"] + MAVWEB["mavweb
process
HTTP 127.0.0.1:9201"] + MAVPOLL["mavpoll
process
network_mode: host"] + SEARX["SearXNG
external"] + KIWIX["kiwix-server
external"] + NETDATA["netdata
external"] + KUMA["uptime-kuma
external"] + end + + subgraph OFF["built, not deployed — commented out in docker-compose.yml"] + direction TB + MAVCALDAV["mavcaldav
process"] + MAVMAILD["mavmaild
process"] + end + + subgraph ECO["ecosystem network — external compose project"] + direction TB + NEXUS["Nexus
external · identity"] + PRAXIS["Praxis
external · attention"] + HEXIS["Hexis
external · capabilities"] + end + + subgraph NET["internet"] + direction TB + TG["Telegram Bot API
external · via SOCKS relay"] + NTFY["ntfy
external · DISABLED in config"] + ZM["zenmoney
external · no token mounted"] + end + + HA["Home Assistant
external · enabled:false"] + + %% ---- voice path + ALSA --- MAVWAKED + MAVWAKED -->|"PushToTalk · TCP"| TUNNEL + TUNNEL -->|"ssh to 127.0.0.1:9110"| VOICESRV + VOICESRV -->|"proactive Push on the same conn"| MAVWAKED + + %% ---- module IPC + MAVWEB -->|"3 × ipc.Client · unix"| IPCSRV + MAVWEB -->|"POST /api/ptt · TCP mavend:9100"| VOICESRV + MAVPOLL -->|"WriteFact · unix"| IPCSRV + MAVCALDAV -.->|"WriteFact · unix"| IPCSRV + MAVMAILD -.->|"IngestMail · unix"| IPCSRV + + %% ---- worker sockets + TURN -->|"worker · unix stt.sock"| MAVSTTD + TURN -->|"worker · unix tts.sock"| MAVTTSD + TURN -->|"HTTP · preferred, silent fallback"| CW2 + + %% ---- models + MAVEND_P ---|"spawns and owns"| LLAMA_H + MAVGPUD ---|"spawns and supervises"| LLAMA_W + MAVGPUD ---|"spawns and supervises"| CW2 + TURN -.->|"llm.Pair · model_disabled:true"| MAVGPUD + + %% ---- world and ecosystem + TURN -->|"HTTP · query string only"| SEARX + TURN -->|"HTTP"| KIWIX + TURN -->|"HTTP · v1 contract, correlation id"| NEXUS + TURN -->|"HTTP"| PRAXIS + TURN -->|"HTTP"| HEXIS + MAVWEB -->|"HTTP · read-only panel"| NEXUS + MAVWEB -->|"HTTP · read-only panel"| PRAXIS + MAVWEB -->|"HTTP · read-only panel"| HEXIS + TURN -.->|"HTTP · enabled:false"| HA + + %% ---- reaches + TICK -->|"telegram sink"| TG + TG -->|"getUpdates long poll"| TURN + TICK -.->|"ntfy sink · nil, disabled"| NTFY + + %% ---- pollers + MAVPOLL --> NETDATA + MAVPOLL --> KUMA + MAVPOLL -.-> ZM + + classDef proc fill:#1f3a5f,stroke:#7fb3ff,color:#eaf2ff + classDef ext fill:#3d2f4f,stroke:#c39bd3,color:#f4ecf7 + classDef model fill:#4a3a1f,stroke:#e0b050,color:#fff6e0 + classDef store fill:#1f4a3a,stroke:#6ed0a8,color:#e8fff5 + classDef off fill:#3a3a3a,stroke:#888,color:#ccc,stroke-dasharray:4 3 + class MAVWAKED,MAVGPUD,MAVSTTD,MAVTTSD,MAVWEB,MAVPOLL,IPCSRV,VOICESRV,TURN,TICK,WORKERS proc + class ALSA,SEARX,KIWIX,NETDATA,KUMA,NEXUS,PRAXIS,HEXIS,TG,TUNNEL ext + class LLAMA_H,LLAMA_W,CW2 model + class STORE store + class MAVCALDAV,MAVMAILD,NTFY,ZM,HA off diff --git a/docs/architecture/diagrams/02-core-internals.mmd b/docs/architecture/diagrams/02-core-internals.mmd new file mode 100644 index 0000000..47eb508 --- /dev/null +++ b/docs/architecture/diagrams/02-core-internals.mmd @@ -0,0 +1,160 @@ +%% View 2 — Core internals of mavend. +%% The real path, in the order runTurn actually runs it. The sequence is NOT +%% input → routing → intent → state → tools → response: eleven stateful +%% pre-emptors get first refusal BEFORE routing, and a query intent then enters +%% a second, longer arbitration of its own. +%% Evidence: cmd/mavend/voice.go runTurn, cmd/mavend/turnroute.go, +%% cmd/mavend/actions.go, cmd/mavend/actions_query.go, internal/router/router.go. +flowchart TB + + subgraph IN["input — three reaches, one pipeline"] + A1["voice.Server
HandlePushToTalk"] + A2["daemonAPI.Chat
mavweb /api/chat"] + A3["telegram poller
getUpdates"] + STT["stt seam
Remote mavsttd · CW2 · Stub"] + end + + A1 --> STT --> RT + A2 --> RT + A3 --> A2 + + RT["runTurn
cmd/mavend/voice.go"] + + RT --> D0["decision.With
one arbitration record per turn"] + D0 --> TR0["turnRoute created
sync.Once, on the context"] + + subgraph PRE["pre-route ladder — 11 rungs, order load-bearing"] + direction TB + P1["1 expired-clarify notice"] + P2["2 confirm answer
resolveConfirm"] + P3["3 targeted repair"] + P4["3b untargeted repair"] + P5["3c command prohibition"] + P6["4 clarify answer"] + P7["5 quiet toggle"] + P8["5b snooze"] + P9["5c ack"] + P10["5d reminder cancellation"] + P11["5e ordinal selection"] + P1-->P2-->P3-->P4-->P5-->P6-->P7-->P8-->P9-->P10-->P11 + end + TR0 --> PRE + PRE -->|"any rung claims"| OUT + + subgraph ROUTE["step 6 — the cascade · internal/router"] + direction TB + CONT["continuationDecision
an elliptical follow-up is answered
from the previous turn, not routed"] + S0["stage 0 grammars
StageZeroGrammars · first match wins at 1.0
the ONLY arm that may set SourceAnchored"] + SH["stage 0b routing heads
ONNX softmax over the label set"] + SL["stage 1a LLM router
resident model, grammar-constrained"] + SC["stage 1 classifier
nearest centroid · THE FLOOR
names no destination"] + SE["stage 2 extractor + stage 3 gate"] + CONT -->|"not a continuation"| S0 + S0 -->|"no match"| SH + SH -->|"declines"| SL + SL -->|"error or unparsable"| SC + SH --> SE + SL --> SE + SC --> SE + end + PRE -->|"nobody claimed"| ROUTE + + ROUTE --> DLG["step 7 dialogue merge
followUpMerge · rememberTurn"] + DLG --> CLAR{"step 8
dec.Clarify OR a required slot missing?"} + CLAR -->|"yes"| ASK["askClarify
park the request, ask one question"] + ASK --> OUT + CLAR -->|"no"| ACT + + subgraph ACT["step 9 — actionHandlers, 7 intents"] + direction TB + HF["fact
actions_fact.go"] + HR["reminder
actions_reminder.go"] + HA["act
actions_act.go"] + HN["note"] + HC["chat"] + HS["system"] + HQ["query → the chain"] + end + + subgraph QC["the query chain — a SECOND arbitration, 22 sources"] + direction TB + QW["queryWalk
removes only guesses:true sources
when the cascade named a destination"] + Q1["his data
fact-by-key · day-plan · habits · tasks
attention · list · money · history · feeds
home · network · calendar · weather · self
embed · memory · notes"] + QB["personal boundary
the only source a stage 0 anchor may drop"] + Q2["the world
search → kiwix → web → general-knowledge"] + QW --> Q1 --> QB --> Q2 + end + HQ --> QC + + HF -->|"question-shaped ⇒ re-route"| QC + HF -->|"complaint ⇒ re-route"| HC + + subgraph STATE["state and memory"] + direction TB + DB[("store · sqlite
facts · reminders · notes · tools
tasks · lists · nudges")] + VEC[("memory_vectors
brute-force cosine")] + DLGS[("dialogue_sessions
persisted, TTL 2m")] + CLS["clarifyStore
IN MEMORY ONLY, by design"] + PEND["pending act / routine / hexis
3 single-slot registers, one mutex"] + SURF["surfacedItems
last Praxis read-out order"] + RING["decision.Ring
bounded, in memory"] + end + + HF --> DB + HF --> VEC + HR --> DB + HN --> DB + HN --> VEC + Q1 --> DB + Q1 --> VEC + DLG --> DLGS + ASK --> CLS + HA --> PEND + QC --> SURF + D0 --> RING + + subgraph TOOLS["act execution"] + direction TB + ALLOW[("tools table
only status='enabled' runs")] + EXEC["tool.Executor
+ MCP + Home Assistant callers"] + CONF["destructive confirm turn
binds capability, entity, args, requester, expiry"] + HEX["Hexis capability
entity id resolved via Nexus first"] + end + HA --> ALLOW --> EXEC + HA --> CONF + HA --> HEX + + subgraph RESP["response generation"] + direction TB + REP["replier
only when the handler returned \"\""] + PHR["phraser
parseResponseMood is the one parser"] + TTS["tts seam
Remote mavttsd · Stub"] + end + ACT --> RESP + QC --> RESP + REP --> PHR + OUT["reply text
+ notice + resumed question"] + RESP --> OUT + OUT -->|"voice path only"| TTS + + subgraph PROACT["the other half of the process — nothing above touches it"] + direction TB + TICKL["tick loop · 60s
13 jobs in one function"] + GTH["loop.Gatherer
one consistent snapshot"] + RUL["loop rules + restraint gate
pure"] + DISP["delivery.Dispatcher
ChannelsFor severity,presence"] + SNK["sinks: voice · ntfy · telegram"] + TICKL --> GTH --> RUL --> TICKL + TICKL --> DISP --> SNK + end + TICKL --> DB + SNK -->|"PushToMostRecent on the request conn"| A1 + + classDef stage fill:#1f3a5f,stroke:#7fb3ff,color:#eaf2ff + classDef store fill:#1f4a3a,stroke:#6ed0a8,color:#e8fff5 + classDef mem fill:#4a3a1f,stroke:#e0b050,color:#fff6e0 + classDef danger fill:#4f2626,stroke:#e08080,color:#ffecec + class S0,SH,SL,SC,SE,CONT stage + class DB,VEC,DLGS,ALLOW store + class CLS,PEND,SURF,RING mem + class QB,CONF danger diff --git a/docs/architecture/diagrams/03a-flow-reminder.mmd b/docs/architecture/diagrams/03a-flow-reminder.mmd new file mode 100644 index 0000000..003be8b --- /dev/null +++ b/docs/architecture/diagrams/03a-flow-reminder.mmd @@ -0,0 +1,78 @@ +%% View 3a — Runtime flow: a reminder request. +%% Traced through cmd/mavend/voice.go runTurn, internal/router/stagezero.go, +%% cmd/mavend/clarify.go, cmd/mavend/actions_reminder.go, cmd/mavend/tick.go, +%% internal/loop/loop.go and internal/delivery/dispatcher.go. +%% Shows the branch where the hour is missing, the parked clarify, the answer +%% turn, the write and the eventual delivery with durable retry. +sequenceDiagram + autonumber + participant K as Owner + participant W as mavwaked + participant V as voice.Server + participant H as reactiveHandler.runTurn + participant PRE as pre-route ladder + participant R as router cascade + participant CL as clarifyStore + participant AR as actionReminder + participant DB as store + participant T as tick loop + participant D as dispatcher + + Note over K,W: "Мэйвен, напомни позвонить маме" + K->>W: speech + W->>W: silero VAD + keyword head, score ≥ 0.999 + W->>V: PushToTalkReq, one utterance + V->>H: HandlePushToTalk + H->>H: stt seam → text + H->>H: decision.With, turnRoute created + H->>PRE: 11 rungs + PRE-->>H: nobody claims + H->>R: rt.resolve + R->>R: stage 0 ReminderGrammar matches, Stage=0, conf 1.0 + R-->>H: IntentReminder, Slots.Text="позвонить маме", HasTime=false + + rect rgb(70,40,40) + Note over H,CL: BRANCH — missingFor names `time`, whatever the confidence + H->>H: dec.Clarify false BUT len missingFor > 0 → step 8 fires + H->>CL: Push a PendingQuestion, park the request + H-->>V: "во сколько напомнить?" + V-->>W: reply audio + text + end + + Note over K,W: "в семь вечера" + K->>W: speech + W->>V: PushToTalkReq + V->>H: runTurn + H->>PRE: rung 4, resolveClarifyAnswer + PRE->>CL: Pop the parked question + PRE->>R: extractor parses the hour with the SAME parsers stage 2 uses + PRE->>AR: finishClarified → applyAction + Note right of AR: filling in an argument never grants authority + AR->>AR: router.ResolvedTheHour guard + AR->>DB: CreateReminder fire_ts, payload + DB-->>AR: id + AR-->>H: reminderConfirm, phrased FROM THE ROW not the utterance + H-->>V: "хорошо, напомню сегодня в 19:00." + + Note over T,D: later — the proactive half, no shared code with the turn path + loop every 60s + T->>DB: Gatherer.GatherState, due reminders, collapsed by group + T->>T: loop.RemindDecisions — reminders BYPASS the restraint gate + alt not cached + T->>T: phraser.PhraseReminder + end + T->>D: DispatchReminder + D->>DB: BeginDeliveryAttempt BEFORE the external send + alt a voice session is live + D->>V: voicesink push on the request conn + else away + D->>D: ntfy is nil (disabled) → telegram + end + alt success + D->>DB: CompleteSuccessfulReminderAttempt + fire the originals, one txn + else failure + D->>DB: advance the persisted bounded backoff, next_attempt_ts + end + end + + Note over T,DB: Recurring is NOT on this path. reminders.cron and next_fire_ts
exist since migration #2 and no spoken path writes them. diff --git a/docs/architecture/diagrams/03b-flow-fact.mmd b/docs/architecture/diagrams/03b-flow-fact.mmd new file mode 100644 index 0000000..c1fe842 --- /dev/null +++ b/docs/architecture/diagrams/03b-flow-fact.mmd @@ -0,0 +1,61 @@ +%% View 3b — Runtime flow: a factual / state update. +%% Traced through cmd/mavend/actions_fact.go, cmd/mavend/ack.go, +%% cmd/mavend/patterns.go, cmd/mavend/factenrichment.go, cmd/mavend/intake.go +%% and internal/morning. +%% Shows the two re-route branches this handler owns, the vector prune-and-insert, +%% the nudge it can close, and the async entity resolution behind it. +sequenceDiagram + autonumber + participant K as Owner + participant H as runTurn + participant R as router cascade + participant AF as actionFact + participant API as CoreAPI · intakeAPI then storeAPI + participant DB as facts table + participant VEC as memory_vectors + participant BUS as event.Bus + participant FE as fact-enrichment worker + participant NX as Nexus + participant T as tick loop + + Note over K,H: "выпил воды" + K->>H: utterance, src=tap:voice + H->>R: rt.resolve + R->>R: stage 0 declines → heads → LLM router → classifier + R-->>H: IntentFact, Slots.Key="water", Slots.Value=... + + rect rgb(70,40,40) + Note over AF: two guards that RE-ROUTE rather than write + AF->>AF: router.IsQuestionShaped? → becomes actionQuery, Key cleared + AF->>AF: router.IsTransientComplaint? → becomes actionChat, nothing stored + end + + AF->>AF: factConfidence — 1.0 only for a value he actually said + AF->>API: WriteFact kind=self, source=tap:voice, Subject=Key + API->>DB: append-only row + API->>BUS: publish one intake envelope + API-->>AF: factID + + AF->>VEC: pruneFactVectors by key + AF->>VEC: EmbedPassage(FactRecallText) then Insert "fact::" + Note right of VEC: the FACT is embedded, not the utterance.
The utterance rides along as provenance only + + AF->>DB: RecordEvent action+object, for pattern detection + H->>H: step 9b ackFromFact — a fact answering a live nudge closes it as `acted`, silently + + par asynchronous, minutes later + FE->>DB: read facts with resolution_state='pending' + FE->>NX: Resolve(Subject) + alt resolved + NX-->>FE: entity_id + FE->>DB: UPDATE entity_id, resolution_state='resolved' + else ambiguous + Note right of FE: candidates are NOT stored —
ambiguity blocks, it does not pick + end + and the next tick + T->>DB: Gatherer reads the same row + T->>T: morning routine item `water` is now evidenced, so it will not nudge + T->>T: detectPatterns scans events for a stable interval + end + + Note over DB,VEC: A wrong value is superseded, never overwritten:
voids_id points at the row it cancels, and CorrectValue /
VoidLatestFact drop the key's vectors so recall keeps exactly one. diff --git a/docs/architecture/diagrams/03c-flow-world-query.mmd b/docs/architecture/diagrams/03c-flow-world-query.mmd new file mode 100644 index 0000000..5236223 --- /dev/null +++ b/docs/architecture/diagrams/03c-flow-world-query.mmd @@ -0,0 +1,70 @@ +%% View 3c — Runtime flow: a world query, tool-backed. +%% Traced through internal/router/worldquery.go, internal/router/source.go, +%% cmd/mavend/actions_query.go queryWalk + querySources, cmd/mavend/personalboundary.go, +%% cmd/mavend/searchwire.go, cmd/mavend/kiwixwire.go. +%% Shows destination anchoring, which sources are skipped and why, and the +%% four-step fallback to the model's own weights. +sequenceDiagram + autonumber + participant K as Owner + participant H as runTurn + participant R as router cascade + participant QC as actionQuery + participant W as queryWalk + participant LOC as local sources + participant PB as personal boundary + participant SX as SearXNG + participant KX as kiwix-server + participant PH as phraser / resident model + participant REC as decision record + + Note over K,H: "что такое TCP?" + K->>H: utterance + H->>R: rt.resolve + R->>R: stage 0 — WorldQueryGrammars matches a literal definition frame + R->>R: d.SourceAnchored = true, set HERE and nowhere else + R-->>H: IntentQuery, Source=SourceWorld, anchored + + H->>QC: applyAction → actionQuery + QC->>REC: Expect the full 22-source roster + QC->>W: queryWalk(SourceWorld, anchored=true) + + rect rgb(70,40,40) + Note over W: removes ONLY sources with guesses:true whose dest ≠ world + W-->>REC: skipped: attention, list, feeds, home, network, weather, self + W-->>REC: skipped: personal boundary — anchored, so a literal pattern may drop it + Note right of W: a model or a softmax naming SourceWorld
would NOT drop the boundary (V-666) + end + + W-->>QC: the sources that LOOK still walk, in table order + + loop first source to claim answers the turn + QC->>LOC: fact-by-key, day-plan, habits, tasks, money, history, calendar + LOC-->>QC: no rows → pass + QC->>LOC: embed → memory → notes (vector recall, gated by min score + margin) + LOC-->>QC: below the gate → pass + QC->>PB: personal boundary + PB-->>QC: SKIPPED this turn + QC->>SX: Search(utterance verbatim, max 4) + alt results + SX-->>QC: snippets + QC->>PH: phraseSource("search", utterance, evidence) + PH-->>QC: reply + QC->>REC: claimed by "search", and everyone below is NeverAsked + else empty or unreachable + QC->>KX: ZIM search, ru then en + alt hit + KX-->>QC: article snippet + QC->>PH: phraseSource("kiwix", ...) + else miss + QC->>QC: "web" claims only if he named a URL out loud + QC->>PH: queryGeneral — the model answers from its own weights, LAST + end + end + end + + QC-->>H: reply text + H-->>K: spoken or written answer + + Note over LOC,SX: What leaves the box is the query string and nothing else.
His notes, his facts, the persona block and the history never travel. + Note over W,PB: With no destination named — the classifier arm sets none —
the whole chain walks in table order. That is the floor. diff --git a/docs/architecture/diagrams/04-state-ownership.mmd b/docs/architecture/diagrams/04-state-ownership.mmd new file mode 100644 index 0000000..3084ca9 --- /dev/null +++ b/docs/architecture/diagrams/04-state-ownership.mmd @@ -0,0 +1,115 @@ +%% View 4 — State ownership. +%% Every persistent and shared store, its authoritative owner, its writers and +%% readers, its synchronisation boundary and its lifecycle. +%% Red = written by components that do not know about each other. +%% Evidence: internal/store/schema.sql, internal/store/migrations.go, +%% internal/store/crypt.go, cmd/mavend/voice.go, cmd/mavend/tick.go, +%% cmd/mavweb/*.go, cmd/mavpoll/main.go, cmd/mavcaldav/main.go. +flowchart LR + + subgraph OWNER["authoritative owner — mavend, the only key holder"] + STORE[("store.Store
SetMaxOpenConns(1)
every write serialised at the db")] + end + + subgraph LIFE["lifecycle of the database itself"] + direction TB + ENC[("maven.db.enc
AES-256-GCM at rest
volume dbdata")] + TMP[("/dev/shm/maven-plain.db
tmpfs working copy
dies with the container")] + ENC -->|"Open: decrypt"| TMP + TMP -->|"Close: checkpoint, re-encrypt, atomic rename"| ENC + SEAL["mavseal
recovery only, VACUUM INTO"] + TMP -.->|"when mavend was killed, not stopped"| SEAL + SEAL -.-> ENC + end + STORE --- TMP + + %% ------------- multiply written tables + FACTS[("facts
append-only, ts = valid-time
correction sets voids_id")]:::multi + NOTES[("notes
float32 blob, brute-force scan")]:::multi + TOOLS[("tools
only status='enabled' executes")]:::multi + + %% ------------- singly owned tables + REM[("reminders")] + NUD[("nudges — the restraint memory
AND the only feedback input")] + VEC[("memory_vectors
marked with the embedder id")] + PRES[("presence_state — singleton row")] + EV[("events")] + PROP[("proposed_routines")] + DIG[("digest_entries — gate-BLOCKED candidates")] + DEL[("delivery_attempts — the outbox")] + ACK[("ack_sends")] + DLGS[("dialogue_sessions — TTL 2m")] + TASKS[("tasks")] + LISTS[("list_items")] + RTR[("routing_traces — 14-day retention")] + RLB[("routing_labels")] + ETR[("ecosystem_traces")] + META[("meta — schema version + embedder marker")] + + STORE --- FACTS & NOTES & TOOLS & REM & NUD & VEC & PRES & EV & PROP & DIG & DEL & ACK & DLGS & TASKS & LISTS & RTR & RLB & ETR & META + + %% ------------- writers into facts + WF1["actionFact — tap:voice / tap:text"] --> FACTS + WF2["quiet toggle — config fact"] --> FACTS + WF3["mavpoll — poll:netdata, poll:uptimekuma,
infer:wg, poll:zenmoney"] --> FACTS + WF4["mavcaldav — poll:caldav
NOT DEPLOYED"]:::off -.-> FACTS + WF5["mavweb — /api/signal presence,
/api/ambient meeting time"] --> FACTS + WF6["feed + crawl watermarks
crawl:hash:*"] --> FACTS + WF7["fact-enrichment worker
entity_id, resolution_state"] --> FACTS + WF8["tick loop tune()
cooldown: feedback fact"] --> FACTS + WF9["mavweb /api/revert
voids the latest fact for a key"] --> FACTS + + %% ------------- writers into notes + WN1["actionNote"] --> NOTES + WN2["RSS poller — source rss:*"] --> NOTES + WN3["crawl watcher"] --> NOTES + WN4["meeting capture"]:::off -.-> NOTES + WN5["image description"]:::off -.-> NOTES + WN6["netscan record"] --> NOTES + + %% ------------- writers into tools + WT1["seedTools from mavend.json"] --> TOOLS + WT2["MCP discovery — proposed"]:::off -.-> TOOLS + WT3["Home Assistant discovery
proposed, always destructive"]:::off -.-> TOOLS + WT4["mavweb POST /tools
the ONLY enable path"] --> TOOLS + + %% ------------- readers + FACTS --> RD1["loop.Gatherer — the tick snapshot"] + FACTS --> RD2["queryFactByKey · money · history · morning"] + NOTES --> RD3["queryNotes · queryFeeds · recall"] + VEC --> RD4["queryMemory · queryEmbed"] + TOOLS --> RD5["tool.Matcher + tool.Executor"] + NUD --> RD6["restraint gate · TuneCooldown · UnackedTelegramRules"] + + %% ------------- in-memory shared state + subgraph MEM["shared mutable state — process-local, no synchronisation boundary beyond a mutex"] + direction TB + CLS["clarifyStore
per-reach stack · NOT persisted on purpose:
a restart expires the open question"] + PEND["pending act / pendingRoutine / pendingHexis
3 single-slot registers under handler.mu
last-asked wins, TTL each"] + SURF["surfacedItems
replaced by the next digest, NO TTL"] + RING["decision.Ring — bounded, diagnosis only"] + BUS["event.Bus — bounded journal, read surface only"] + TICKM["tickLoop: lastPhrase, lastTrace, digestQ,
routineLast, morningLast, lastProposalAt"] + LASTR["lastRouted — the previous acted turn, for a spoken correction"] + end + + H1["reactiveHandler
one instance, called from per-conn goroutines"] --- CLS + H1 --- PEND + H1 --- SURF + H1 --- RING + H1 --- LASTR + TICKL["tickLoop"] --- TICKM + INTAKE["intakeAPI decorator"] --- BUS + + %% ------------- outside the database + subgraph OUT["state outside the database"] + direction TB + PK[("passkeys.json
OWNED BY mavweb, not mavend")]:::multi + WK[("wrapped key blob
written by mavend WrapKeyFn,
triggered by mavweb")]:::multi + MAIL[("mavmaild seen-UID file
own volume · NOT DEPLOYED")]:::off + BLOB[("media blobs · retention loop")]:::off + end + PK -.->|"a v1 blob + this file together
recover the database key with no authenticator"| WK + + classDef multi fill:#4f2626,stroke:#e08080,color:#ffecec + classDef off fill:#3a3a3a,stroke:#888,color:#ccc,stroke-dasharray:4 3 diff --git a/docs/architecture/diagrams/05-dependency-boundary.mmd b/docs/architecture/diagrams/05-dependency-boundary.mmd new file mode 100644 index 0000000..af004de --- /dev/null +++ b/docs/architecture/diagrams/05-dependency-boundary.mmd @@ -0,0 +1,141 @@ +%% View 5 — Dependency and boundary map. +%% Architectural components, not classes. Highlights the cycle, the cross-layer +%% calls, the duplicated responsibilities, the fan-in and fan-out hotspots, the +%% process and IPC boundaries, and where a failure propagates. +%% Evidence: cmd/mavend/boot.go, cmd/mavend/tick_api.go, cmd/mavend/voice.go, +%% cmd/mavend/voicewire.go, internal/ipc/server.go, internal/delivery/channel.go. +flowchart TB + + subgraph B1["process boundary — mavend"] + direction TB + + subgraph L_EDGE["entry layer"] + IPCS["ipc.Server
fan-in: 6 processes
+ 8 bypass function fields"] + VSRV["voice.Server"] + HTTPIN["telegram poller"] + end + + subgraph L_API["API layer"] + DAPI["daemonAPI
store adapter + 8 closures"] + IAPI["intakeAPI decorator"] + SAPI["ipc.NewStoreAPI"] + end + + subgraph L_TURN["turn layer"] + RH["reactiveHandler
GOD COMPONENT
34 fields · fan-out ≈ 20"] + TRT["turnRoute"] + PRE["pre-route ladder · 11 rungs"] + ATBL["actionHandlers · 7"] + QCH["querySources · 22"] + end + + subgraph L_ROUTE["routing layer"] + RTR["router.Router cascade"] + G0["stage 0 grammars · 22+"] + HDS["routing heads"] + LLMR["LLM router"] + CLF["classifier"] + end + + subgraph L_PROACT["proactive layer"] + TICK["tickLoop
13 jobs, one function
fan-out ≈ 10"] + GATH["loop.Gatherer"] + RULES["loop rules + gate · pure"] + DISP["delivery.Dispatcher"] + end + + subgraph L_WIRE["construction layer"] + WIRE["wireVoice
builds 17 subsystems
returns voiceWiring"] + BOOT["boot.go
newDaemonAPI + startBackground"] + end + + subgraph L_STATE["state layer"] + ST[("store.Store")] + end + end + + subgraph B2["process boundary — modules"] + MSTT["mavsttd"] + MTTS["mavttsd"] + MWEB["mavweb"] + MPOLL["mavpoll"] + end + + subgraph B3["process boundary — workstation"] + MWAKE["mavwaked"] + MGPU["mavgpud"] + end + + subgraph B4["external services"] + EXT["SearXNG · kiwix · Nexus · Praxis · Hexis
Telegram · ntfy · Home Assistant"] + end + + %% ---------- boundaries + MWEB -.->|"UNIX IPC · 3 conns"| IPCS + MPOLL -.->|"UNIX IPC"| IPCS + MWAKE -.->|"TCP over ssh · plaintext, no auth"| VSRV + MWEB -.->|"TCP · /api/ptt"| VSRV + RH -.->|"UNIX worker"| MSTT + RH -.->|"UNIX worker"| MTTS + RH -.->|"HTTP"| EXT + RH -.->|"HTTP"| MGPU + DISP -.->|"HTTP"| EXT + + %% ---------- the cycle + IPCS --> DAPI + DAPI -->|"chatFn = handler.handleText"| RH + RH -->|"h.api, back-patched by upgradeAPI"| DAPI + + DAPI --> IAPI --> SAPI --> ST + + %% ---------- turn layer + VSRV --> RH + HTTPIN --> DAPI + RH --> TRT --> RTR + RH --> PRE --> TRT + RH --> ATBL --> QCH + QCH --> ST + ATBL --> ST + RTR --> G0 & HDS & LLMR & CLF + + %% ---------- cross-layer calls + QCH -->|"CROSS-LAYER: a query source reads the tick loop"| TICK + RH -->|"CROSS-LAYER: dataStore, the raw store beside the CoreAPI"| ST + DAPI -->|"reads tick state"| TICK + WIRE --> RH + WIRE --> RTR + WIRE --> DISP + BOOT --> DAPI + BOOT --> TICK + + %% ---------- proactive + TICK --> GATH --> ST + TICK --> RULES + TICK --> DISP + DISP --> ST + DISP -->|"voicesink pushes on the request conn"| VSRV + + %% ---------- annotations + DUP1["DUPLICATED RESPONSIBILITY
two independent arbitrations decide a turn:
the 22-grammar cascade, then the 22-source chain.
Both are ordered lists; neither can compare scores."]:::note + DUP1 -.- RTR + DUP1 -.- QCH + + DUP2["DUPLICATED RESPONSIBILITY
restraint is decided twice:
loop.Gate says whether a rule EMITS,
delivery.ChannelsFor says where it LANDS.
Deliberate, and documented in channel.go."]:::note + DUP2 -.- RULES + DUP2 -.- DISP + + DUP3["DUPLICATED RESPONSIBILITY
three unrelated components propose tool rows:
config seeding, MCP discovery, HA discovery."]:::note + DUP3 -.- ST + + FRAG1["FRAGILE PATH
4 seams degrade silently:
workstation model → resident model,
CW2 → mavsttd, heads → LLM → classifier,
search → kiwix → weights.
Nothing on the turn says which one answered."]:::warn + FRAG1 -.- RTR + FRAG1 -.- QCH + + FRAG2["FAILURE PROPAGATION
ipc.Server holds long-lived conns from 4 modules.
Before V-638 that deadlocked EVERY shutdown and
the deployed ciphertext went 11 days stale."]:::warn + FRAG2 -.- IPCS + + FRAG3["PLANNED, UNWIRED
internal/claim + router.ClaimOf: a comparable
unit of evidence for exactly the two arbitrations above.
Nothing calls it. internal/modes: nothing imports it."]:::warn + FRAG3 -.- RTR + + classDef note fill:#2a3f2a,stroke:#7fbf7f,color:#eaffea + classDef warn fill:#4f2626,stroke:#e08080,color:#ffecec diff --git a/docs/architecture/findings.md b/docs/architecture/findings.md new file mode 100644 index 0000000..fc5e7a9 --- /dev/null +++ b/docs/architecture/findings.md @@ -0,0 +1,699 @@ +# Architecture findings: Maven as built + +Read at commit `5cae33a`, 2026-08-25. Working tree dirty: `deploy/mavend.json` +swaps `phraser.model_path` to `maven-instruct-b2-Q4_K_XL.gguf`, plus an edited +`docs/evals/CLAUDE.md` and two untracked files. + +This file is analysis. The factual inventory is +`docs/architecture/maven-architecture.json` and the diagrams under +`docs/architecture/diagrams/`. Nothing here proposes a new architecture. + +**Revised 2026-08-25 after an independent second pass over the evidence pack.** +Four readings changed, and section 6.3 contained one statement that was wrong: +the voice server defaults an empty `Surface`, it does not overwrite the client's. +The sections marked below carry the corrections. + +**The ranking changed with them.** The missing end-to-end authority model +(6.3 through 6.3d) is the first architectural issue, ahead of `reactiveHandler` +size (4.1) and the process boundaries (section 5). Those are refactors. This one +is a property nobody can state. + +Each finding cites what it was read from. Where the repository already names a +problem in its own comments, that is said. A known defect and an undiscovered +one are different facts. + +--- + +## 1. Unclear ownership + +### 1.1 The `facts` table has nine writers and no owner + +`internal/store/schema.sql` calls facts "substrate, all observations". Nine +components append to it, and no component owns the key namespace: + +| Writer | Source tag | Evidence | +|---|---|---| +| `actionFact` | `tap:voice`, `tap:text` | `cmd/mavend/actions_fact.go` | +| quiet-hours toggle | `config` | `cmd/mavend/quiet_toggle.go` | +| mavpoll | `poll:netdata`, `poll:uptimekuma`, `infer:wg`, `poll:zenmoney` | `cmd/mavpoll/main.go` | +| mavcaldav | `poll:caldav` | `cmd/mavcaldav/main.go`, not deployed | +| mavweb | presence, ambient meeting time | `cmd/mavweb/facts.go`, `cmd/mavweb/ambient.go` | +| feed worker | RSS watermark | `cmd/mavend/feeds.go` | +| crawl worker | `crawl:hash:` | `cmd/mavend/crawls.go` `hashKey` | +| fact-enrichment worker | mutates `entity_id`, `resolution_state` | `cmd/mavend/factenrichment.go` | +| tick loop autotune | `cooldown:` | `cmd/mavend/tick.go` `tune`, `internal/loop/feedback.go` `FeedbackKey` | + +Two of these are not observations at all. `crawl:hash:*` is a fetch watermark +and `cooldown:` is a tuning parameter. Both live in the same append-only +table that recall embeds and that `queryFactByKey` reads back as an answer. The +`source` column is what keeps them apart, and it is a convention, not a +constraint: `schema.sql` documents the vocabulary in a comment and the `CHECK` +covers only `kind`. + +### 1.2 `notes` has six writers and one of them is a LAN scan + +`cmd/mavend/netscan.go` `writeScanRecord` writes a scan result as a note. Notes +are the recall corpus: `queryNotes` and `queryMemory` answer from them. So a +network scan record competes by cosine similarity with things he said. + +### 1.3 `tools` is proposed by three unrelated components + +Config seeding (`seedTools`), MCP discovery (`cmd/mavend/mcp.go` `propose`) and +Home Assistant discovery (`cmd/mavend/smarthome.go` `propose`) all write rows. +Only `mavweb` `POST /tools` can enable one, which is the invariant that holds. +But nothing arbitrates a name collision between the three proposers, and +`tools.name` is the primary key. + +### 1.4 The day plan has no store and two owners + +`queryDayPlan` is a query source. The day plan it reads is assembled by the tick +loop (`cmd/mavend/tick_morning.go` `dayPlan`). The bare store adapter cannot +answer it, which is why `upgradeAPI` exists at all (finding 3.1). So a read of +his calendar depends on a proactive scheduler being wired. + +--- + +## 2. Duplicated responsibilities + +### 2.1 Two independent arbitrations decide one turn + +The cascade sorts an utterance into one of seven intents through four arms +(`internal/router/router.go` `Route`). An `IntentQuery` then enters a second +arbitration of twenty-two ordered sources (`cmd/mavend/actions_query.go` +`querySources`, counted in the source). Both are ordered lists. Neither can +compare scores across arms. + +The repository states this itself, in `internal/router/source.go`: + +> The cascade sorted an utterance into one of seven intents with stage 0 rules, +> the resident model and the classifier behind it, a fixture measuring it and +> the decision trace recording it. Then IntentQuery handed the turn to +> querySources in the daemon, a chain of twenty-two branches deciding by seed +> similarity in a fixed order, with none of that. + +`Source` and `queryWalk` narrow the second arbitration with a decision from the +first. They do not merge the two. + +### 2.2 A third arbitration runs before both + +`runTurn` steps 1 through 5e are eleven stateful pre-emptors, each answering "is +this mine?" alone (`cmd/mavend/voice.go`, `preRouteLadder` in +`cmd/mavend/decisiontrace.go`). Their order is argued rung by rung in comments. +That is three ordered lists deciding one utterance, in three files, with three +different notions of confidence. + +`internal/claim/claim.go` names exactly this and counts it: + +> Maven's cascade has twenty-two stage-0 grammars, seven router intents, +> twenty-two query sources and seven stateful pre-emptors, and every one of them +> answers "is this mine?" alone. None can answer "is this more mine than +> yours?" … So list order is the whole arbitration. + +The unit that would fix it is written, tested and called by nothing. See 6.1. + +### 2.3 Restraint is decided twice, deliberately + +`internal/loop/loop.go` `Gate` decides whether a rule emits. +`internal/delivery/channel.go` `ChannelsFor` decides where it lands, and drops +care nudges on away for its own reasons. `channel.go` argues the duplication: + +> double authority is intentional: the gate decides whether a rule EMITS; +> delivery decides where it LANDS. + +Recorded here as duplication that is owned, not as a defect. + +### 2.4 Two digest mechanisms with the same word in the name + +`tickLoop.digestQ` is an in-memory queue batching candidates the gate **allowed**. +`digest_entries` is a table durably holding candidates the gate **blocked**. Both +are flushed in the same `tick()` body, six lines apart +(`cmd/mavend/tick_digest.go`). The distinction is carried entirely by a comment. + +--- + +## 3. Accidental coupling + +### 3.1 A construction cycle between the API layer and the turn layer + +Two back-patches, each documented, together forming a cycle: + +- `cmd/mavend/boot.go`: `api.chatFn = d.voiceW.handler.handleText` +- `cmd/mavend/voice.go` `upgradeAPI`: `h.api = api`, the daemon's own CoreAPI + +So `daemonAPI` holds the handler and the handler holds `daemonAPI`. The comment +on `upgradeAPI` states the reason and the safety argument: + +> Wiring order forces this. wireVoice runs before the tick loop exists … main +> already back-patches the other direction … this is the same seam in reverse. +> Safe against the obvious loop: nothing in the voice path calls api.Chat. + +The safety rests on a negative that nothing enforces. Adding a query source that +calls `api.Chat` would recurse. + +### 3.2 The handler holds the raw store beside the mediated one + +`reactiveHandler` carries both `api ipc.CoreAPI` and +`dataStore *store.Store`, "direct store access for event extraction + pattern +detection" (`cmd/mavend/voice.go`). `internal/ipc/frame.go` states the opposing +rule for the boundary: + +> Core mediates, never hands back a db handle … Anything needing raw db access +> lives in core and is unreachable. + +That holds across the process boundary and not inside it. The turn path has two +ways to reach the same tables, with different auditing. + +### 3.3 The intake journal is bypassed by the one path that needed it + +`cmd/mavend/intake.go` decorates `CoreAPI` so every intake write narrates +itself, and names its own exception: + +> The exception is cmd/mavend/mail.go, which reaches past the interface to +> st.CaptureTask directly. It publishes explicitly. + +One caller reaching past a decorator means the decorator is not the boundary it +claims to be. + +### 3.4 A query source reads the proactive scheduler + +`queryDayPlan` → `tickLoop.dayPlan`. The reactive and proactive halves otherwise +share only the store. This is the single call across that line, and it is the +reason for the `upgradeAPI` back-patch in 3.1. + +--- + +## 4. God components + +### 4.1 `reactiveHandler` has 34 fields + +`cmd/mavend/voice.go:75`. One struct holds stt, tts, the router, the CoreAPI, +the raw store, the tool executor and matcher, the phraser, the replier, the +recall wiring, the crawler, the search client, the Kiwix client, the feeds flag, +the Home Assistant wiring, the LAN scanner, the weather provider and its default +location, the time parser, the dialogue session store, the decision ring, the +trace writer, the encoder id, the clarify store and its attempt cap, the +extractor, a mutex, `lastRouted`, three pending-confirmation registers, +`surfacedItems`, and the ecosystem clients. + +`docs/handler-wiring.md` exists because grouping five of these into `recall` +was itself a task (Vikunja #433). + +Every query source, every action handler and every pre-route resolver is a +method on this one type. There is no seam between "the thing that routes a +turn" and "the thing that knows the house is a Home Assistant". + +### 4.2 `runTurn` is one function with eleven early returns + +`cmd/mavend/voice.go:270`, about 226 lines. Two deferred finalisers, six numbered +steps with lettered sub-steps up to `5e`, and an explicit statement that the +ordering is load-bearing. Eleven of the returns are `return withNotice(...)` +from a pre-emptor. + +### 4.3 `tick` runs thirteen jobs in one function + +`cmd/mavend/tick.go:160`. Gather, save presence, pick a candidate, queue or +phrase-and-dispatch, flush the digest, enqueue gate-suppressed candidates, +expire stale digest, drain digest, fire routines, fire accepted routines, fire +morning routines, detect patterns, deliver reminders, repeat un-acked sev4 +alarms. One 60s ticker drives all of it, so a slow phraser call delays every job +after it. + +### 4.4 `wireVoice` is one constructor for seventeen subsystems + +`cmd/mavend/voicewire.go:108`, about 270 lines, returning a `voiceWiring` struct +whose fields the rest of the daemon reaches into (`embedderOf`, `nexusOf`, +`d.voiceW.mcp`, `d.voiceW.home`, `d.voiceW.server`, `d.voiceW.handler`). + +--- + +## 5. Process boundaries + +### 5.1 Unnecessary: `mavsttd` and `mavttsd` at current scale + +Both are justified in their own headers as "restart-free, key-free, +fail-independent". Both run in the same container image, on the same host, as +the same user, over a socket in a shared volume, and both are hard dependencies +of a turn: `HandlePushToTalk` returns an error reply when either is unavailable. +The key argument is real but partial. `internal/ipc/frame.go` says "a crashing +tts can't read the key page", and the same holds for any goroutine that never +touches the key. + +The boundary earns itself for a different reason the docs do not lead with: +whisper.cpp and piper are cgo and subprocess dependencies, so an in-process +crash would be a daemon crash. Recorded as a boundary whose stated reason and +real reason differ. + +### 5.2 Unnecessary: three IPC connections from one process + +`cmd/mavweb/main.go` opens `core`, `swapConn` and `turnConn` to the same socket, +because `ipc.Client` serialises every call on one mutex and a model swap or a +chat turn would otherwise freeze every page. The comments say so. Connection +count is standing in for request concurrency. + +### 5.3 Missing: the turn path and the tick loop are one process + +They share `store.Store` at `SetMaxOpenConns(1)`, one `phraser.Phraser` and one +`llm.Gate`. A reminder being phrased and a spoken turn being answered contend +for the same llama-server through `internal/llm/gate.go`. Nothing isolates a +foreground turn from a background job beyond that gate. + +### 5.4 Missing: the act executor runs in the key holder + +`internal/tool/tool.go:238` is `exec.CommandContext(ctx, argv[0], argv[1:]...)`, +running inside mavend, the only process holding the database key. +`deploy/mavend.json` seeds twelve rows, five of them destructive, including +`systemctl restart`, `docker restart` and `systemctl reboot`. + +The controls are the enabled allowlist, the risk tier (6.3b) and the confirm +turn. The process boundary is not one of them. `internal/tool/risk.go:84` says +so directly: "It is not a sandbox and it does not try to be one. An enabled row +can already run anything the daemon's user can run." + +### 5.5 The one boundary that is load-bearing and undefended by itself + +The voice TCP wire is plaintext with no auth (`internal/voice/server.go`). Its +security argument is entirely external: loopback publish plus an ssh tunnel +(`docker-compose.yml` `ports: ["127.0.0.1:9110:9100"]`, +`deploy/mavwaked.service` `Requires=maven-voice-tunnel.service`). Correct, and +it means a single compose edit silently removes the whole control. + +--- + +## 6. Implementation disagreeing with apparent responsibility + +### 6.1 `internal/claim` and `router.ClaimOf` are called by nothing + +`internal/router/claim.go` says so in its own doc comment: + +> Nothing in Route calls this yet. The arbiter that reads claims is V-560. + +V-560 landed as `turnRoute` (memoise the route), not as an arbiter. The package +and its `router` adapter are complete and tested and are on no path. + +### 6.2 `internal/modes` is imported by nothing outside itself + +`grep -rn "internal/modes"` over `cmd/` and `internal/` returns only its own +test. It describes itself as "the roughly thirty distinct downstream behaviours +mavend has". That is an inventory of the very thing findings 2.1 and 2.2 are +about. + +### 6.3 The auth tier system does not bind the turn path + +`internal/auth/tier.go` documents "voice can never reach EnableTool, not +because we check the method, but because the surface can't carry the layer", +and `MaxLayer(SurfaceVoice)` returns `Layer0`. `cmd/mavwaked/main.go:275` duly +sends `Surface: voice.SurfaceVoice` on the wire. + +Nothing in `cmd/mavend` reads it. `grep -rn "internal/auth" cmd/ internal/` +outside tests returns `cmd/mavend/main.go` (building the IPC `Gate`), +`cmd/mavweb/webauthn.go`, `internal/webauthn/session.go` and +`internal/voice/wire.go` (type aliases only). `actionAct` +(`cmd/mavend/actions_act.go`) contains no surface check. + +**Two representations of reach exist, and both are ignored.** An earlier draft +of this file said the server overwrites the client's value. It does not. + +1. **Client-asserted, and it survives.** `internal/voice/server.go:198` reads + `if p.Surface == "" { p.Surface = SurfacePCClient }`. That defaults an empty + field. `mavwaked`'s `SurfaceVoice` arrives intact and reaches + `HandlePushToTalk`, which ignores it (`cmd/mavend/voice.go:200`, the + parameter is `req` and only `req.Audio` is read). +2. **Server-created, and it is wrong.** `internal/voice/server.go:148` is + `sess := s.sessions.Add(c, SurfacePCClient)`, hardcoded for every connection + whatever the peer is. Nothing reads that either. + +The consequence matters more than the finding. `req.Surface` is request payload +on a plaintext wire with no auth, so **any voice-wire client can claim +`"pc_client"`**. It must not become an authorization input as it stands. A reach +has to be derived from the transport or the session, never trusted from the +body. + +`auth.Can` runs only in `ipc.Server.Check`, and `FloorEnrollment` maps every +same-uid caller there to `SurfaceCoreProcess` / `Layer3` +(`internal/auth/enrollment.go:65`). + +The comments in `cmd/mavwaked/main.go`, `deploy/mavwaked.service` and +`CLAUDE.md` all present "SurfaceVoice caps acts at L0" as a live control. On the +reactive turn path, `internal/auth` is not what enforces it. Finding 6.3b is. + +### 6.3b There is a second tier system, it is live, and it is not keyed on the reach + +`internal/tool/risk.go` carries its own two-axis policy, and this one runs on +every act: + +```go +policy := PolicyFor(RiskOf(t)) // internal/tool/tool.go:181 +if !policy.VoiceMayRun { return "", ErrNeedsAuthedSurface } +if policy.Confirm && !confirmed { return "", ErrNeedsConfirm } +``` + +`RiskOf` sorts a row into `TierSafe`, `TierDestructive` or `TierIrreversible`. +`PolicyFor` maps those to `{Confirm:false, VoiceMayRun:true}`, +`{Confirm:true, VoiceMayRun:true}` and `{Confirm:true, VoiceMayRun:false}` +(`internal/tool/risk.go:69`). So the control that actually stops an act is real, +well argued, and fails safe on an unknown shape. + +Two observations about it: + +1. **`VoiceMayRun` is not conditioned on voice.** `Executor.Exec` takes + `(ctx, name, args, confirmed)` and no surface. The same policy is applied to + the mic, to telegram inbound and to `POST /api/chat` on the authed page. A + field named for a reach is evaluated identically for every reach. +2. **`systemctl reboot` is `TierDestructive`, not `TierIrreversible`.** + `irreversibleVerbs` (`internal/tool/risk.go:88`) lists `rm`, `mkfs`, `dd`, + `prune`, `truncate` and eleven more. `reboot` is not among them, and + `deploy/mavend.json` seeds it as an enabled row with `destructive: true`. So + it runs on the reactive path after one spoken "да", which is exactly what + `PolicyFor(TierDestructive)` says and is worth stating out loud. + +So the repository has two tier systems: `Surface × Layer` in `internal/auth`, +unread on the turn path, and `Risk × Policy` in `internal/tool`, live. + +They are **not two implementations of one idea**, which is how an earlier draft +of this file read. They are two orthogonal dimensions that never meet. `auth` +answers who or where may carry what authority. `tool` answers what effect a +capability has and what proof it demands. The decision that combines them does +not exist anywhere. + +That both dimensions are also thin today makes the gap easier to see: + +- `FloorEnrollment.Lookup` maps **every** same-uid IPC caller to + `SurfaceCoreProcess` (`internal/auth/enrollment.go:65`), so the process-radius + distinction behind IPC is a future contract, not a live one. +- `PasskeySession` is one global timestamp. `CurrentLayer` and `Assert` both + take a `Scope` and both ignore it (`internal/webauthn/session.go:38` and + `:62`), so step-up is per-daemon rather than per-scope. + +### 6.3c The act policy is more distributed than one gate + +`RiskOf → PolicyFor → Executor.Exec` is one of three act paths, not the act +path. + +| path | risk policy? | evidence | +|---|---|---| +| local tool row | yes | `internal/tool/tool.go:181` | +| Hexis capability | yes, explicitly reused | `cmd/mavend/ecosystem_acts.go:768` `tool.RiskOfCapability` then `tool.PolicyFor` | +| Praxis lifecycle | **no** | `cmd/mavend/ecosystem_acts.go:158` `praxisItemAction.handle` calls `a.call(ctx, px, id)` directly | + +Acknowledge, resolve, ignore and pin are remote mutations that run on first +hearing, with no tier and no confirm turn. They are reversible on the Praxis +side, which is a reason, and it is a reason nothing in the code states. + +`Exec` also has no proof that its `confirmed bool` was bound correctly. The +invariant that a confirmation names one capability, one target and an expiry +lives in `pendingAct` and `resolveConfirm` (`cmd/mavend/confirm.go`), not at the +boundary that acts on it. `Exec` trusts the boolean because only two callers +exist today. + +So the authorization function is spread across origin handling, routing, parked +confirm state, risk classification, allowlist state and execution. Section +"The authorization function as implemented" in `README.md` writes down the part +that is one expression. The rest is not. + +### 6.3d `Claim.Coverage` returns 1.0 for a claim that extracted nothing + +`ClaimOf` builds its consumed span from `claimSpans`, which includes +`d.Slots.Text` unconditionally (`internal/router/claim.go:38`). +`Router.fillSlots` backfills the raw utterance into `Text` for a note, a query +and a chat turn (`internal/router/router.go:334`, `if d.Slots.Text == "" && +d.Intent != IntentReminder`). + +`claim.Split` then marks every token of the utterance explained, and +`Coverage()` is `len(Consumed) / total` (`internal/claim/claim.go:122`). A query +claim that extracted nothing scores 1.0, and `MoreSpecificThan` reads coverage +first. + +`filledSlots` in the same file already knows about this: it counts `Text` "only +when it differs from the whole utterance". `claimSpans`, four functions above +it, does not. + +`internal/router/claim_test.go` does not catch it. All five cases in +`TestClaimOfBands` set `Text` equal to `Utterance`, and the test asserts `Band` +only. Coverage is never asserted anywhere. + +This is why `internal/claim` is not yet an answer to "what competes for a turn". +It is the beginning of a vocabulary. It also has no production callers, no +builders for query sources or pre-route claimants, and it identifies only the +seven-intent destination rather than the roughly thirty behaviours +`internal/modes` enumerates. Keeping it unwired is the right state until that is +resolved, and the file's own comment already warns against it becoming a fourth +arbitration layer. + +### 6.4 Two query sources do not do what their names say + +Two of the twenty-two "query sources" have side effects or read a different +substrate than their name implies. `queryNetwork` triggers a live LAN scan +inside a read path (`cmd/mavend/netscan.go` `scanSummary`), and the scan writes +a note. + +### 6.5 `actionFact` answers queries and chat + +`cmd/mavend/actions_fact.go` re-routes a question-shaped utterance into +`actionQuery` and a complaint into `actionChat`. Both re-routes are argued and +correct in effect. The consequence is that the fact handler is one of three +entry points into the query chain. + +### 6.6 `mavgpud`'s model arm is off and its STT arm is on + +`deploy/mavend.json` sets `workstation.model_disabled: true` while +`workstation.stt` is live. One config block, two independently authenticated +services, one flag that turns off half of it. The block's own comment explains +this. A reader of the topology would not guess it. + +--- + +## 7. Hidden shared state + +### 7.1 Six context keys carry per-turn state + +`querySourceKey`, `turnRouteKey`, `dialogueKey`, `ecosystemCorrelationKey`, +`traceIDKey` (all `cmd/mavend/`), and `recorderKey` +(`internal/decision/decision.go`). Plus `callerKey` in `internal/ipc/api.go`. + +Every one is invisible in a function signature. `turnRouteFrom` returns nil +"when the caller is not inside runTurn, a unit test calling one resolver +directly, most often". That is the shape of the problem: a resolver behaves +differently depending on invisible context. + +### 7.2 Three single-slot confirmation registers under one mutex + +`reactiveHandler.pending`, `pendingRoutine`, `pendingHexis` +(`cmd/mavend/voice.go:170-180`). The comment states the posture: "single slot, +single-user box, a second act while one waits overwrites it (last-asked wins)". +Three separate registers, one shared mutex, and the pre-route ladder decides +between them by position rather than by comparing them. + +### 7.3 `surfacedItems` has no TTL + +Same struct. The comment argues it: a stale position resolves to an item Praxis +reports as already acknowledged, "which is a harmless answer, unlike a stale +confirmation". That is correct given Praxis is the arbiter. It also means an +ordinal can refer to a list read out an arbitrarily long time ago. + +### 7.4 The tick loop's memory is in-process and unbounded in one place + +`tickLoop.lastPhrase` is a `map[string]delivery.PhrasedNudge` keyed by rule +name, and rules are a fixed set, so it is bounded. `digestQ` is a slice with a +config `MaxItems`. `lastProposalAt` is deliberately not persisted: "a restart is +allowed to permit one more announcement". + +### 7.5 The clarify store is deliberately not persisted, while the dialogue store is + +`cmd/mavend/voicewire.go`: `dialogue.NewPersistentSessionStore` for follow-up +slots, `dialogue.NewClarifyStore` for the parked question. The reasoning is +recorded (Vikunja #385). The consequence is that a restart mid-clarify silently +drops a request the user believes is parked, and the "expired clarify notice" +path in `runTurn` step 1 cannot fire for it, because the store it reads is gone +too. + +--- + +## 8. Fragile request paths + +### 8.1 Four silent degradations stacked on one turn + +| Seam | Falls back to | Told to the user? | +|---|---|---| +| workstation model → resident model | `internal/llm/remote.go` `Pair.Complete` | no, by design (`docs/offload.md`) | +| CW2 → mavsttd | `cmd/mavend/voicewire.go` `sttSeam` | no | +| routing heads → LLM router → classifier | `internal/router/router.go` | no | +| search → kiwix → named page → model weights | `cmd/mavend/actions_query.go` | no | + +Each is individually argued. Together, a single answer can be the resident model +routing a worse transcript with the classifier as a floor and answering from its +own weights, and nothing in the reply distinguishes that from the best case. The +only instrument is the decision record and the query-source log line. + +### 8.2 The reminder path depends on a table nobody writes + +`queryCalendar` reads `facts(kind=env, source=caldav:*)`, and `mavcaldav` is +commented out in `docker-compose.yml`. `loop.State.CalendarBusy` reads the same +facts, so the "do not nag mid-meeting" suppressor is permanently false. The +compose comment says both of these explicitly, which makes it a known gap rather +than a hidden one. + +### 8.3 Recurring reminders have storage, an IPC parameter, and no caller + +`reminders.cron` and `reminders.next_fire_ts` exist since migration #2 +(`internal/store/migrations.go`). `ipc.CreateReminder` takes a cron argument. +`actionReminder` passes `""`. Nothing on the spoken path can create one. + +### 8.4 Shutdown is a known past failure with a bounded workaround + +`cmd/mavend/main.go` carries the history: long-lived module connections +deadlocked every shutdown, `run()` never returned, `defer st.Close()` never +sealed, and "the deployed ciphertext was eleven days stale before anyone +noticed". The fix is `workerGrace = 4 * time.Second` plus tracked connections. +A worker parked in a model call still loses its tick, and the seal proceeds +without it. + +### 8.5 One inbound worker is outside the assertable worker set + +`backgroundWorkers` in `cmd/mavend/boot.go` exists so "a test can compare the +set the two paths would start without standing a daemon up". `wireTelegramIntake` +starts its poller with `wg.Add(1)` and a bare goroutine +(`cmd/mavend/telegramintake.go:41`), so it is not in that set. It is at least on +the outer `WaitGroup`, unlike the seven workers V-639 fixed. + +### 8.6 The daemon is wired twice, in two places + +`run()` wires everything at boot. `srv.UnlockFn` wires everything again after a +passkey assertion. `boot.go` exists because those two lists had already drifted: +"seven workers started untracked on the unlock path and two daemonAPI fields +were never set there, silently". Both paths now funnel through `newDaemonAPI` +and `startBackground`. But `wireRules`, `wireGatherer`, `wirePhraser`, +`wireEcosystem`, `wireVoice`, `wireDispatcher`, `wireTickLoop`, the four worker +constructors, `wireMailIntake`, `wireModelSwap`, `wireTelegramIntake`, +`wireVision`, `wireCapture` and `wireSpeaker` are still listed twice, by hand, +in the same file. + +--- + +## 9. Difficult-to-test boundaries + +### 9.1 A resolver's behaviour depends on invisible context + +See 7.1. `turnRouteFrom(ctx)` returning nil is the documented test case, and it +changes what the resolver does. + +### 9.2 The single-instance handler is the unit under test for ~60 behaviours + +Twenty-two query sources, seven action handlers, eleven pre-route resolvers and +the recall gate are all methods on `*reactiveHandler`. Testing one requires +constructing a struct with 34 fields, most of them nil. + +### 9.3 The static gates pass against a baseline, and the baseline records the debt + +`scripts/analyzers/deadcode.baseline` accepts thirteen unreachable symbols, +eleven of them from the 2026-08-10 audit (V-686), with three marked as +"must stay". `make audit` is a git-grep inventory and is explicitly not a +reachability check (`CLAUDE.md`). + +### 9.4 Measurement needs weights that are not in the tree + +`make t` self-skips the four `TestONNX*` measurements without `MAVEN_ONNX_LIB`, +and still prints `ok` (`CLAUDE.md`). The routing heads, the embedder, silero and +the keyword head are all ONNX files under `models/`, bind-mounted from +`/mnt/hdd1/llms` in the case of the gguf. A checkout alone cannot reproduce a +routing measurement. + +### 9.5 Only 5 of 51 spec entries cite a scenario that exists + +Recorded in the previous session's handoff, from `docs/spec.md` and +`cmd/mavend/testdata/scenarios/`. Not re-verified here. + +--- + +## 10. Excessive fan-in and fan-out + +**Fan-in.** `ipc.Server` is reached by six processes (mavweb ×3 connections, +mavpoll, mavcaldav, mavmaild, mavupdate, e2eprobe) and carries eight function +fields that bypass `CoreAPI` entirely: `StepUp`, `UnlockFn`, `WrapKeyFn`, +`IngestMailFn`, `SwapModelFn`, `ModelStatusFn`, `DescribeImageFn` and the four +`Capture*` fields. Each is nil unless its config block exists, so the wire +surface of the daemon depends on `deploy/mavend.json`. + +**Fan-out.** `reactiveHandler` reaches roughly twenty distinct subsystems +(4.1). `tickLoop` reaches ten (4.3). `wireVoice` constructs seventeen (4.4). + +**Failure propagation.** The store is the shared point: `SetMaxOpenConns(1)` +means every writer in the daemon and every module over IPC serialises through +one connection. The measurement backing that cap is +`docs/evals/2026-08-07-store-connection-cap.md` (V-642), cited in +`internal/ipc/server.go` and not re-run here. + +--- + +## 11. What is dark, and what that costs + +Sixteen components are wired in code and off in the deployed configuration: +`ntfy`, `zenmoney`, Home Assistant, MCP, the weather provider, the workstation +model arm, vision, meeting capture, speaker identification, mail intake, model +swap, memory evaluation, and the `mavcaldav` and `mavmaild` services. + +Three of these have a visible cost: + +1. **ntfy disabled** means the away reach is telegram alone, through a SOCKS + relay, through `api.telegram.org`. `deploy/mavend.json` documents that this + was exactly the fragility ntfy was added to remove: "three things in series + that have each failed once, and when they do a sev4 nudge has nowhere to go." +2. **mavcaldav absent** disables both the calendar answer and the busy + suppressor (8.2). +3. **The weather provider is a stub.** `wireVoice` selects Open-Meteo only when + `cfg.Voice.Weather.Provider == "open-meteo"`, and the deployed `voice` block + has no `weather` key at all. `weather` is nevertheless a live query source with + `guesses: true`, so it can claim a turn and answer it from a stub. + +--- + +# Questions the current architecture raises + +1. **Which of the three ordered lists is the arbiter?** Stage 0 grammars, the + query-source chain and the pre-route ladder each decide by position. If + `internal/claim` is the answer, what stops it being a fourth list rather than + the thing that collapses the other three? + +2. **Where is the one point that decides whether this authenticated origin may + perform this specific effect using this specific evidence?** Today there is + no such point. `reboot` shows why the question is not "which tier system + wins": it is correctly classified as not irreversible, and that does not + imply a room microphone plus "да" should carry reboot authority. + Reversibility, effect severity, reach authority and confirmation strength are + four dimensions, and `TierDestructive → VoiceMayRun:true` collapses them into + one. + +3. **What owns the `facts` key namespace?** Nine writers, two of which store + watermarks and tuning parameters in the table that recall embeds. Is `source` + meant to be a partition, and if so what enforces it? + +4. **Should the executor live in the key holder?** `systemctl reboot` is a + seeded, enabled row in a process holding the unlocked database. The controls + are an allowlist and a spoken confirm. Is that the intended trust boundary, + or the one that happened? + +5. **Should the tick loop and the turn path share one llama-server?** + `internal/llm/gate.go` exists to arbitrate them. What is the acceptable + latency a foreground turn may pay for a background nudge being phrased? + +6. **Is a silent four-level degradation still honest?** Each fallback is argued + separately. Nothing tells the user when all four fire at once. The M1 honesty + milestone in `docs/roadmap.md` is about the turn path. Does it cover this? + +7. **What is `docker-compose.yml` the source of truth for?** Two complete + services are commented out in it with their reasoning, and one of them + silently disables two behaviours elsewhere. Should absence be expressible in + `deploy/mavend.json` where the rest of the capability switches live? + +8. **Why is the daemon wired twice?** `boot.go` fixed the drift that had already + happened. Fifteen `wire*` calls are still listed by hand on both paths. Is + cold-start unlock worth a second wiring path, or should the locked daemon + wire everything and gate at the `Check` hook alone? + +9. **What is a query source allowed to do?** One triggers a live LAN scan and + writes a note. If a source may have side effects, what does "first source to + claim answers the turn" guarantee about the sources that ran before it? + +10. **Is `mavsttd`/`mavttsd`'s process boundary about the key or about cgo?** + The stated reason is key isolation. The operative reason looks like crash + isolation from cgo and subprocesses. Which one governs whether the next + model caller gets its own process? diff --git a/docs/architecture/maven-architecture.json b/docs/architecture/maven-architecture.json new file mode 100644 index 0000000..76694cd --- /dev/null +++ b/docs/architecture/maven-architecture.json @@ -0,0 +1,6263 @@ +{ + "schema": 1, + "generated": "2026-08-25", + "repo": "/mnt/server/home/kami/apps/Maven", + "commit": "5cae33a517ebeca55e2875512766c007b4e7457b", + "working_tree": "dirty: deploy/mavend.json modified (phraser.model_path swapped to maven-instruct-b2), docs/evals/CLAUDE.md modified, two untracked files", + "method": "Read from source, config, compose, systemd units, schema and migrations. No component is inferred from a directory name. Every entry cites the files and symbols it was read from.", + "legend": { + "type": [ + "process", + "service", + "worker", + "handler", + "arbitration", + "query_source", + "adapter", + "boundary", + "storage", + "table", + "shared-state", + "model", + "external", + "config", + "test", + "planned" + ], + "status": { + "implemented": "on the running path of the deployed configuration", + "configured-off": "wired in code, dark because its config block is absent or disabled", + "built-not-deployed": "a complete binary or service that docker-compose.yml does not run", + "partially-wired": "the code path exists and one end of it has no producer or consumer", + "planned-unwired": "written and tested, called by nothing", + "temporary": "explicitly marked in its own source as disposable", + "dead": "unreachable, accepted in scripts/analyzers/deadcode.baseline" + }, + "confidence": { + "high": "read directly from code, config or schema", + "medium": "the wiring is in the source but the full call path was not traced end to end", + "low": "inferred from one reference; treat as uncertain" + } + }, + "components": [ + { + "id": "proc.mavend", + "type": "process", + "group": "homesrv", + "responsibility": "The core daemon and the only holder of the database key. Owns the store, the IPC socket, the voice TCP listener, the tick loop, every in-core background worker and the child llama-server.", + "files": [ + "cmd/mavend/main.go", + "cmd/mavend/boot.go", + "docker-compose.yml" + ], + "symbols": [ + "main", + "run", + "daemonLock", + "startBackground", + "backgroundWorkers", + "newDaemonAPI", + "depsNow" + ], + "reads": [ + "cfg.mavend" + ], + "writes": [ + "state.db_file" + ], + "calls": [ + "core.ipc_server", + "core.voice_server", + "core.tick_loop", + "svc.llama_server" + ], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "compose service `mavend`. Two nets: `default` (module DNS) and `ecosystem`. Publishes 127.0.0.1:9110 -> container 9100." + }, + { + "id": "proc.mavsttd", + "type": "process", + "group": "homesrv", + "responsibility": "Speech-to-text module process. Loads whisper.cpp (ggml-small.bin) through cgo and serves transcription on a unix socket.", + "files": [ + "cmd/mavsttd/main.go", + "cmd/mavsttd/whisper_handler.go", + "docker-compose.yml" + ], + "symbols": [ + "main", + "whisperHandler" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.stt_seam" + ], + "confidence": "high", + "status": "implemented", + "notes": "Key-free. /run/maven/stt.sock on the shared `sockets` volume. Needs /dev/dri and the render gid for the Vulkan build." + }, + { + "id": "proc.mavttsd", + "type": "process", + "group": "homesrv", + "responsibility": "Text-to-speech module process. Runs piper with ru_RU-irina-medium and returns wav bytes on a unix socket.", + "files": [ + "cmd/mavttsd/main.go", + "cmd/mavttsd/piper_handler.go", + "docker-compose.yml" + ], + "symbols": [ + "main", + "piperHandler" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.tts_seam" + ], + "confidence": "high", + "status": "implemented", + "notes": "/run/maven/tts.sock." + }, + { + "id": "proc.mavweb", + "type": "process", + "group": "homesrv", + "responsibility": "The HTTP surface: PWA, dashboards, chat page, tool and routine consoles, WebAuthn enrollment and step-up, presence and ambient ingest.", + "files": [ + "cmd/mavweb/main.go", + "cmd/mavweb/pages.go", + "cmd/mavweb/chat.go", + "cmd/mavweb/ambient.go", + "cmd/mavweb/credentials.go", + "cmd/mavweb/models.go", + "cmd/mavweb/facts.go", + "cmd/mavweb/ecosystem.go", + "cmd/mavweb/notifications.go" + ], + "symbols": [ + "main", + "corePage", + "gatedPage", + "handlePTT", + "handleChatPage", + "handleModels", + "handleSignal", + "handleTools", + "handleRoutines", + "logUnguardedSurfaces" + ], + "reads": [], + "writes": [ + "state.passkey_file" + ], + "calls": [ + "core.ipc_server", + "core.voice_server", + "ext.nexus", + "ext.praxis", + "ext.hexis" + ], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Opens THREE ipc.Client connections to mavend (main, /models, /api/chat) because ipc.Client serialises on one mutex. Published on 127.0.0.1:9201 only." + }, + { + "id": "proc.mavpoll", + "type": "process", + "group": "homesrv", + "responsibility": "Environment poller. Reads netdata alarms, uptime-kuma metrics, wireguard handshakes and optionally zenmoney, writing facts(kind=env, source=poll:*) over IPC. Writes only on value change.", + "files": [ + "cmd/mavpoll/main.go", + "internal/zenmoney/" + ], + "symbols": [ + "run" + ], + "reads": [], + "writes": [ + "state.facts" + ], + "calls": [ + "core.ipc_server", + "ext.netdata", + "ext.uptimekuma", + "ext.zenmoney" + ], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "network_mode: host. Holds the third-party credentials so core never sees them; the zenmoney arm is dark because compose mounts no token file." + }, + { + "id": "proc.mavwaked", + "type": "process", + "group": "workpc", + "responsibility": "Always-on listening client. Runs arecord, silero VAD and the keyword head, ships one utterance per activation over the voice wire, plays the reply through aplay, and receives proactive pushes on the same conn.", + "files": [ + "cmd/mavwaked/main.go", + "cmd/mavwaked/vad.go", + "cmd/mavwaked/silero.go", + "cmd/mavwaked/wakeword.go", + "cmd/mavwaked/wakefeatures.go", + "cmd/mavwaked/nudge.go", + "cmd/mavwaked/playback.go", + "cmd/mavwaked/session.go", + "deploy/mavwaked.service" + ], + "symbols": [ + "main", + "run" + ], + "reads": [], + "writes": [], + "calls": [ + "core.voice_server" + ], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "systemd USER unit on workpc, never in docker-compose. Requires maven-voice-tunnel.service: it reaches mavend's loopback 9100 over ssh, not the LAN." + }, + { + "id": "proc.mavgpud", + "type": "process", + "group": "workpc", + "responsibility": "GPU supervisor on the workstation. Keeps llama-server loaded while the card is free, unloads on idle or contention, supervises the CrisperWhisper2 transcriber, and proxies both behind a bearer token.", + "files": [ + "cmd/mavgpud/main.go", + "cmd/mavgpud/gpu.go", + "cmd/mavgpud/runner.go", + "cmd/mavgpud/auth.go", + "deploy/mavgpud.service" + ], + "symbols": [ + "main", + "config", + "requireToken" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.model_seam", + "core.stt_seam" + ], + "confidence": "high", + "status": "implemented", + "notes": "Deployed separately from every Maven daemon. Maven never asks it to start anything; llm.Pair only reads /health." + }, + { + "id": "proc.mavcaldav", + "type": "process", + "group": "homesrv", + "responsibility": "CalDAV reader and renderer. Polls a collection into facts(kind=env, source=poll:caldav) and can publish Maven's own reminders back as iCal.", + "files": [ + "cmd/mavcaldav/main.go", + "cmd/mavcaldav/render.go", + "internal/calendar/" + ], + "symbols": [ + "run", + "checkRenderTarget" + ], + "reads": [], + "writes": [ + "state.facts" + ], + "calls": [ + "core.ipc_server" + ], + "called_by": [], + "confidence": "high", + "status": "built-not-deployed", + "notes": "Commented out in docker-compose.yml, and the comment there names the cost: the `calendar` query source and loop.State.CalendarBusy read facts nobody writes." + }, + { + "id": "proc.mavmaild", + "type": "process", + "group": "homesrv", + "responsibility": "IMAP reader. Fetches unseen messages and hands each to core on ipc.MethodIngestMail; core extracts task candidates with the resident model.", + "files": [ + "cmd/mavmaild/main.go", + "internal/email/" + ], + "symbols": [ + "run" + ], + "reads": [], + "writes": [ + "state.maildata" + ], + "calls": [ + "core.mail_intake" + ], + "called_by": [], + "confidence": "high", + "status": "built-not-deployed", + "notes": "Commented out in docker-compose.yml. Reads its password from a file so core never sees it." + }, + { + "id": "proc.mavenclient", + "type": "process", + "group": "workpc", + "responsibility": "Reference voice client. Ships one wav per invocation over the voice wire and writes the reply wav. No VAD, no keyword.", + "files": [ + "cmd/mavenclient/main.go" + ], + "symbols": [ + "main" + ], + "reads": [], + "writes": [], + "calls": [ + "core.voice_server" + ], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "A reference and test binary, not a deployed service." + }, + { + "id": "proc.mavseal", + "type": "process", + "group": "homesrv", + "responsibility": "Recovery CLI. Re-encrypts a live tmpfs working copy back over the ciphertext file when mavend was killed rather than shut down.", + "files": [ + "cmd/mavseal/main.go" + ], + "symbols": [ + "main" + ], + "reads": [ + "state.db_tmpfs" + ], + "writes": [ + "state.db_file" + ], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Not part of the daemon. Uses VACUUM INTO, so it is safe against a live database." + }, + { + "id": "proc.mavupdate", + "type": "process", + "group": "homesrv", + "responsibility": "Deployment CLI with automatic rollback. The only trigger for the update path.", + "files": [ + "cmd/mavupdate/main.go", + "internal/update/" + ], + "symbols": [ + "main", + "update.Updater" + ], + "reads": [], + "writes": [], + "calls": [ + "core.ipc_server" + ], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Deliberately has no IPC method and no web button: there is no MethodApplyUpdate in internal/ipc, and mavend never constructs an update.Updater." + }, + { + "id": "proc.e2eprobe", + "type": "process", + "group": "dev", + "responsibility": "Typed IPC driver written for the 2026-08-15 acceptance session.", + "files": [ + "cmd/e2eprobe/main.go" + ], + "symbols": [ + "main" + ], + "reads": [], + "writes": [], + "calls": [ + "core.ipc_server" + ], + "called_by": [], + "confidence": "high", + "status": "temporary", + "notes": "Its own doc comment says it is removed after the session. It is still in the tree." + }, + { + "id": "proc.labelgen", + "type": "process", + "group": "dev", + "responsibility": "Offline labeller. Runs the real stage 0 grammars over an utterance file and prints JSONL training data for the routing heads.", + "files": [ + "cmd/labelgen/main.go" + ], + "symbols": [ + "main" + ], + "reads": [ + "router.stage0" + ], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Omits the wakeword-act grammar, whose allowlist is a deployment's enabled tool names." + }, + { + "id": "svc.llama_server", + "type": "model", + "group": "homesrv", + "responsibility": "The resident model. A llama-server child process mavend starts and owns, serving both routing and phrasing.", + "files": [ + "internal/phraser/server.go", + "internal/phraser/llmphraser.go", + "internal/llm/client.go", + "deploy/mavend.json" + ], + "symbols": [ + "phraser.NewLLMPhraser", + "phraser.Config", + "llm.Client", + "llm.Gate" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.phraser", + "core.llm_router", + "core.replier" + ], + "confidence": "high", + "status": "implemented", + "notes": "deploy/mavend.json currently points model_path at maven-instruct-b2-Q4_K_XL.gguf; the committed value was Qwen3-1.7B-UD-Q4_K_XL. n_ctx 4096, n_gpu_layers 99, cache_ram_mib 512." + }, + { + "id": "ext.searxng", + "type": "external", + "group": "homesrv", + "responsibility": "Self-hosted metasearch. The first world source, asked after every source reading his own data.", + "files": [ + "cmd/mavend/searchwire.go", + "internal/websearch/", + "deploy/mavend.json" + ], + "symbols": [ + "wireSearch", + "websearch.Client.Search" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.q.search" + ], + "confidence": "high", + "status": "implemented", + "notes": "http://searxng:9563. Needs `json` in search.formats. Only the query string leaves the box." + }, + { + "id": "ext.kiwix", + "type": "external", + "group": "homesrv", + "responsibility": "Offline ZIM encyclopedia server. The fallback behind SearXNG.", + "files": [ + "cmd/mavend/kiwixwire.go", + "internal/kiwix/", + "deploy/mavend.json" + ], + "symbols": [ + "wireKiwix", + "kiwix.Client" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.q.kiwix" + ], + "confidence": "high", + "status": "implemented", + "notes": "http://kiwix-server:8080. Books wikipedia_en_all_maxi_2026-02 and wikipedia_ru_all_maxi_2026-02." + }, + { + "id": "ext.nexus", + "type": "external", + "group": "ecosystem", + "responsibility": "Identity service. Resolves free text to a canonical entity id; ambiguity asks rather than picks.", + "files": [ + "cmd/mavend/ecosystem.go", + "deploy/mavend.json" + ], + "symbols": [ + "nexusClient", + "nexusClient.Resolve", + "wireEcosystem" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.ecosystem", + "core.fact_enrichment", + "core.daemon_api" + ], + "confidence": "high", + "status": "implemented", + "notes": "http://nexus:9740." + }, + { + "id": "ext.praxis", + "type": "external", + "group": "ecosystem", + "responsibility": "Attention and operational-state service, read over its HTTP tools API and never from its SQLite file.", + "files": [ + "cmd/mavend/ecosystem.go", + "cmd/mavend/attentionq.go", + "deploy/mavend.json" + ], + "symbols": [ + "praxisClient", + "praxisClient.ListAttention", + "praxisClient.postItemAction" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.q.attention", + "core.ecosystem_acts" + ], + "confidence": "high", + "status": "implemented", + "notes": "http://praxis:8989. Surfaced is not acknowledged; acknowledged is not resolved." + }, + { + "id": "ext.hexis", + "type": "external", + "group": "ecosystem", + "responsibility": "Capability execution service. The path a mutating act takes when it is not a local allowlisted tool.", + "files": [ + "cmd/mavend/ecosystem.go", + "cmd/mavend/ecosystem_acts.go", + "deploy/mavend.json" + ], + "symbols": [ + "hexisclient.New", + "handleHexisAct", + "pendingHexisExec" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.action_act" + ], + "confidence": "high", + "status": "implemented", + "notes": "http://hexis:9741. Free text never reaches a mutating call: an entity id is resolved first." + }, + { + "id": "ext.telegram", + "type": "external", + "group": "internet", + "responsibility": "Telegram Bot API through a SOCKS relay. Both an away reach and, since V-637, an inbound channel.", + "files": [ + "internal/delivery/telegramsink/telegramsink.go", + "internal/delivery/telegramsink/intake.go", + "deploy/mavend.json" + ], + "symbols": [ + "telegramsink.New", + "telegramsink.NewPoller" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.sink_telegram", + "core.telegram_intake" + ], + "confidence": "high", + "status": "implemented", + "notes": "proxy socks5://192.168.240.1:10808, intake: true in the deployed config. Long-poll getUpdates, not a webhook." + }, + { + "id": "ext.ntfy", + "type": "external", + "group": "internet", + "responsibility": "Push reach. Present in the config and disabled there.", + "files": [ + "internal/delivery/ntfysink/", + "cmd/mavend/main.go", + "deploy/mavend.json" + ], + "symbols": [ + "ntfysink.New", + "wireNtfySink" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.sink_ntfy" + ], + "confidence": "high", + "status": "configured-off", + "notes": "deploy/mavend.json sets ntfy.disabled = true, so wireNtfySink returns a nil Sink and the dispatcher slot is nil." + }, + { + "id": "ext.netdata", + "type": "external", + "group": "homesrv", + "responsibility": "Resource alarms, read by mavpoll.", + "files": [ + "cmd/mavpoll/main.go" + ], + "symbols": [ + "run" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavpoll" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "ext.uptimekuma", + "type": "external", + "group": "homesrv", + "responsibility": "Service up/down, read by mavpoll over /metrics with an API key. The source of truth for service_down.", + "files": [ + "cmd/mavpoll/main.go", + "internal/loop/rules.go" + ], + "symbols": [ + "run", + "loop.ServiceDownRule" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavpoll" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "ext.zenmoney", + "type": "external", + "group": "internet", + "responsibility": "Spending totals. Dark: compose mounts no token file.", + "files": [ + "internal/zenmoney/", + "cmd/mavpoll/main.go" + ], + "symbols": [ + "zenmoney.Client" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavpoll" + ], + "confidence": "high", + "status": "configured-off", + "notes": "" + }, + { + "id": "ext.homeassistant", + "type": "external", + "group": "lan", + "responsibility": "The house. Discovery proposes one always-destructive tool row per controllable device.", + "files": [ + "internal/smarthome/", + "cmd/mavend/smarthome.go", + "deploy/mavend.json" + ], + "symbols": [ + "wireSmartHome", + "smarthome.Client", + "homeWiring" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.home_worker", + "core.q.home" + ], + "confidence": "high", + "status": "configured-off", + "notes": "deploy/mavend.json smarthome.enabled = false." + }, + { + "id": "ext.openmeteo", + "type": "external", + "group": "internet", + "responsibility": "Weather provider.", + "files": [ + "internal/weather/", + "cmd/mavend/voicewire.go" + ], + "symbols": [ + "weather.NewOpenMeteoProvider", + "weather.NewStubProvider" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.q.weather" + ], + "confidence": "high", + "status": "configured-off", + "notes": "wireVoice picks it only when cfg.Voice.Weather.Provider == 'open-meteo'. The deployed voice block has no `weather` key, so the Stub provider is wired and the weather query source answers from a stub." + }, + { + "id": "ext.vikunja_mcp", + "type": "external", + "group": "lan", + "responsibility": "MCP server whose tools are PROPOSED into the same act allowlist as everything else.", + "files": [ + "internal/mcp/", + "cmd/mavend/mcp.go", + "deploy/mavend.json" + ], + "symbols": [ + "wireMCP", + "mcp.Manager", + "mcpWiring" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.mcp_worker" + ], + "confidence": "high", + "status": "configured-off", + "notes": "mcp.servers[0].enabled = false." + }, + { + "id": "ext.cw2_stt", + "type": "external", + "group": "workpc", + "responsibility": "CrisperWhisper2 turbo on the workstation, supervised by mavgpud on port 8081. A second service, not a second endpoint.", + "files": [ + "cmd/mavgpud/main.go", + "internal/stt/", + "cmd/mavend/voicewire.go", + "deploy/cw2/", + "deploy/mavend.json" + ], + "symbols": [ + "stt.Pair", + "sttSeam" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.stt_seam" + ], + "confidence": "high", + "status": "implemented", + "notes": "Silent fallback to mavsttd when the workstation is down; a worse transcript is still a turn." + }, + { + "id": "ext.piper", + "type": "external", + "group": "homesrv", + "responsibility": "The TTS binary mavttsd runs.", + "files": [ + "cmd/mavttsd/piper_handler.go" + ], + "symbols": [ + "piperHandler" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavttsd" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "ext.whispercpp", + "type": "external", + "group": "homesrv", + "responsibility": "whisper.cpp, linked into mavsttd through cgo.", + "files": [ + "cmd/mavsttd/whisper_handler.go" + ], + "symbols": [ + "whisperHandler" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavsttd" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "ext.alsa", + "type": "external", + "group": "workpc", + "responsibility": "arecord and aplay, spawned by mavwaked; the mic is a named ALSA plug device.", + "files": [ + "cmd/mavwaked/main.go", + "deploy/asoundrc", + "deploy/mavwaked.service" + ], + "symbols": [ + "run" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavwaked" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "bnd.ipc", + "type": "boundary", + "group": "homesrv", + "responsibility": "The core-to-module boundary. Length-prefixed JSON over a unix domain socket; 0700 dir and 0600 socket are the auth floor. Core mediates and never hands back a db handle.", + "files": [ + "internal/ipc/frame.go", + "internal/ipc/wire.go", + "internal/ipc/server.go", + "internal/ipc/client.go" + ], + "symbols": [ + "writeFrame", + "readFrame", + "maxFrame", + "Method", + "ipc.Server", + "ipc.Client", + "ipc.DialWait" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavweb", + "proc.mavpoll", + "proc.mavcaldav", + "proc.mavmaild", + "proc.mavupdate", + "proc.e2eprobe" + ], + "confidence": "high", + "status": "implemented", + "notes": "Hand-rolled framing, kept deliberately (Vikunja #410). 64 method constants in internal/ipc/wire.go, 63 plus Ping." + }, + { + "id": "bnd.voice_tcp", + "type": "boundary", + "group": "homesrv", + "responsibility": "The client-to-core network surface. Plaintext TCP with no auth of its own; every conn registers a Session and carries both requests and server-initiated pushes.", + "files": [ + "internal/voice/server.go", + "internal/voice/wire.go", + "internal/voice/session.go", + "docker-compose.yml" + ], + "symbols": [ + "voice.Server", + "voice.Sessions", + "PushToTalkReq", + "PushToTalkResp", + "Sessions.PushToMostRecent" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavwaked", + "proc.mavenclient", + "proc.mavweb" + ], + "confidence": "high", + "status": "implemented", + "notes": "Bound 0.0.0.0:9100 inside the container so mavweb can reach it by name, published only on 127.0.0.1:9110. workpc reaches it over ssh. TWO representations of reach, both ignored: server.go:198 only DEFAULTS an empty p.Surface, so a client-asserted SurfaceVoice survives and HandlePushToTalk never reads it; server.go:148 hardcodes Session.Surface to SurfacePCClient for every conn. req.Surface is request payload on an unauthenticated wire, so any client can claim pc_client. It must not become an authorization input as it stands." + }, + { + "id": "bnd.worker", + "type": "boundary", + "group": "homesrv", + "responsibility": "The core-to-stt/tts boundary. One Client, one conn, one mutex; the Transcriber and Synthesizer interfaces are the Stub/Remote swap seam.", + "files": [ + "internal/worker/client.go", + "internal/worker/server.go", + "internal/worker/wire.go", + "internal/stt/stt.go", + "internal/tts/tts.go" + ], + "symbols": [ + "worker.Client", + "worker.Dial", + "stt.NewRemote", + "tts.NewRemote" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.stt_seam", + "core.tts_seam" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "bnd.http_web", + "type": "boundary", + "group": "homesrv", + "responsibility": "The mavweb HTTP surface. Loopback-only by construction; step-up exists only when -webauthn-origin and -webauthn-rpid are set.", + "files": [ + "cmd/mavweb/main.go" + ], + "symbols": [ + "mux", + "logUnguardedSurfaces", + "mavwebHTTPServer", + "requireStepUp" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Six surfaces are named as unguarded without WebAuthn: POST /tools, /routines, /models, /api/revert, /api/chat, /api/ptt." + }, + { + "id": "bnd.http_ecosystem", + "type": "boundary", + "group": "ecosystem", + "responsibility": "The Maven-to-ecosystem HTTP boundary. Every request carries a contract version header, X-Requested-By: maven and a correlation id minted once per action.", + "files": [ + "cmd/mavend/ecosystem.go" + ], + "symbols": [ + "ecosystemHTTP", + "setHeaders", + "withCorrelationID", + "ecosystemAPIVersion", + "mavenRequester", + "ecosystemError" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.ipc_server", + "type": "service", + "group": "mavend", + "responsibility": "The IPC listener. Accepts module connections, runs the one Check authorization hook, then dispatches to CoreAPI or to one of the bypass function fields.", + "files": [ + "internal/ipc/server.go", + "cmd/mavend/main.go" + ], + "symbols": [ + "ipc.Listen", + "Server.Serve", + "Server.Check", + "Server.SetAPI", + "Server.StepUp", + "Server.UnlockFn", + "Server.WrapKeyFn", + "Server.IngestMailFn", + "Server.SwapModelFn", + "Server.ModelStatusFn", + "Server.DescribeImageFn", + "Server.CaptureStartFn" + ], + "reads": [], + "writes": [], + "calls": [ + "core.auth_gate", + "core.daemon_api" + ], + "called_by": [ + "bnd.ipc" + ], + "confidence": "high", + "status": "implemented", + "notes": "Eight function fields bypass CoreAPI entirely. Each is nil unless its config block exists, and nil means ErrUnknownMethod on the wire." + }, + { + "id": "core.auth_gate", + "type": "service", + "group": "mavend", + "responsibility": "The single authorization guard. Locked, it is a default-deny allowlist of three methods; unlocked, it is auth.Gate over Enrollment and PasskeySession.", + "files": [ + "internal/auth/gate.go", + "internal/auth/policy.go", + "internal/auth/tier.go", + "internal/auth/enrollment.go", + "cmd/mavend/main.go" + ], + "symbols": [ + "auth.Gate.Check", + "auth.Requirement", + "auth.Can", + "auth.MaxLayer", + "auth.Surface", + "auth.Layer", + "auth.NewFloorEnrollment", + "webauthn.NewPasskeySession", + "errLocked" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.ipc_server" + ], + "confidence": "high", + "status": "implemented", + "notes": "Two tier systems exist and this is the one the turn path does NOT read. FloorEnrollment maps every same-uid caller to SurfaceCoreProcess/L3, and no file in cmd/mavend reads req.Surface or Session.Surface. The live act gate is core.risk_policy in internal/tool." + }, + { + "id": "core.daemon_lock", + "type": "service", + "group": "mavend", + "responsibility": "Cold-start unlock. When a wrapped key blob exists and no env key is set the daemon boots LOCKED, serves three methods, and wires everything else inside UnlockFn after a passkey assertion.", + "files": [ + "cmd/mavend/main.go", + "cmd/mavend/keyfile.go", + "internal/webauthn/" + ], + "symbols": [ + "daemonLock", + "daemonLock.unlock", + "daemonLock.closeStore", + "srv.UnlockFn", + "srv.WrapKeyFn", + "webauthn.UnwrapKey", + "webauthn.WrapKey", + "wrapKeyToFile", + "BlobV1" + ], + "reads": [ + "state.wrapped_key" + ], + "writes": [ + "state.wrapped_key", + "state.db_file" + ], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "The whole daemon is wired twice, in two places, minutes or days apart. boot.go exists because those two lists had already drifted (V-639)." + }, + { + "id": "core.daemon_api", + "type": "adapter", + "group": "mavend", + "responsibility": "The daemon's CoreAPI: the store adapter plus eight closures over the tick loop, the event bus, the decision ring, the voice handler and the Nexus client.", + "files": [ + "cmd/mavend/tick_api.go", + "cmd/mavend/boot.go" + ], + "symbols": [ + "daemonAPI", + "newDaemonAPI", + "bootDeps", + "daemonAPI.Chat", + "daemonAPI.TickTrace", + "daemonAPI.DayPlan", + "daemonAPI.ResolveEntity", + "daemonAPI.RecentEvents", + "daemonAPI.TurnDecisions" + ], + "reads": [], + "writes": [], + "calls": [ + "core.store_api", + "core.tick_loop", + "core.reactive_handler", + "core.event_bus", + "ext.nexus" + ], + "called_by": [ + "core.ipc_server" + ], + "confidence": "high", + "status": "implemented", + "notes": "daemonAPI.chatFn is the voice handler; the handler's own api field is back-patched to daemonAPI by upgradeAPI. A deliberate two-way back-patch, documented on both sides." + }, + { + "id": "core.store_api", + "type": "adapter", + "group": "mavend", + "responsibility": "The plain CoreAPI over the store. Every module method that is a state operation lands here.", + "files": [ + "internal/ipc/storeapi.go", + "internal/ipc/coreapi.go", + "internal/ipc/api.go", + "internal/ipc/unimplemented.go" + ], + "symbols": [ + "ipc.NewStoreAPI", + "ipc.CoreAPI", + "ipc.UnimplementedCoreAPI" + ], + "reads": [], + "writes": [], + "calls": [ + "state.db" + ], + "called_by": [ + "core.daemon_api", + "core.intake_api" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.intake_api", + "type": "adapter", + "group": "mavend", + "responsibility": "A decorator over CoreAPI that publishes one envelope per successful intake write (WriteFact, WriteNote, CaptureTask) into the in-memory journal.", + "files": [ + "cmd/mavend/intake.go" + ], + "symbols": [ + "newIntakeAPI", + "intakeAPI", + "newEventBus", + "intakeEventsFn" + ], + "reads": [], + "writes": [], + "calls": [ + "core.store_api", + "core.event_bus" + ], + "called_by": [ + "core.daemon_api" + ], + "confidence": "high", + "status": "implemented", + "notes": "cmd/mavend/mail.go reaches past the decorator to st.CaptureTask and publishes by hand. That exception is stated in intake.go." + }, + { + "id": "core.event_bus", + "type": "shared-state", + "group": "mavend", + "responsibility": "Bounded in-memory intake journal. Read only by /events and recent_events; nothing Maven says depends on it, and it dispatches nothing.", + "files": [ + "internal/event/bus.go", + "internal/event/event.go" + ], + "symbols": [ + "event.Bus", + "Bus.Publish", + "Bus.Recent", + "Bus.Subscribe", + "Event.Normalize" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.intake_api", + "core.daemon_api" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.voice_server", + "type": "service", + "group": "mavend", + "responsibility": "The voice TCP listener and session registry. One goroutine per conn; the same conn carries request/response and server-initiated pushes, serialised by the per-session mutex.", + "files": [ + "internal/voice/server.go", + "internal/voice/session.go", + "cmd/mavend/voicewire.go" + ], + "symbols": [ + "voice.NewServer", + "voice.Server.Serve", + "voice.Sessions", + "voice.Handler" + ], + "reads": [], + "writes": [], + "calls": [ + "core.reactive_handler" + ], + "called_by": [ + "bnd.voice_tcp" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.wiring", + "type": "service", + "group": "mavend", + "responsibility": "wireVoice: builds the stt and tts seams, the embedder, the routing heads, the tool executor, MCP, the house, the LAN scanner, the weather provider, the model seam, the router cascade, the sessions registry, the voice sink, memory, dialogue, the replier, the handler and the TCP listener.", + "files": [ + "cmd/mavend/voicewire.go" + ], + "symbols": [ + "wireVoice", + "voiceWiring", + "buildRouter", + "modelSeam", + "sttSeam", + "pickLLMRouter", + "seedClassifier", + "seedTools", + "repairFactVectors", + "checkStoredEmbedder", + "runReembed" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "implemented", + "notes": "One 270-line function that constructs seventeen subsystems and returns a struct the rest of the daemon reads fields off." + }, + { + "id": "core.reactive_handler", + "type": "service", + "group": "mavend", + "responsibility": "The turn pipeline shared by every reach. Holds stt, tts, the router, the CoreAPI, the tool executor and matcher, the phraser, the replier, recall, the crawler, search, Kiwix, the house, the LAN scanner, the weather provider, the raw store, the time parser, dialogue and clarify state, the decision ring, the trace sink, the ecosystem clients and five pieces of per-turn mutable state.", + "files": [ + "cmd/mavend/voice.go", + "cmd/mavend/voicewire.go" + ], + "symbols": [ + "reactiveHandler", + "runTurn", + "HandlePushToTalk", + "handleText", + "applyAction", + "upgradeAPI", + "reply", + "chatHistory", + "turnSource", + "sourceVoice", + "sourceText" + ], + "reads": [], + "writes": [], + "calls": [ + "core.turn_route", + "core.preroute", + "core.action_table", + "core.replier", + "core.stt_seam", + "core.tts_seam" + ], + "called_by": [ + "core.voice_server", + "core.daemon_api", + "core.telegram_intake" + ], + "confidence": "high", + "status": "implemented", + "notes": "34 fields. It is the single junction of routing, memory, tools, ecosystem, search, the house, the LAN and dialogue state." + }, + { + "id": "core.turn_route", + "type": "arbitration", + "group": "mavend", + "responsibility": "This turn's routing, memoised with sync.Once and carried on the context so the clarify resolver and the action pipeline act on one decision. Also decides an elliptical follow-up from the previous turn instead of routing it.", + "files": [ + "cmd/mavend/turnroute.go", + "cmd/mavend/continuation.go", + "cmd/mavend/turnrole.go" + ], + "symbols": [ + "turnRoute", + "turnRoute.resolve", + "withTurnRoute", + "turnRouteFrom", + "routeForRole", + "needsRoute", + "continuationDecision" + ], + "reads": [], + "writes": [], + "calls": [ + "router.cascade" + ], + "called_by": [ + "core.reactive_handler", + "core.preroute" + ], + "confidence": "high", + "status": "implemented", + "notes": "Exists because arbitration was inverted in V-560: routing twice cost a second on the resident model and could disagree with itself." + }, + { + "id": "core.preroute", + "type": "arbitration", + "group": "mavend", + "responsibility": "The ordered ladder of stateful pre-emptors that may claim a turn before routing: expired-clarify notice, confirm answer, targeted repair, untargeted repair, command prohibition, clarify answer, quiet toggle, snooze, ack, reminder cancellation, ordinal selection.", + "files": [ + "cmd/mavend/voice.go", + "cmd/mavend/confirm.go", + "cmd/mavend/repair.go", + "cmd/mavend/clarify.go", + "cmd/mavend/command_prohibition.go", + "cmd/mavend/quiet_toggle.go", + "cmd/mavend/snooze.go", + "cmd/mavend/ack.go", + "cmd/mavend/reminder_cancel.go", + "cmd/mavend/ordinal.go", + "cmd/mavend/decisiontrace.go" + ], + "symbols": [ + "resolveConfirm", + "resolveRepair", + "resolveUntargetedRepair", + "resolveCommandProhibition", + "resolveClarifyAnswer", + "resolveQuietToggle", + "resolveSnooze", + "resolveAck", + "resolveReminderCancellation", + "resolveCandidate", + "preRouteLadder", + "notePreRoute", + "clarifyExpiredNotice", + "withNotice", + "withResumed" + ], + "reads": [], + "writes": [], + "calls": [ + "state.clarify_store", + "state.pending_act", + "state.dialogue_sessions" + ], + "called_by": [ + "core.reactive_handler" + ], + "confidence": "high", + "status": "implemented", + "notes": "Eleven rungs, order argued step by step in runTurn. A new rung must also appear in preRouteLadder or it is missing from the decision record." + }, + { + "id": "core.action_table", + "type": "arbitration", + "group": "mavend", + "responsibility": "Per-intent dispatch. Seven entries, one per router intent; a handler returning the empty string defers to the replier.", + "files": [ + "cmd/mavend/actions.go" + ], + "symbols": [ + "actionHandlers", + "applyAction" + ], + "reads": [], + "writes": [], + "calls": [ + "core.action_fact", + "core.action_reminder", + "core.action_act", + "core.action_note", + "core.action_chat", + "core.action_system", + "core.query_chain" + ], + "called_by": [ + "core.reactive_handler" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.action_fact", + "type": "handler", + "group": "mavend", + "responsibility": "IntentFact: refuse question-shaped and complaint utterances, write the fact, prune and re-insert its recall vector, feed pattern detection.", + "files": [ + "cmd/mavend/actions_fact.go", + "cmd/mavend/factgate.go", + "cmd/mavend/patterns.go" + ], + "symbols": [ + "actionFact", + "factConfidence", + "pruneFactVectors", + "store.FactRecallText", + "router.IsQuestionShaped", + "router.IsTransientComplaint" + ], + "reads": [], + "writes": [ + "state.facts", + "state.memory_vectors" + ], + "calls": [ + "core.recall", + "core.query_chain", + "core.action_chat" + ], + "called_by": [ + "core.action_table" + ], + "confidence": "high", + "status": "implemented", + "notes": "Re-routes a question-shaped fact into actionQuery and a complaint into actionChat, so a fact handler can return a query answer or a chat answer." + }, + { + "id": "core.action_reminder", + "type": "handler", + "group": "mavend", + "responsibility": "IntentReminder: fall back to the time parser for stage-0 matches, create the row, and confirm from the stored fire time rather than from the utterance.", + "files": [ + "cmd/mavend/actions_reminder.go", + "cmd/mavend/reminderwhen.go", + "cmd/mavend/reminderbody.go" + ], + "symbols": [ + "actionReminder", + "reminderConfirm", + "reminderBody", + "router.ResolvedTheHour", + "refusesCommand" + ], + "reads": [], + "writes": [ + "state.reminders" + ], + "calls": [], + "called_by": [ + "core.action_table" + ], + "confidence": "high", + "status": "implemented", + "notes": "Never sets Cron. store.Reminder and ipc.CreateReminder both carry a Cron field and no spoken path writes it." + }, + { + "id": "core.action_act", + "type": "handler", + "group": "mavend", + "responsibility": "IntentAct: the enabled-tool allowlist, the destructive confirm gate, the Hexis capability path, and entity resolution in front of any mutation.", + "files": [ + "cmd/mavend/actions_act.go", + "cmd/mavend/confirm.go", + "cmd/mavend/ecosystem_acts.go", + "cmd/mavend/entityname.go", + "internal/tool/" + ], + "symbols": [ + "actionAct", + "pendingAct", + "resolveConfirm", + "tool.Executor", + "tool.Matcher", + "handleHexisAct", + "pendingHexisExec", + "hexisBeforeClarify" + ], + "reads": [ + "state.tools" + ], + "writes": [], + "calls": [ + "ext.hexis", + "ext.homeassistant", + "ext.vikunja_mcp" + ], + "called_by": [ + "core.action_table" + ], + "confidence": "high", + "status": "implemented", + "notes": "Three gates, none of them the caller's surface: the enabled allowlist, the risk tier (core.risk_policy) and the confirm turn. A Hexis confirmation binds capability id, target entity, arguments, requester and expiry. Filling in a clarified argument never grants authority." + }, + { + "id": "core.ecosystem_hexis_gate", + "type": "handler", + "group": "mavend", + "responsibility": "The Hexis capability act path. Resolves the entity through Nexus, reads the tier Hexis declares rather than deriving a second opinion, and parks a mutating capability for a confirm bound to capability, target and expiry.", + "files": [ + "cmd/mavend/ecosystem_acts.go" + ], + "symbols": [ + "handleHexisAct", + "execHexis", + "pendingHexisExec", + "hexisBeforeClarify" + ], + "reads": [], + "writes": [], + "calls": [ + "ext.hexis", + "ext.nexus" + ], + "called_by": [ + "core.action_act" + ], + "confidence": "high", + "status": "implemented", + "notes": "The second act path. It reuses internal/tool's policy deliberately (ecosystem_acts.go:768), so local rows and Hexis capabilities share one tier vocabulary. Praxis does not." + }, + { + "id": "core.praxis_acts", + "type": "handler", + "group": "mavend", + "responsibility": "The Praxis item-lifecycle mutations: acknowledge, resolve, ignore, pin. Reached from actionAct before the local executor, resolved against the last read-out list for an ordinal, and executed directly.", + "files": [ + "cmd/mavend/ecosystem_acts.go" + ], + "symbols": [ + "handlePraxisAct", + "praxisItemAction", + "praxisItemAction.handle", + "praxisCapabilities", + "resolveSurfacedPosition", + "rememberSurfaced" + ], + "reads": [ + "state.surfaced_items" + ], + "writes": [], + "calls": [ + "ext.praxis" + ], + "called_by": [ + "core.action_act" + ], + "confidence": "high", + "status": "implemented", + "notes": "The third act path, and the one with NO risk tier and NO confirm turn: handle() calls a.call(ctx, px, id) directly (ecosystem_acts.go:158). A remote mutation that runs on first hearing. Reversible on the Praxis side, which is a reason nothing in the code states." + }, + { + "id": "core.risk_policy", + "type": "service", + "group": "mavend", + "responsibility": "The tier system that actually gates an act. RiskOf sorts a tool row into safe, destructive or irreversible; PolicyFor maps the tier to a confirm requirement and a VoiceMayRun flag. Unknown shapes default upward to destructive.", + "files": [ + "internal/tool/risk.go", + "internal/tool/tool.go" + ], + "symbols": [ + "Risk", + "TierSafe", + "TierDestructive", + "TierIrreversible", + "RiskOf", + "RiskOfCapability", + "PolicyFor", + "Policy.Confirm", + "Policy.VoiceMayRun", + "irreversibleVerbs", + "Executor.Exec" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.action_act" + ], + "confidence": "high", + "status": "implemented", + "notes": "VoiceMayRun is NOT conditioned on the reach: Executor.Exec takes (ctx, name, args, confirmed) and no surface. It also has no proof the boolean was bound correctly: that invariant lives in pendingAct and resolveConfirm, and Exec trusts it because only two callers exist. Covers two of the three act paths (local rows, and Hexis via RiskOfCapability at ecosystem_acts.go:768); the Praxis lifecycle path bypasses it entirely. `systemctl reboot` is TierDestructive, not TierIrreversible, so it runs after one spoken confirm. internal/tool/risk.go:84 states it is not a sandbox." + }, + { + "id": "core.action_note", + "type": "handler", + "group": "mavend", + "responsibility": "IntentNote: write the note and index it for recall.", + "files": [ + "cmd/mavend/actions_note.go", + "cmd/mavend/notefragment.go" + ], + "symbols": [ + "actionNote" + ], + "reads": [], + "writes": [ + "state.notes", + "state.memory_vectors" + ], + "calls": [], + "called_by": [ + "core.action_table" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.action_chat", + "type": "handler", + "group": "mavend", + "responsibility": "IntentChat: build history from the dialogue session and let the phraser answer from general knowledge plus the context block.", + "files": [ + "cmd/mavend/actions.go" + ], + "symbols": [ + "actionChat", + "chatHistory", + "phraser.PhraseChat", + "phraser.ChatFallback" + ], + "reads": [], + "writes": [], + "calls": [ + "core.phraser" + ], + "called_by": [ + "core.action_table", + "core.action_fact" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.action_system", + "type": "handler", + "group": "mavend", + "responsibility": "IntentSystem: clock, date and system-observable answers, read off the handler's own clock and keyword tests.", + "files": [ + "cmd/mavend/voice.go", + "cmd/mavend/actions.go" + ], + "symbols": [ + "actionSystem", + "replySystem" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.action_table" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "router.cascade", + "type": "arbitration", + "group": "mavend", + "responsibility": "The routing cascade. Stage 0 grammars win outright at confidence 1.0; then the routing heads; then the LLM router; then the classifier with slot extraction and a confidence gate. Every stage may decline and the next one answers.", + "files": [ + "internal/router/router.go", + "internal/router/intent.go", + "internal/router/source.go", + "internal/router/slots.go" + ], + "symbols": [ + "router.Router", + "router.New", + "Router.Route", + "router.Config", + "Decision", + "Intent", + "Source", + "Sources", + "ValidSource", + "gateLLMDecision", + "fillSlots", + "CorrectMisroute", + "SourceAnchored" + ], + "reads": [], + "writes": [], + "calls": [ + "router.stage0", + "router.heads", + "router.llm", + "router.classifier", + "router.extractor" + ], + "called_by": [ + "core.turn_route" + ], + "confidence": "high", + "status": "implemented", + "notes": "A route produces TWO decisions: Intent (one of seven) and Source (one of twelve, read on IntentQuery alone). SourceAnchored is set only by a stage 0 grammar." + }, + { + "id": "router.stage0", + "type": "arbitration", + "group": "mavend", + "responsibility": "The deterministic grammar set, in the daemon's order. First match wins. The order is the contract and each rule's comment argues its position.", + "files": [ + "internal/router/stagezero.go", + "internal/router/stage0.go", + "internal/router/agendaq.go", + "internal/router/worldquery.go", + "internal/router/praxis.go", + "internal/router/task.go", + "internal/router/commandframe.go", + "internal/router/barecapture.go", + "internal/router/possession.go", + "internal/router/list.go", + "internal/router/feeds.go", + "internal/router/help.go", + "internal/router/implicitquery.go", + "internal/router/fragment.go", + "internal/router/reminderreport.go", + "internal/router/taskstatus.go", + "internal/router/knowledge.go" + ], + "symbols": [ + "StageZeroGrammars", + "Grammar", + "Grammar.Evaluate", + "DefaultGrammars", + "CommandProhibitionGrammar", + "AmbiguousFragmentGrammar", + "SystemTimeDateGrammars", + "AgendaQueryGrammars", + "ImplicitElapsedQueryGrammar", + "WorldQueryGrammars", + "MavenHelpGrammar", + "FeedQueryGrammar", + "TaskListGrammar", + "ListGrammars", + "ReminderGrammar", + "ReminderCancellationReportGrammar", + "PraxisGrammars", + "TaskStatusGrammar", + "BareCaptureGrammar", + "TaskCaptureGrammar", + "NarrativeQueryGrammars", + "PossessionStatementGrammar", + "StripWakeToken" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "router.cascade", + "proc.labelgen", + "eval.router" + ], + "confidence": "high", + "status": "implemented", + "notes": "One list, called by both buildRouter and the eval fixture, because the two copies had already drifted (V-693). PraxisGrammars is the only path to Praxis." + }, + { + "id": "router.heads", + "type": "model", + "group": "mavend", + "responsibility": "Routing heads: a softmax over the label set, run on a fine-tuned copy of the e5-small encoder. Runs after stage 0 and before the model. Declines below its own threshold.", + "files": [ + "internal/router/heads.go", + "internal/router/onnxruntime.go", + "deploy/mavend.json" + ], + "symbols": [ + "RouterHeads", + "NewRouterHeads", + "RouterHeads.Route" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "router.cascade" + ], + "confidence": "high", + "status": "implemented", + "notes": "Configured at voice.embedder.heads_path. A missing or broken weights file logs and leaves the field nil, which is the pre-V-664 cascade exactly. heads_path must never equal model_path; that is refused at config load." + }, + { + "id": "router.llm", + "type": "model", + "group": "mavend", + "responsibility": "The LLM router. A grammar-constrained call to the resident model that names an intent and a destination. Any error or parse failure falls through to the classifier.", + "files": [ + "internal/router/llmrouter.go", + "internal/router/currentversion.go", + "cmd/mavend/voicewire.go" + ], + "symbols": [ + "LLMRouter", + "LLMRouter.Route", + "Completer", + "pickLLMRouter" + ], + "reads": [], + "writes": [], + "calls": [ + "svc.llama_server" + ], + "called_by": [ + "router.cascade" + ], + "confidence": "high", + "status": "implemented", + "notes": "voice.llm_router = true in the deployed config. gateLLMDecision thins a structurally incomplete route rather than trusting it." + }, + { + "id": "router.classifier", + "type": "model", + "group": "mavend", + "responsibility": "The nearest-centroid classifier over the embedder. The floor: it answers when the resident model is off, absent or erroring, and it names no destination.", + "files": [ + "internal/router/classifier.go", + "internal/router/embedder.go", + "cmd/mavend/voicewire.go", + "cmd/mavend/seed.go", + "models/seeds/" + ], + "symbols": [ + "Classifier", + "Classifier.Classify", + "seedClassifier", + "loadSeedFile", + "ErrNoIntents", + "seedDir" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "router.cascade" + ], + "confidence": "high", + "status": "implemented", + "notes": "Seeded from models/seeds at boot. It sets no Source, so a box whose model is down walks the whole query chain in table order." + }, + { + "id": "router.embedder", + "type": "model", + "group": "mavend", + "responsibility": "The ONNX multilingual-e5-small encoder, or the HashEmbedder floor when no embedder block is configured. Backs the classifier, recall, topics and the trace encoder id.", + "files": [ + "internal/router/onnxembedder.go", + "internal/router/embedder.go", + "deploy/mavend.json" + ], + "symbols": [ + "NewONNXEmbedder", + "NewHashEmbedder", + "EmbedQuery", + "EmbedPassage", + "EmbedderID", + "tokenizerRev" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "router.classifier", + "core.recall", + "core.feed_worker", + "core.crawl_worker", + "core.topics" + ], + "confidence": "high", + "status": "implemented", + "notes": "EmbedQuery and EmbedPassage apply the query:/passage: prefixes the model was trained with. Calling plain Embed on a note is a bug." + }, + { + "id": "router.extractor", + "type": "service", + "group": "mavend", + "responsibility": "Stage 2 slot extraction: time, act target, fact key and value, ordinals, number words, URLs.", + "files": [ + "internal/router/slots.go", + "internal/router/dateparser.go", + "internal/router/acttarget.go", + "internal/router/timementions.go", + "internal/router/numwords.go", + "internal/router/halfpast.go", + "internal/router/url.go", + "internal/router/remindersubject.go" + ], + "symbols": [ + "Extractor", + "Extractor.Extract", + "DateTimeParser", + "NewPythonDateParser", + "DefaultFactParser", + "ResolvedTheHour" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "router.cascade", + "core.preroute" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "router.claim", + "type": "planned", + "group": "mavend", + "responsibility": "A comparable unit of evidence for the many claimants that compete for one utterance: consumed span, unexplained remainder, band and veto reason.", + "files": [ + "internal/claim/claim.go", + "internal/router/claim.go", + "docs/plans/19-dialogue-arbitration.md" + ], + "symbols": [ + "claim.Claim", + "claim.Split", + "claim.Band", + "router.ClaimOf", + "bandOf", + "vetoOf", + "filledSlots" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "planned-unwired", + "notes": "Nothing calls ClaimOf. Its own doc comment says so. DEFECT: claimSpans includes Slots.Text unconditionally (internal/router/claim.go:38) while Router.fillSlots backfills the raw utterance into Text for note, query and chat (internal/router/router.go:334), so Claim.Coverage returns 1.0 for a claim that extracted nothing, and MoreSpecificThan reads coverage first. filledSlots in the same file guards against exactly this and claimSpans does not. All five cases in claim_test.go set Text == Utterance and assert Band only." + }, + { + "id": "router.modes", + "type": "planned", + "group": "mavend", + "responsibility": "An inventory of roughly thirty downstream behaviours mapped back to the seven public intents.", + "files": [ + "internal/modes/modes.go", + "internal/modes/modes_v1.json" + ], + "symbols": [ + "modes.Inventory", + "modes.Mode" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "planned-unwired", + "notes": "No file outside internal/modes imports it. Only its own test loads the JSON." + }, + { + "id": "core.query_chain", + "type": "arbitration", + "group": "mavend", + "responsibility": "The ordered chain of 22 query sources actionQuery walks. First source to claim answers the turn; queryWalk narrows the chain against the destination the cascade named.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/querysource.go" + ], + "symbols": [ + "querySources", + "querySource", + "queryWalk", + "actionQuery", + "queryTurn", + "noteQuerySource", + "querySourceNames", + "withQuerySourceSink" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.action_table", + "core.action_fact" + ], + "confidence": "high", + "status": "implemented", + "notes": "Table order IS the arbitration. queryWalk takes sources OUT and never moves one. Only sources marked guesses:true are droppable, and the personal boundary only when SourceAnchored was set by a literal stage 0 pattern (V-666)." + }, + { + "id": "core.q.factbykey", + "type": "query_source", + "group": "query-chain", + "responsibility": "Reads the current value for a key straight out of the fact store.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryFactByKey", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'fact-by-key', dest SourceRecall. " + }, + { + "id": "core.q.dayplan", + "type": "query_source", + "group": "query-chain", + "responsibility": "Assembles today's plan from the tick loop, not from a table.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryDayPlan", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'day-plan', dest SourceCalendar. " + }, + { + "id": "core.q.habits", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers a habit question from the behaviour profile over recorded events.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryHabits", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'habits', dest SourceCalendar. " + }, + { + "id": "core.q.tasks", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers from the task list.", + "files": [ + "cmd/mavend/actions_task.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryTasks", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'tasks', dest SourceTasks. " + }, + { + "id": "core.q.attention", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers from what Praxis says needs looking at.", + "files": [ + "cmd/mavend/attentionq.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryAttention", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'attention', dest SourceAttention. guesses:true — droppable when the cascade names another destination" + }, + { + "id": "core.q.list", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers from the shopping and other named lists.", + "files": [ + "cmd/mavend/actions_list.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryList", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'list', dest SourceList. guesses:true — droppable when the cascade names another destination" + }, + { + "id": "core.q.money", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers from the spending facts the poller wrote.", + "files": [ + "cmd/mavend/actions_money.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryMoney", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'money', dest SourceMoney. " + }, + { + "id": "core.q.history", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers 'what did I tell you' from the facts he tapped in.", + "files": [ + "cmd/mavend/historyq.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryHistory", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'history', dest SourceRecall. " + }, + { + "id": "core.q.feeds", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers from the RSS notes the feed worker wrote.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryFeeds", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'feeds', dest SourceFeeds. guesses:true — droppable when the cascade names another destination" + }, + { + "id": "core.q.home", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers from Home Assistant state.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryHome", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'home', dest SourceHome. guesses:true — droppable when the cascade names another destination" + }, + { + "id": "core.q.network", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers from a bounded LAN scan.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryNetwork", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'network', dest SourceNetwork. guesses:true — droppable when the cascade names another destination" + }, + { + "id": "core.q.calendar", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers from calendar events. The only date-aware source, so the only one a continuation may reach.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryCalendar", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'calendar', dest SourceCalendar. " + }, + { + "id": "core.q.weather", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers the forecast for a place.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryWeather", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'weather', dest SourceWeather. guesses:true — droppable when the cascade names another destination" + }, + { + "id": "core.q.self", + "type": "query_source", + "group": "query-chain", + "responsibility": "Answers a question about Maven herself, above the sources that search his data.", + "files": [ + "cmd/mavend/self.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "querySelf", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'self', dest SourceSelf. guesses:true — droppable when the cascade names another destination" + }, + { + "id": "core.q.embed", + "type": "query_source", + "group": "query-chain", + "responsibility": "Embeds the query once and fills the turn scratch for the passes below it.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryEmbed", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'embed', dest SourceRecall. " + }, + { + "id": "core.q.memory", + "type": "query_source", + "group": "query-chain", + "responsibility": "Vector search over notes and facts together.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryMemory", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'memory', dest SourceRecall. " + }, + { + "id": "core.q.notes", + "type": "query_source", + "group": "query-chain", + "responsibility": "The notes-only recall pass.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryNotes", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'notes', dest SourceRecall. " + }, + { + "id": "core.q.personal", + "type": "query_source", + "group": "query-chain", + "responsibility": "THE BOUNDARY. A question about him that got this far has no answer in his data, so the walk stops rather than letting the world guess.", + "files": [ + "cmd/mavend/personalboundary.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryPersonal", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'personal', dest SourceRecall. guesses:true — droppable when the cascade names another destination boundary:true — droppable only by an anchored stage 0 destination (V-666)" + }, + { + "id": "core.q.search", + "type": "query_source", + "group": "query-chain", + "responsibility": "SearXNG, the live web. Only the query string leaves the box.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "querySearch", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'search', dest SourceWorld. " + }, + { + "id": "core.q.kiwix", + "type": "query_source", + "group": "query-chain", + "responsibility": "The offline ZIMs, the fallback behind the live search.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryKiwix", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'kiwix', dest SourceWorld. " + }, + { + "id": "core.q.web", + "type": "query_source", + "group": "query-chain", + "responsibility": "Reads a page he named by URL. Claims only when a URL was spoken.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryWeb", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'web', dest SourceWorld. " + }, + { + "id": "core.q.general", + "type": "query_source", + "group": "query-chain", + "responsibility": "The resident model answering from its own weights. Last.", + "files": [ + "cmd/mavend/actions_query.go", + "cmd/mavend/actions_query.go" + ], + "symbols": [ + "queryGeneral", + "querySources" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain" + ], + "confidence": "high", + "status": "implemented", + "notes": "chain name 'general-knowledge', dest SourceWorld. " + }, + { + "id": "core.recall", + "type": "service", + "group": "mavend", + "responsibility": "The note-and-fact recall subsystem: the embedder, the vector store, and the two numbers that gate an answer.", + "files": [ + "cmd/mavend/recall.go", + "internal/memory/store.go", + "internal/memory/gate.go", + "deploy/mavend.json" + ], + "symbols": [ + "recallWiring", + "memory.Store", + "memory.Search", + "memory.Confident", + "memory.ConfidentScores", + "query_min_score", + "query_min_margin" + ], + "reads": [ + "state.memory_vectors" + ], + "writes": [ + "state.memory_vectors" + ], + "calls": [], + "called_by": [ + "core.q.memory", + "core.q.notes", + "core.q.embed", + "core.action_fact", + "core.action_note" + ], + "confidence": "high", + "status": "implemented", + "notes": "query_min_score 0.80, query_min_margin 0.008 in the deployed config." + }, + { + "id": "core.phraser", + "type": "service", + "group": "mavend", + "responsibility": "The phrasing seam. One parser for every model reply; a deterministic Stub when no phraser block is configured.", + "files": [ + "internal/phraser/phraser.go", + "internal/phraser/llmphraser.go", + "internal/phraser/parse.go", + "internal/phraser/prompts.go", + "internal/phraser/nudge_llm.go", + "internal/phraser/query.go", + "internal/phraser/world.go", + "internal/phraser/replier.go", + "internal/phraser/swap.go" + ], + "symbols": [ + "phraser.Phraser", + "phraser.NewStub", + "phraser.NewLLMPhraser", + "parseResponseMood", + "PhraseNudge", + "PhraseReminder", + "PhraseChat", + "LLMPhraser.UseRemote" + ], + "reads": [], + "writes": [], + "calls": [ + "svc.llama_server", + "core.model_seam" + ], + "called_by": [ + "core.action_chat", + "core.tick_loop", + "core.query_chain", + "core.mail_intake", + "core.memory_eval" + ], + "confidence": "high", + "status": "implemented", + "notes": "parseResponseMood is the one parser for model text; every phrasing path reaches it." + }, + { + "id": "core.replier", + "type": "service", + "group": "mavend", + "responsibility": "Phrases the reply across a router decision when the action handler returned nothing. LLM-backed when a completion seam exists, a deterministic stub otherwise.", + "files": [ + "cmd/mavend/replier_llm.go", + "internal/voice/replier.go" + ], + "symbols": [ + "voice.Replier", + "voice.NewStubReplier", + "newLLMReplier" + ], + "reads": [], + "writes": [], + "calls": [ + "core.model_seam" + ], + "called_by": [ + "core.reactive_handler" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.model_seam", + "type": "service", + "group": "mavend", + "responsibility": "The completion seam the hot paths use. Either an llm.Pair preferring the workstation model with a silent fallback to the resident model, or the resident client alone, or nothing.", + "files": [ + "cmd/mavend/voicewire.go", + "cmd/mavend/main.go", + "internal/llm/remote.go", + "internal/llm/client.go", + "internal/llm/gate.go", + "deploy/mavend.json" + ], + "symbols": [ + "modelSeam", + "llm.Pair", + "llm.NewPair", + "Pair.Available", + "Pair.Complete", + "llm.Gate", + "llmClientFor", + "cacheRAMMiB" + ], + "reads": [], + "writes": [], + "calls": [ + "svc.llama_server", + "proc.mavgpud" + ], + "called_by": [ + "router.llm", + "core.replier", + "core.phraser" + ], + "confidence": "high", + "status": "configured-off", + "notes": "deploy/mavend.json sets workstation.model_disabled = true, so the model arm is dark and only the STT arm of the workstation block is live. Deleting the block restores pre-workstation behaviour." + }, + { + "id": "core.stt_seam", + "type": "service", + "group": "mavend", + "responsibility": "Transcription. Remote to mavsttd when a socket is configured, an in-process Stub otherwise, with an optional stt.Pair preferring the workstation CW2 service.", + "files": [ + "cmd/mavend/voicewire.go", + "internal/stt/" + ], + "symbols": [ + "sttSeam", + "stt.Transcriber", + "stt.NewRemote", + "stt.NewStub", + "stt.Pair" + ], + "reads": [], + "writes": [], + "calls": [ + "proc.mavsttd", + "ext.cw2_stt" + ], + "called_by": [ + "core.reactive_handler" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.tts_seam", + "type": "service", + "group": "mavend", + "responsibility": "Synthesis. Remote to mavttsd when a socket is configured, an in-process Stub otherwise.", + "files": [ + "cmd/mavend/voicewire.go", + "internal/tts/", + "internal/ttsnorm/" + ], + "symbols": [ + "tts.Synthesizer", + "tts.NewRemote", + "tts.NewStub", + "ttsnorm" + ], + "reads": [], + "writes": [], + "calls": [ + "proc.mavttsd" + ], + "called_by": [ + "core.reactive_handler", + "core.sink_voice" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.ecosystem", + "type": "adapter", + "group": "mavend", + "responsibility": "The three ecosystem clients and the shared JSON transport. All HTTP; no component reads another's database.", + "files": [ + "cmd/mavend/ecosystem.go", + "cmd/mavend/ecosystem_acts.go" + ], + "symbols": [ + "wireEcosystem", + "ecosystemWiring", + "nexusClient", + "praxisClient", + "hexisclient", + "resolveEntityReference", + "newCorrelationID" + ], + "reads": [], + "writes": [ + "state.ecosystem_traces" + ], + "calls": [ + "ext.nexus", + "ext.praxis", + "ext.hexis" + ], + "called_by": [ + "core.reactive_handler", + "core.fact_enrichment", + "core.daemon_api" + ], + "confidence": "high", + "status": "implemented", + "notes": "All three are nil unless configured and each degrades alone. An outage means a named gap, never a guess." + }, + { + "id": "core.decision_trace", + "type": "service", + "group": "mavend", + "responsibility": "The per-turn arbitration record: who claimed the turn, who lost it, who was never asked. Written to a bounded in-memory ring and, since V-629, persisted for fitting the routing heads.", + "files": [ + "internal/decision/", + "cmd/mavend/decisiontrace.go", + "cmd/mavend/routingtrace.go" + ], + "symbols": [ + "decision.Record", + "decision.Ring", + "decision.With", + "decision.Note", + "decision.Expect", + "decision.Claim", + "preRouteLadder", + "traceSink", + "pruneTracesOnStart", + "persistDecision" + ], + "reads": [], + "writes": [ + "state.routing_traces", + "state.decision_ring" + ], + "calls": [], + "called_by": [ + "core.reactive_handler", + "router.cascade" + ], + "confidence": "high", + "status": "implemented", + "notes": "Retained 14 days, enforced on write and again on start." + }, + { + "id": "core.topics", + "type": "service", + "group": "mavend", + "responsibility": "Open-set Russian topic matching with the embedder, one of the three sanctioned word-matching mechanisms.", + "files": [ + "cmd/mavend/topics.go", + "cmd/mavend/ruwords.go", + "internal/lexicon/", + "internal/morph/" + ], + "symbols": [ + "topics", + "lexicon", + "morph" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.query_chain", + "core.preroute" + ], + "confidence": "high", + "status": "implemented", + "notes": "Russian words are matched by internal/lexicon (closed classes), internal/morph (grammar) and topics.go with the embedder (open sets), and by no fourth mechanism." + }, + { + "id": "core.tick_loop", + "type": "worker", + "group": "mavend", + "responsibility": "The proactive driver. One pass per tick: gather state, pick at most one nudge candidate, queue or phrase and dispatch it, flush the digest, fire operator routines, accepted routines and morning routines, detect patterns, deliver due reminders, and repeat un-acked sev4 telegram alarms.", + "files": [ + "cmd/mavend/tick.go", + "cmd/mavend/tick_digest.go", + "cmd/mavend/tick_routines.go", + "cmd/mavend/tick_morning.go", + "cmd/mavend/tick_api.go" + ], + "symbols": [ + "tickLoop", + "newTickLoop", + "tickLoop.run", + "tickLoop.tick", + "deliverReminder", + "savePresence", + "maybeFlush", + "flushDigest", + "enqueueSuppressedDigest", + "fireRoutines", + "fireAcceptedRoutines", + "fireMorningRoutines", + "detectPatterns", + "stopFinishedAlarms", + "tune", + "cachePhrase" + ], + "reads": [ + "state.facts", + "state.reminders", + "state.nudges", + "state.digest_entries", + "state.proposed_routines", + "state.events", + "state.presence_state" + ], + "writes": [ + "state.nudges", + "state.reminders", + "state.presence_state", + "state.digest_entries", + "state.facts", + "state.proposed_routines", + "state.delivery_attempts" + ], + "calls": [ + "core.gatherer", + "core.rules", + "core.phraser", + "core.dispatcher", + "core.pattern" + ], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "implemented", + "notes": "Thirteen distinct jobs in one tick function on one 60s ticker, plus a second slower autotune ticker." + }, + { + "id": "core.gatherer", + "type": "service", + "group": "mavend", + "responsibility": "Builds the tick's consistent state snapshot out of the store: facts, presence, quiet hours, calendar busy, due reminders, collapsed reminder groups.", + "files": [ + "internal/loop/gather.go", + "internal/loop/state.go" + ], + "symbols": [ + "loop.Gatherer", + "NewGatherer", + "GatherState", + "SetQuietHours", + "loop.State", + "State.Since", + "State.FactsUnder", + "inQuietWindow", + "collapseReminders" + ], + "reads": [ + "state.facts", + "state.reminders", + "state.nudges", + "state.presence_state" + ], + "writes": [], + "calls": [], + "called_by": [ + "core.tick_loop" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.rules", + "type": "service", + "group": "mavend", + "responsibility": "The pure nudge rule set and the restraint gate. Rules are data; Gate and ExplainGate are pure functions of state.", + "files": [ + "internal/loop/rules.go", + "internal/loop/loop.go", + "internal/loop/explain.go", + "internal/loop/feedback.go", + "internal/loop/digest_identity.go" + ], + "symbols": [ + "loop.Rule", + "DefaultRules", + "RulesExcept", + "WaterRule", + "MealRule", + "BreakRule", + "ServiceDownRule", + "NetdataCriticalRule", + "loop.Gate", + "ExplainGate", + "ExplainTick", + "Tick", + "Severity", + "RemindDecisions", + "TuneCooldown", + "DigestEligible" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.tick_loop", + "core.gatherer" + ], + "confidence": "high", + "status": "implemented", + "notes": "disabled_rules is empty in the deployed config, so all five rules are live." + }, + { + "id": "core.pattern", + "type": "service", + "group": "mavend", + "responsibility": "Detects a stable recurring action+object pair in the events table and proposes a routine for it.", + "files": [ + "internal/pattern/detector.go", + "cmd/mavend/patterns.go", + "cmd/mavend/tick_routines.go", + "deploy/mavend.json" + ], + "symbols": [ + "pattern.Detect", + "pattern.ProposedRoutine", + "detectPatterns", + "announceProposal" + ], + "reads": [ + "state.events" + ], + "writes": [ + "state.proposed_routines" + ], + "calls": [], + "called_by": [ + "core.tick_loop" + ], + "confidence": "high", + "status": "implemented", + "notes": "pattern_proposals.notify = false in the deployed config: detect silently, never announce." + }, + { + "id": "core.morning", + "type": "service", + "group": "mavend", + "responsibility": "Daily checklists. An item is done when its fact_key gets a non-voided fact inside the window; the nudge fires once at the end of the window and only for what is still open.", + "files": [ + "internal/morning/", + "cmd/mavend/tick_morning.go", + "deploy/mavend.json" + ], + "symbols": [ + "morning.Routine", + "morning.Candidate", + "fireMorningRoutines", + "gatherMorningFacts", + "morningStatus", + "dayPlan" + ], + "reads": [ + "state.facts" + ], + "writes": [], + "calls": [], + "called_by": [ + "core.tick_loop" + ], + "confidence": "high", + "status": "implemented", + "notes": "One routine configured: medicine, water, pets, 08:00-11:00, nudge at 10:30." + }, + { + "id": "core.routines", + "type": "service", + "group": "mavend", + "responsibility": "Operator-declared and user-accepted scheduled behaviours, fired through the normal delivery routing. Bodies are literal operator text, never model-phrased.", + "files": [ + "internal/routine/", + "cmd/mavend/tick_routines.go" + ], + "symbols": [ + "routine.Routine", + "routine.Due", + "routinesFromConfig", + "fireRoutines", + "fireAcceptedRoutines" + ], + "reads": [ + "state.proposed_routines" + ], + "writes": [], + "calls": [], + "called_by": [ + "core.tick_loop" + ], + "confidence": "high", + "status": "implemented", + "notes": "The deployed config declares no `routines` block, so only accepted proposals can fire." + }, + { + "id": "core.dispatcher", + "type": "service", + "group": "mavend", + "responsibility": "Delivery fan-out. A pure routing table over (severity, presence) picks channels; the dispatcher holds the sinks, the durable outbox and the nudge recorder.", + "files": [ + "internal/delivery/dispatcher.go", + "internal/delivery/channel.go", + "internal/delivery/sink.go", + "internal/delivery/ack.go", + "internal/delivery/sendable.go", + "cmd/mavend/main.go" + ], + "symbols": [ + "delivery.NewDispatcher", + "delivery.Config", + "ChannelsFor", + "Channel", + "DispatchNudge", + "RepeatUnacked", + "beginOutbox", + "completeOutbox", + "wireDispatcher", + "NudgeRecorder", + "Outbox", + "ErrVoiceNoSession", + "ErrPermanent" + ], + "reads": [], + "writes": [ + "state.nudges", + "state.delivery_attempts", + "state.ack_sends" + ], + "calls": [ + "core.sink_voice", + "core.sink_ntfy", + "core.sink_telegram" + ], + "called_by": [ + "core.tick_loop" + ], + "confidence": "high", + "status": "implemented", + "notes": "Double authority is intentional: the gate decides whether a rule EMITS, delivery decides where it LANDS. Reminders bypass the restraint gate." + }, + { + "id": "core.sink_voice", + "type": "adapter", + "group": "mavend", + "responsibility": "Pushes a spoken nudge to the most recently active voice session through the same conn that serves requests.", + "files": [ + "internal/delivery/voicesink/", + "cmd/mavend/voicewire.go" + ], + "symbols": [ + "voicesink.New", + "voicesink.Sink", + "Sessions.PushToMostRecent" + ], + "reads": [], + "writes": [], + "calls": [ + "core.tts_seam", + "core.voice_server" + ], + "called_by": [ + "core.dispatcher" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.sink_ntfy", + "type": "adapter", + "group": "mavend", + "responsibility": "The ntfy push reach.", + "files": [ + "internal/delivery/ntfysink/" + ], + "symbols": [ + "ntfysink.New" + ], + "reads": [], + "writes": [], + "calls": [ + "ext.ntfy" + ], + "called_by": [ + "core.dispatcher" + ], + "confidence": "high", + "status": "configured-off", + "notes": "nil in the deployed dispatcher because the config block says disabled." + }, + { + "id": "core.sink_telegram", + "type": "adapter", + "group": "mavend", + "responsibility": "The telegram away reach, and the sev4 repeat-til-ack channel.", + "files": [ + "internal/delivery/telegramsink/telegramsink.go", + "internal/delivery/telegramsink/botapi.go" + ], + "symbols": [ + "telegramsink.New", + "telegramsink.Sink" + ], + "reads": [], + "writes": [], + "calls": [ + "ext.telegram" + ], + "called_by": [ + "core.dispatcher" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.telegram_intake", + "type": "worker", + "group": "mavend", + "responsibility": "Long-polls telegram getUpdates from exactly one chat and runs each message as a text turn, offering the same correction gesture the web does.", + "files": [ + "cmd/mavend/telegramintake.go", + "internal/delivery/telegramsink/intake.go", + "internal/delivery/telegramsink/correction.go" + ], + "symbols": [ + "wireTelegramIntake", + "chatTurnFn", + "telegramsink.NewPoller", + "Poller.Run" + ], + "reads": [], + "writes": [], + "calls": [ + "core.daemon_api", + "core.reactive_handler" + ], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "implemented", + "notes": "Started with wg.Add plus a bare goroutine rather than through goWorker or backgroundWorkers, so it is absent from the assertable worker set." + }, + { + "id": "core.fact_enrichment", + "type": "worker", + "group": "mavend", + "responsibility": "Resolves a fact's free-text Subject to a canonical Nexus entity id, with per-fact backoff.", + "files": [ + "cmd/mavend/factenrichment.go" + ], + "symbols": [ + "factEnrichmentWorker", + "newFactEnrichmentWorker", + "tick", + "resolveOne", + "enrichmentBackoff", + "forgetDeparted" + ], + "reads": [ + "state.facts" + ], + "writes": [ + "state.facts" + ], + "calls": [ + "ext.nexus" + ], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "core.memory_eval", + "type": "worker", + "group": "mavend", + "responsibility": "Scores recall quality against a stored fixture using the phraser.", + "files": [ + "cmd/mavend/memoryeval.go", + "internal/memeval/", + "internal/memory/recalleval/" + ], + "symbols": [ + "memoryEvalWorker", + "newMemoryEvalWorker" + ], + "reads": [], + "writes": [], + "calls": [ + "core.phraser" + ], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "configured-off", + "notes": "nil unless configured; the deployed config has no block for it." + }, + { + "id": "core.feed_worker", + "type": "worker", + "group": "mavend", + "responsibility": "Polls RSS sources and writes each item as a note with source rss:, embedding it for recall.", + "files": [ + "cmd/mavend/feeds.go", + "internal/rss/poller.go", + "deploy/mavend.json" + ], + "symbols": [ + "feedWorker", + "newFeedWorker", + "rss.Poller" + ], + "reads": [], + "writes": [ + "state.notes", + "state.memory_vectors", + "state.facts" + ], + "calls": [ + "router.embedder", + "core.intake_api" + ], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "implemented", + "notes": "Two sources configured: lwn and archlinux, 30m interval." + }, + { + "id": "core.crawl_worker", + "type": "worker", + "group": "mavend", + "responsibility": "Watches configured pages, and holds the fetcher the on-demand queryWeb source uses.", + "files": [ + "cmd/mavend/crawls.go", + "internal/crawl/", + "internal/webfetch/", + "deploy/mavend.json" + ], + "symbols": [ + "crawlWorker", + "newCrawlWorker", + "crawl.Crawler", + "crawl.Watcher", + "onDemandCrawler", + "factHashes", + "crawlFetcher" + ], + "reads": [], + "writes": [ + "state.notes", + "state.facts" + ], + "calls": [], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "implemented", + "notes": "crawl.on_demand = true and no watches are configured, so the worker runs with nothing to watch while queryWeb is live." + }, + { + "id": "core.mcp_worker", + "type": "worker", + "group": "mavend", + "responsibility": "Connects to configured MCP servers, proposes their tools into the act allowlist, and withdraws proposals for tools that vanish.", + "files": [ + "cmd/mavend/mcp.go", + "internal/mcp/", + "deploy/mavend.json" + ], + "symbols": [ + "mcpWiring", + "wireMCP", + "connect", + "propose", + "withdrawGone", + "status", + "caller" + ], + "reads": [], + "writes": [ + "state.tools" + ], + "calls": [ + "ext.vikunja_mcp" + ], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "configured-off", + "notes": "" + }, + { + "id": "core.home_worker", + "type": "worker", + "group": "mavend", + "responsibility": "Refreshes Home Assistant state and proposes one always-destructive tool row per controllable device.", + "files": [ + "cmd/mavend/smarthome.go", + "internal/smarthome/", + "deploy/mavend.json" + ], + "symbols": [ + "homeWiring", + "wireSmartHome", + "propose", + "run", + "caller" + ], + "reads": [], + "writes": [ + "state.tools" + ], + "calls": [ + "ext.homeassistant" + ], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "configured-off", + "notes": "" + }, + { + "id": "core.netscan", + "type": "service", + "group": "mavend", + "responsibility": "Bounded LAN scan. A read, so it has no allowlist row; what keeps it safe is that its range comes from config alone.", + "files": [ + "cmd/mavend/netscan.go", + "internal/netscan/", + "deploy/mavend.json" + ], + "symbols": [ + "netWiring", + "wireNetScan", + "scan", + "scanSummary", + "writeScanRecord", + "isNetworkQuery" + ], + "reads": [], + "writes": [ + "state.notes" + ], + "calls": [], + "called_by": [ + "core.q.network" + ], + "confidence": "high", + "status": "implemented", + "notes": "netscan.enabled = true in the deployed config, subnet 192.168.1.0/24." + }, + { + "id": "core.vision", + "type": "service", + "group": "mavend", + "responsibility": "Describes one image and stores the blob. Dark without a media block; MethodDescribeImage then answers ErrUnknownMethod.", + "files": [ + "cmd/mavend/vision.go", + "internal/vision/", + "internal/media/" + ], + "symbols": [ + "wireVision", + "Server.DescribeImageFn", + "media.Store" + ], + "reads": [], + "writes": [ + "state.notes", + "state.media_blobs" + ], + "calls": [], + "called_by": [ + "core.ipc_server" + ], + "confidence": "high", + "status": "configured-off", + "notes": "No `media` block in the deployed config." + }, + { + "id": "core.capture", + "type": "service", + "group": "mavend", + "responsibility": "The meeting recorder. Off unless a media block exists AND capture.enabled is true; all four capture methods then answer ErrUnknownMethod.", + "files": [ + "cmd/mavend/capture.go", + "internal/capture/" + ], + "symbols": [ + "wireCapture", + "Server.CaptureStartFn" + ], + "reads": [], + "writes": [ + "state.notes", + "state.media_blobs" + ], + "calls": [], + "called_by": [ + "core.ipc_server" + ], + "confidence": "high", + "status": "configured-off", + "notes": "The load-bearing default: on an unconfigured box no wire path begins a recording." + }, + { + "id": "core.speaker", + "type": "service", + "group": "mavend", + "responsibility": "Voice identification. Enrolment plumbing only until a speaker-embedding model exists on disk; off entirely without a speaker block.", + "files": [ + "cmd/mavend/speaker.go", + "internal/speaker/" + ], + "symbols": [ + "wireSpeaker" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.ipc_server" + ], + "confidence": "high", + "status": "configured-off", + "notes": "" + }, + { + "id": "core.mail_intake", + "type": "service", + "group": "mavend", + "responsibility": "Extracts task candidates from one fetched message using the resident model and writes them as candidates he reviews on /tasks.", + "files": [ + "cmd/mavend/mail.go", + "internal/email/" + ], + "symbols": [ + "wireMailIntake", + "mailIntake.ingest", + "Server.IngestMailFn" + ], + "reads": [], + "writes": [ + "state.tasks" + ], + "calls": [ + "core.phraser" + ], + "called_by": [ + "core.ipc_server" + ], + "confidence": "high", + "status": "configured-off", + "notes": "Nil unless an email block is configured AND there is a llama-server. Reaches past the intake decorator to st.CaptureTask directly." + }, + { + "id": "core.modelswap", + "type": "service", + "group": "mavend", + "responsibility": "Swaps the resident model on the fly and reports model status. Nil unless phraser.swap_models lists at least one model.", + "files": [ + "cmd/mavend/modelswap.go", + "internal/phraser/swap.go" + ], + "symbols": [ + "wireModelSwap", + "Server.SwapModelFn", + "Server.ModelStatusFn" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.ipc_server" + ], + "confidence": "high", + "status": "configured-off", + "notes": "No swap_models key in the deployed phraser block." + }, + { + "id": "state.db", + "type": "storage", + "group": "persistence", + "responsibility": "The one SQLite database. Opened at SetMaxOpenConns(1), so every write is serialised at the database and the IPC server adds no locking of its own.", + "files": [ + "internal/store/store.go", + "internal/store/schema.sql", + "internal/store/migrations.go" + ], + "symbols": [ + "store.Store", + "store.Open", + "store.OpenEncrypted", + "Store.Close", + "migrations", + "schemaSQL" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.store_api", + "core.tick_loop", + "core.wiring", + "core.reactive_handler" + ], + "confidence": "high", + "status": "implemented", + "notes": "Append-only discipline: a wrong fact is superseded, never overwritten. 20 migrations on top of schema.sql." + }, + { + "id": "state.db_file", + "type": "storage", + "group": "persistence", + "responsibility": "The at-rest ciphertext. A fixed magic header, a random GCM nonce and AES-256-GCM over the whole sqlite file. Decrypted into tmpfs on Open and re-encrypted atomically on Close.", + "files": [ + "internal/store/crypt.go", + "docker-compose.yml", + "deploy/mavend.json" + ], + "symbols": [ + "OpenEncrypted", + "cryptMagic", + "encState.closeAndSeal", + "atomicWrite" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "/var/lib/maven/maven.db.enc on the dbdata volume. Wrong key or a tampered file fails closed; there is never a plaintext fallback." + }, + { + "id": "state.db_tmpfs", + "type": "storage", + "group": "persistence", + "responsibility": "The decrypted working copy, in RAM. Lives and dies with the container.", + "files": [ + "internal/store/crypt.go", + "deploy/mavend.json" + ], + "symbols": [ + "db_tmpfs" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "/dev/shm/maven-plain.db. A daemon killed rather than stopped loses everything since the last clean shutdown; that is what mavseal recovers." + }, + { + "id": "state.wrapped_key", + "type": "storage", + "group": "persistence", + "responsibility": "The database key wrapped under a passkey PRF output (HKDF-SHA256 + AES-GCM). Its presence with no env key is what puts the daemon in locked mode.", + "files": [ + "cmd/mavend/keyfile.go", + "internal/webauthn/", + "cmd/mavend/main.go" + ], + "symbols": [ + "wrapKeyToFile", + "webauthn.WrapKey", + "webauthn.UnwrapKey", + "BlobV1", + "BlobV2" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "A v1 blob is derived from the credential PUBLIC key, which mavweb also writes to its passkey file; unwrapping one logs a SECURITY line." + }, + { + "id": "state.facts", + "type": "table", + "group": "persistence", + "responsibility": "Every observation: self, env and config. ts is valid-time. A correction points voids_id at the row it cancels.", + "files": [ + "internal/store/facts.go", + "internal/store/entityfacts.go", + "internal/store/schema.sql" + ], + "symbols": [ + "Store.WriteFact", + "Store.LatestFact", + "Store.RecentFacts", + "Store.VoidLatestFact", + "Store.CorrectValue", + "FactKind", + "FactResolutionState" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Written by six unrelated components: the voice fact handler, the quiet toggle, mavpoll, mavcaldav, mavweb (/api/signal and /api/ambient), the feed and crawl watermark writers, and the fact enrichment worker." + }, + { + "id": "state.reminders", + "type": "table", + "group": "persistence", + "responsibility": "User intent, with a fire time, a durable delivery group, a cached phrase, an attempt count and a bounded backoff.", + "files": [ + "internal/store/reminders.go", + "internal/store/migrations.go" + ], + "symbols": [ + "Store.CreateReminder", + "ListPendingReminders", + "CompleteReminderDelivery", + "CompleteSuccessfulReminderAttempt", + "BlockReminderDelivery", + "store.Reminder" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Carries cron and next_fire_ts columns since migration #2. No spoken path writes them." + }, + { + "id": "state.nudges", + "type": "table", + "group": "persistence", + "responsibility": "Every proactive send and its outcome. This table IS the restraint memory and the only input to the feedback tuner.", + "files": [ + "internal/store/nudges.go", + "internal/store/schema.sql" + ], + "symbols": [ + "Store.RecordNudge", + "Store.ResolveNudge", + "Store.RecentOutcomes", + "Store.UnackedTelegramRules", + "Store.SnoozedUntil" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Recorded AFTER a successful send, so a failed send does not pollute the feedback signal." + }, + { + "id": "state.notes", + "type": "table", + "group": "persistence", + "responsibility": "Free text he captured, plus everything the feed, crawl, capture, vision and netscan paths write. Embedding is a little-endian float32 blob, scanned brute force.", + "files": [ + "internal/store/notes.go", + "internal/store/schema.sql" + ], + "symbols": [ + "Store.WriteNote", + "Store.QueryNotes", + "Store.RecentNotes" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Six writers: the note handler, the RSS poller, the crawl watcher, meeting capture, image description and the LAN scan record." + }, + { + "id": "state.memory_vectors", + "type": "table", + "group": "persistence", + "responsibility": "The vector index behind recall, over notes and facts together. Backed by store.VectorMemory or the in-memory floor.", + "files": [ + "internal/store/memory.go", + "internal/store/factvectors.go", + "internal/memory/store.go", + "cmd/mavend/actions_fact.go", + "cmd/mavend/voicewire.go" + ], + "symbols": [ + "Store.VectorMemory", + "MemoryStore.Insert", + "MemoryStore.Search", + "MemoryStore.DeletePrefix", + "pruneFactVectors", + "repairFactVectors", + "checkStoredEmbedder", + "runReembed" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Marked with the embedder id. Swapping the embedder or bumping tokenizerRev requires ReembedAll." + }, + { + "id": "state.tools", + "type": "table", + "group": "persistence", + "responsibility": "The act allowlist. A `proposed` row drives nothing; the executor only runs `enabled` rows, and enabling is a human act on an authed surface, never the voice path.", + "files": [ + "internal/store/tools.go", + "internal/store/schema.sql", + "cmd/mavend/voicewire.go", + "deploy/mavend.json" + ], + "symbols": [ + "Store.ProposeTool", + "Store.EnableTool", + "Store.LookupTool", + "seedTools" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Three unrelated proposers write here: config seeding, MCP discovery and Home Assistant discovery. Twelve tools are seeded from the deployed config, five of them destructive." + }, + { + "id": "state.presence_state", + "type": "table", + "group": "persistence", + "responsibility": "The one stateful bit of presence: the hysteresis bucket, rewritten each tick.", + "files": [ + "internal/store/presence.go", + "internal/store/presence_state.go", + "internal/store/schema.sql" + ], + "symbols": [ + "Store.SavePresenceState", + "Store.LoadPresenceState", + "Store.PresenceProbes", + "Bucket" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "A singleton row. Presence is otherwise a pure function." + }, + { + "id": "state.events", + "type": "table", + "group": "persistence", + "responsibility": "Recorded action+object events; the only input to pattern detection.", + "files": [ + "internal/store/events.go", + "internal/ipc/storeapi.go" + ], + "symbols": [ + "Store.CreateEvent", + "Store.DistinctEventPairs", + "Store.EventsFor", + "storeAPI.SeedEvent" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "SeedEvent is reachable only when mavend runs with -allow-seed, and the flag makes seedStore nil rather than merely refusing." + }, + { + "id": "state.proposed_routines", + "type": "table", + "group": "persistence", + "responsibility": "Detected patterns awaiting acceptance, and accepted ones with their fire clock.", + "files": [ + "internal/store/proposed_routines.go" + ], + "symbols": [ + "ListProposedRoutines", + "AcceptProposedRoutine", + "DismissProposedRoutine" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.digest_entries", + "type": "table", + "group": "persistence", + "responsibility": "Candidates the restraint gate BLOCKED, held durably for later resurfacing.", + "files": [ + "internal/store/digest.go", + "cmd/mavend/tick_digest.go" + ], + "symbols": [ + "enqueueSuppressedDigest", + "maybeDrainDigest", + "expireStaleDigest" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "A separate mechanism from the in-memory digestQ, which batches candidates the gate ALLOWED." + }, + { + "id": "state.delivery_attempts", + "type": "table", + "group": "persistence", + "responsibility": "The durable delivery ledger. Intent is recorded BEFORE the external send, so a crash leaves a pending row rather than silence.", + "files": [ + "internal/store/delivery.go", + "internal/delivery/dispatcher.go" + ], + "symbols": [ + "BeginDeliveryAttempt", + "CompleteDeliveryAttempt", + "ReconcileStaleDeliveryAttempts" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Reconciled to `unknown` at boot before the tick loop resumes, so nothing auto-resends into the ambiguity." + }, + { + "id": "state.ack_sends", + "type": "table", + "group": "persistence", + "responsibility": "sev4 telegram repeat-til-ack tracking.", + "files": [ + "internal/store/ack.go" + ], + "symbols": [ + "Store.WasAcked", + "Store.MarkSent", + "Store.MarkAcked", + "Store.LastSent" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.dialogue_sessions", + "type": "table", + "group": "persistence", + "responsibility": "The follow-up slot carry-over, persisted so a restart mid-conversation keeps the thread.", + "files": [ + "internal/store/dialogue.go", + "internal/dialogue/" + ], + "symbols": [ + "dialogue.NewPersistentSessionStore", + "SessionStore.Get", + "SessionStore.Load" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "TTL 2 minutes, pruned on load. The clarify store deliberately does NOT persist: a restart expires the open question." + }, + { + "id": "state.tasks", + "type": "table", + "group": "persistence", + "responsibility": "Task candidates and tasks, from the voice path, the web form and the mail reader.", + "files": [ + "internal/store/tasks.go", + "internal/tasks/" + ], + "symbols": [ + "Store.CaptureTask", + "ListTasks", + "SetTaskStatus", + "EditTask" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.list_items", + "type": "table", + "group": "persistence", + "responsibility": "The shopping and other named lists.", + "files": [ + "internal/store/listitems.go" + ], + "symbols": [ + "Store.AddListItem", + "ListItems" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.routing_traces", + "type": "table", + "group": "persistence", + "responsibility": "Persisted turn decision records, for fitting the routing heads.", + "files": [ + "internal/store/routingtraces.go", + "cmd/mavend/routingtrace.go" + ], + "symbols": [ + "WriteRoutingTrace", + "PruneRoutingTraces" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Retained 14 days, enforced on write and again on start." + }, + { + "id": "state.routing_labels", + "type": "table", + "group": "persistence", + "responsibility": "Owner corrections of a turn's route, the label side of the same data.", + "files": [ + "internal/store/routinglabels.go" + ], + "symbols": [ + "Store.CorrectTurn", + "Store.RoutingLabels" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.ecosystem_traces", + "type": "table", + "group": "persistence", + "responsibility": "One row per ecosystem call, keyed by correlation id.", + "files": [ + "internal/store/ecotraces.go" + ], + "symbols": [ + "Store.WriteEcosystemTrace", + "Store.RecentEcosystemTraces", + "Store.PruneEcosystemTraces" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.meta", + "type": "table", + "group": "persistence", + "responsibility": "Schema version and the embedder marker.", + "files": [ + "internal/store/meta.go" + ], + "symbols": [ + "Store.Meta", + "Store.SetMeta" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.passkey_file", + "type": "storage", + "group": "persistence", + "responsibility": "mavweb's WebAuthn credential store, a JSON file outside the database.", + "files": [ + "cmd/mavweb/credentials.go", + "internal/webauthn/" + ], + "symbols": [ + "passkeys.json", + "-passkey-file" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Held by mavweb, not mavend. A v1 wrapped key blob plus this file together recover the database key with no authenticator." + }, + { + "id": "state.maildata", + "type": "storage", + "group": "persistence", + "responsibility": "mavmaild's seen-UID file, on its own volume so neither side can be restored from the other's backup.", + "files": [ + "cmd/mavmaild/main.go", + "docker-compose.yml" + ], + "symbols": [ + "-state" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "built-not-deployed", + "notes": "" + }, + { + "id": "state.media_blobs", + "type": "storage", + "group": "persistence", + "responsibility": "The blob store shared by vision and the meeting recorder, with a retention loop.", + "files": [ + "internal/media/", + "cmd/mavend/vision.go", + "cmd/mavend/capture.go" + ], + "symbols": [ + "media.Store", + "media.Blob" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "configured-off", + "notes": "" + }, + { + "id": "state.decision_ring", + "type": "shared-state", + "group": "in-memory", + "responsibility": "The last few turns' arbitration records. In memory and bounded, because none of his words belong in a table that outlives the diagnosis.", + "files": [ + "internal/decision/ring.go" + ], + "symbols": [ + "decision.Ring", + "Ring.Push", + "Ring.Recent" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.reactive_handler", + "core.daemon_api" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.clarify_store", + "type": "shared-state", + "group": "in-memory", + "responsibility": "The parked request behind an open question, as a per-reach stack. Deliberately not persisted: a restart expires the question.", + "files": [ + "internal/dialogue/clarify.go" + ], + "symbols": [ + "dialogue.ClarifyStore", + "PendingQuestion", + "Put", + "Push", + "Pop", + "CompleteTop", + "TakeExpired", + "Depth" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.preroute" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.pending_act", + "type": "shared-state", + "group": "in-memory", + "responsibility": "Three single-slot confirmation registers on the handler, under one mutex: a destructive local act, a routine proposal and a mutating Hexis capability. Last-asked wins.", + "files": [ + "cmd/mavend/voice.go", + "cmd/mavend/confirm.go", + "cmd/mavend/ecosystem_acts.go" + ], + "symbols": [ + "reactiveHandler.pending", + "pendingRoutine", + "pendingHexis", + "lastRouted", + "confirmTTL" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.preroute", + "core.action_act" + ], + "confidence": "high", + "status": "implemented", + "notes": "Single-user box. A second act while one waits overwrites it; each expires after confirmTTL." + }, + { + "id": "state.surfaced_items", + "type": "shared-state", + "group": "in-memory", + "responsibility": "The Praxis item ids she last read out, in order, so an ordinal has something to mean. Replaced by the next attention digest, with no TTL.", + "files": [ + "cmd/mavend/voice.go", + "cmd/mavend/ordinal.go", + "cmd/mavend/attentionq.go" + ], + "symbols": [ + "reactiveHandler.surfacedItems", + "resolveCandidate" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.preroute", + "core.q.attention" + ], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "state.tick_memo", + "type": "shared-state", + "group": "in-memory", + "responsibility": "The tick loop's own mutable memory: the last phrase per rule, the last trace, the digest queue, per-routine last-fire maps and the proposal cooldown clock.", + "files": [ + "cmd/mavend/tick.go" + ], + "symbols": [ + "tickLoop.lastPhrase", + "tickLoop.lastTrace", + "tickLoop.digestQ", + "routineLast", + "morningLast", + "lastProposalAt" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "core.tick_loop", + "core.daemon_api" + ], + "confidence": "high", + "status": "implemented", + "notes": "lastProposalAt is in memory on purpose: a restart is allowed to permit one more announcement." + }, + { + "id": "cfg.mavend", + "type": "config", + "group": "configuration", + "responsibility": "The single daemon config. Sets sockets, the database paths, the phraser, the embedder and heads, every reach, every world source, the ecosystem URLs, the tool allowlist and every capability's on/off state, with ${VAR} expansion from a gitignored env file.", + "files": [ + "deploy/mavend.json", + "internal/config/", + "deploy/telegram.env" + ], + "symbols": [ + "config.Load", + "config.Config", + "DBEncryptionKey", + "DefaultWrappedKeyPath", + "DefaultRouterThreshold", + "MorningRoutinesFromConfig" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [ + "proc.mavend" + ], + "confidence": "high", + "status": "implemented", + "notes": "Presence of a block is the on-switch for most capabilities. Absence is silent and, for the calendar, load-bearing." + }, + { + "id": "cfg.compose", + "type": "config", + "group": "configuration", + "responsibility": "The deployment shape. Five services run; mavcaldav and mavmaild are present as commented-out blocks with their reasoning.", + "files": [ + "docker-compose.yml" + ], + "symbols": [ + "x-image", + "services", + "volumes", + "networks" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Count services against compose, not against `make build`. mavwaked and mavgpud are systemd units on workpc." + }, + { + "id": "cfg.systemd", + "type": "config", + "group": "configuration", + "responsibility": "The workstation units: the listening client, the GPU supervisor and the ssh tunnel that is the only path to mavend's voice port.", + "files": [ + "deploy/mavwaked.service", + "deploy/mavgpud.service", + "deploy/maven-voice-tunnel.service", + "deploy/asoundrc" + ], + "symbols": [ + "ExecStart", + "Requires=maven-voice-tunnel.service" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "eval.router", + "type": "test", + "group": "evaluation", + "responsibility": "The routing fixture. Scores the real stage 0 set, the classifier and the resident model on a frozen Russian corpus.", + "files": [ + "internal/router/eval/eval.go", + "internal/router/eval/ru_routing_v1.json", + "internal/router/eval/ru_ecosystem_v1.json", + "internal/router/eval/reach.go", + "Makefile" + ], + "symbols": [ + "eval", + "make eval-router", + "make eval-reach" + ], + "reads": [ + "router.stage0" + ], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "Calls router.StageZeroGrammars directly, so a grammar change moves the measurement with it." + }, + { + "id": "eval.phrasing", + "type": "test", + "group": "evaluation", + "responsibility": "The persona checks: address form, feminine self-reference and the cringe list.", + "files": [ + "internal/phraser/eval/checks.go", + "Makefile" + ], + "symbols": [ + "CheckAddress", + "CheckFeminine", + "CheckCringe", + "make eval-phrasing" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "" + }, + { + "id": "eval.gates", + "type": "test", + "group": "evaluation", + "responsibility": "The static gates. staticcheck, deadcode and govulncheck pass against a baseline rather than against zero, and fail on a baseline entry whose finding is gone.", + "files": [ + "scripts/analyzers/deadcode.baseline", + "scripts/analyzers/staticcheck.baseline", + "Makefile" + ], + "symbols": [ + "make analyze", + "make lint", + "make deadcode", + "make vuln" + ], + "reads": [], + "writes": [], + "calls": [], + "called_by": [], + "confidence": "high", + "status": "implemented", + "notes": "The deadcode baseline currently accepts 13 unreachable symbols." + } + ], + "edges": [ + { + "from": "proc.mavwaked", + "to": "bnd.voice_tcp", + "kind": "tcp", + "label": "PushToTalk over ssh tunnel", + "confidence": "high", + "status": "implemented", + "evidence": "deploy/mavwaked.service -addr 127.0.0.1:9100; Requires=maven-voice-tunnel.service" + }, + { + "from": "proc.mavenclient", + "to": "bnd.voice_tcp", + "kind": "tcp", + "label": "one wav per invocation", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavenclient/main.go" + }, + { + "from": "proc.mavweb", + "to": "bnd.voice_tcp", + "kind": "tcp", + "label": "POST /api/ptt", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavweb/main.go handlePTT(w, r, *voiceAddr, ...)" + }, + { + "from": "bnd.voice_tcp", + "to": "core.voice_server", + "kind": "in-process", + "label": "accept, register Session", + "confidence": "high", + "status": "implemented", + "evidence": "internal/voice/server.go" + }, + { + "from": "proc.mavweb", + "to": "bnd.ipc", + "kind": "unix", + "label": "three ipc.Client connections", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavweb/main.go core/swapConn/turnConn" + }, + { + "from": "proc.mavpoll", + "to": "bnd.ipc", + "kind": "unix", + "label": "WriteFact", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavpoll/main.go -socket /run/maven/mavend.sock" + }, + { + "from": "proc.mavcaldav", + "to": "bnd.ipc", + "kind": "unix", + "label": "WriteFact", + "confidence": "high", + "status": "built-not-deployed", + "evidence": "docker-compose.yml: commented out" + }, + { + "from": "proc.mavmaild", + "to": "bnd.ipc", + "kind": "unix", + "label": "IngestMail", + "confidence": "high", + "status": "built-not-deployed", + "evidence": "docker-compose.yml: commented out" + }, + { + "from": "proc.mavupdate", + "to": "bnd.ipc", + "kind": "unix", + "label": "Ping only", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavupdate/main.go; there is no MethodApplyUpdate" + }, + { + "from": "proc.e2eprobe", + "to": "bnd.ipc", + "kind": "unix", + "label": "typed probe", + "confidence": "high", + "status": "temporary", + "evidence": "cmd/e2eprobe/main.go" + }, + { + "from": "bnd.ipc", + "to": "core.ipc_server", + "kind": "in-process", + "label": "dispatch", + "confidence": "high", + "status": "implemented", + "evidence": "internal/ipc/server.go serveConn" + }, + { + "from": "core.stt_seam", + "to": "bnd.worker", + "kind": "unix", + "label": "Transcribe", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voicewire.go worker.Dial(cfg.Voice.Stt.Socket)" + }, + { + "from": "core.tts_seam", + "to": "bnd.worker", + "kind": "unix", + "label": "Synthesize", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voicewire.go worker.Dial(cfg.Voice.Tts.Socket)" + }, + { + "from": "bnd.worker", + "to": "proc.mavsttd", + "kind": "unix", + "label": "/run/maven/stt.sock", + "confidence": "high", + "status": "implemented", + "evidence": "docker-compose.yml mavsttd command" + }, + { + "from": "bnd.worker", + "to": "proc.mavttsd", + "kind": "unix", + "label": "/run/maven/tts.sock", + "confidence": "high", + "status": "implemented", + "evidence": "docker-compose.yml mavttsd command" + }, + { + "from": "core.stt_seam", + "to": "ext.cw2_stt", + "kind": "http", + "label": "preferred transcriber, silent fallback", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voicewire.go sttSeam; mavend.json workstation.stt" + }, + { + "from": "core.model_seam", + "to": "proc.mavgpud", + "kind": "http", + "label": "/health probe then completion", + "confidence": "high", + "status": "configured-off", + "evidence": "mavend.json workstation.model_disabled = true" + }, + { + "from": "proc.mavsttd", + "to": "ext.whispercpp", + "kind": "in-process", + "label": "cgo", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavsttd/whisper_handler.go" + }, + { + "from": "proc.mavttsd", + "to": "ext.piper", + "kind": "subprocess", + "label": "piper argv", + "confidence": "high", + "status": "implemented", + "evidence": "docker-compose.yml mavttsd -piper" + }, + { + "from": "proc.mavwaked", + "to": "ext.alsa", + "kind": "subprocess", + "label": "arecord / aplay", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavwaked/main.go" + }, + { + "from": "proc.mavend", + "to": "svc.llama_server", + "kind": "subprocess", + "label": "child llama-server", + "confidence": "high", + "status": "implemented", + "evidence": "internal/phraser/server.go; mavend.json phraser.bin_path" + }, + { + "from": "proc.mavgpud", + "to": "svc.llama_server", + "kind": "subprocess", + "label": "supervised child on the workstation card", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavgpud/runner.go" + }, + { + "from": "proc.mavpoll", + "to": "ext.netdata", + "kind": "http", + "label": "alarms", + "confidence": "high", + "status": "implemented", + "evidence": "docker-compose.yml -netdata http://127.0.0.1:19999" + }, + { + "from": "proc.mavpoll", + "to": "ext.uptimekuma", + "kind": "http", + "label": "/metrics", + "confidence": "high", + "status": "implemented", + "evidence": "docker-compose.yml -kuma" + }, + { + "from": "proc.mavpoll", + "to": "ext.zenmoney", + "kind": "http", + "label": "spending", + "confidence": "high", + "status": "configured-off", + "evidence": "docker-compose.yml: the token mount is commented out" + }, + { + "from": "proc.mavweb", + "to": "ext.nexus", + "kind": "http", + "label": "read-only /ecosystem panel", + "confidence": "high", + "status": "implemented", + "evidence": "docker-compose.yml -nexus http://nexus:9740" + }, + { + "from": "proc.mavweb", + "to": "ext.praxis", + "kind": "http", + "label": "read-only /ecosystem panel", + "confidence": "high", + "status": "implemented", + "evidence": "docker-compose.yml -praxis" + }, + { + "from": "proc.mavweb", + "to": "ext.hexis", + "kind": "http", + "label": "read-only /ecosystem panel", + "confidence": "high", + "status": "implemented", + "evidence": "docker-compose.yml -hexis" + }, + { + "from": "core.ipc_server", + "to": "core.auth_gate", + "kind": "calls", + "label": "Check before every dispatch", + "confidence": "high", + "status": "implemented", + "evidence": "internal/ipc/server.go Server.Check" + }, + { + "from": "core.ipc_server", + "to": "core.daemon_api", + "kind": "calls", + "label": "CoreAPI dispatch", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/main.go ipc.Listen(cfg.SocketPath, coreAPI)" + }, + { + "from": "core.ipc_server", + "to": "core.mail_intake", + "kind": "calls", + "label": "IngestMailFn bypass", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/main.go wireMailIntake" + }, + { + "from": "core.ipc_server", + "to": "core.modelswap", + "kind": "calls", + "label": "SwapModelFn / ModelStatusFn bypass", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/main.go wireModelSwap" + }, + { + "from": "core.ipc_server", + "to": "core.vision", + "kind": "calls", + "label": "DescribeImageFn bypass", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/main.go wireVision" + }, + { + "from": "core.ipc_server", + "to": "core.capture", + "kind": "calls", + "label": "four Capture* bypasses", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/main.go wireCapture" + }, + { + "from": "core.ipc_server", + "to": "core.speaker", + "kind": "calls", + "label": "speaker enrolment bypass", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/main.go wireSpeaker" + }, + { + "from": "core.ipc_server", + "to": "core.daemon_lock", + "kind": "calls", + "label": "UnlockFn / WrapKeyFn / StepUp", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/main.go srv.UnlockFn" + }, + { + "from": "core.daemon_api", + "to": "core.store_api", + "kind": "embeds", + "label": "embedded ipc.CoreAPI", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick_api.go type daemonAPI struct { ipc.CoreAPI ... }" + }, + { + "from": "core.daemon_api", + "to": "core.intake_api", + "kind": "wraps", + "label": "coreFor() = newIntakeAPI(ipc.NewStoreAPI(st), evBus, time.Now)", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/main.go coreFor" + }, + { + "from": "core.intake_api", + "to": "core.event_bus", + "kind": "publishes", + "label": "one envelope per intake write", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/intake.go" + }, + { + "from": "core.daemon_api", + "to": "core.reactive_handler", + "kind": "calls", + "label": "chatFn = handler.handleText", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/boot.go newDaemonAPI" + }, + { + "from": "core.reactive_handler", + "to": "core.daemon_api", + "kind": "calls", + "label": "handler.api back-patched by upgradeAPI", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voice.go upgradeAPI; boot.go d.voiceW.handler.upgradeAPI(api)" + }, + { + "from": "core.daemon_api", + "to": "core.tick_loop", + "kind": "reads", + "label": "trace, morningStatus, dayPlan", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/boot.go getTrace / getDayPlan" + }, + { + "from": "core.daemon_api", + "to": "ext.nexus", + "kind": "http", + "label": "ResolveEntity", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick_api.go daemonAPI.ResolveEntity" + }, + { + "from": "core.voice_server", + "to": "core.reactive_handler", + "kind": "calls", + "label": "HandlePushToTalk", + "confidence": "high", + "status": "implemented", + "evidence": "internal/voice/server.go Handler" + }, + { + "from": "core.telegram_intake", + "to": "core.daemon_api", + "kind": "calls", + "label": "api.Chat per inbound message", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/telegramintake.go chatTurnFn" + }, + { + "from": "core.reactive_handler", + "to": "core.stt_seam", + "kind": "calls", + "label": "step 1 transcribe", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voice.go HandlePushToTalk" + }, + { + "from": "core.reactive_handler", + "to": "core.preroute", + "kind": "calls", + "label": "steps 1-5e, eleven rungs", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voice.go runTurn" + }, + { + "from": "core.reactive_handler", + "to": "core.turn_route", + "kind": "calls", + "label": "step 6 route", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voice.go rt.resolve(ctx)" + }, + { + "from": "core.reactive_handler", + "to": "core.action_table", + "kind": "calls", + "label": "step 9 applyAction", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions.go actionHandlers" + }, + { + "from": "core.reactive_handler", + "to": "core.replier", + "kind": "calls", + "label": "step 10, only when the handler returned nothing", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voice.go h.replier.Reply(ctx, dec)" + }, + { + "from": "core.reactive_handler", + "to": "core.tts_seam", + "kind": "calls", + "label": "step 6 synthesize", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voice.go h.reply" + }, + { + "from": "core.reactive_handler", + "to": "core.decision_trace", + "kind": "writes", + "label": "one record per turn", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/voice.go decision.With(ctx, text)" + }, + { + "from": "core.preroute", + "to": "core.turn_route", + "kind": "reads", + "label": "the clarify resolver reads the routed decision before claiming", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/turnroute.go routeForRole" + }, + { + "from": "core.preroute", + "to": "state.clarify_store", + "kind": "reads", + "label": "parked question", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/clarify.go" + }, + { + "from": "core.preroute", + "to": "state.pending_act", + "kind": "reads", + "label": "confirm register", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/confirm.go" + }, + { + "from": "core.preroute", + "to": "state.surfaced_items", + "kind": "reads", + "label": "ordinal resolution", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/ordinal.go" + }, + { + "from": "core.turn_route", + "to": "router.cascade", + "kind": "calls", + "label": "Route", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/turnroute.go r.h.router.Route" + }, + { + "from": "core.turn_route", + "to": "state.dialogue_sessions", + "kind": "reads", + "label": "previous turn for a continuation", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/turnroute.go dialogueSessions.Get" + }, + { + "from": "router.cascade", + "to": "router.stage0", + "kind": "calls", + "label": "stage 0, first match wins at 1.0", + "confidence": "high", + "status": "implemented", + "evidence": "internal/router/router.go for i, g := range r.grammars" + }, + { + "from": "router.cascade", + "to": "router.heads", + "kind": "calls", + "label": "stage 0b, declines below threshold", + "confidence": "high", + "status": "implemented", + "evidence": "internal/router/router.go if r.heads != nil" + }, + { + "from": "router.cascade", + "to": "router.llm", + "kind": "calls", + "label": "stage 1a, any error falls through", + "confidence": "high", + "status": "implemented", + "evidence": "internal/router/router.go if r.llm != nil" + }, + { + "from": "router.cascade", + "to": "router.classifier", + "kind": "calls", + "label": "stage 1, the floor", + "confidence": "high", + "status": "implemented", + "evidence": "internal/router/router.go r.classifier.Classify" + }, + { + "from": "router.cascade", + "to": "router.extractor", + "kind": "calls", + "label": "stage 2 slots", + "confidence": "high", + "status": "implemented", + "evidence": "internal/router/router.go r.extractor.Extract / fillSlots" + }, + { + "from": "router.llm", + "to": "svc.llama_server", + "kind": "http", + "label": "grammar-constrained completion", + "confidence": "high", + "status": "implemented", + "evidence": "internal/router/llmrouter.go" + }, + { + "from": "router.classifier", + "to": "router.embedder", + "kind": "calls", + "label": "nearest centroid", + "confidence": "high", + "status": "implemented", + "evidence": "internal/router/classifier.go" + }, + { + "from": "router.heads", + "to": "router.embedder", + "kind": "reads", + "label": "a fine-tuned COPY of the same encoder", + "confidence": "high", + "status": "implemented", + "evidence": "mavend.json voice.embedder.heads_path; heads_path must never equal model_path" + }, + { + "from": "core.action_table", + "to": "core.query_chain", + "kind": "calls", + "label": "IntentQuery", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions.go actionHandlers[IntentQuery]" + }, + { + "from": "core.action_table", + "to": "core.action_fact", + "kind": "calls", + "label": "IntentFact", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions.go" + }, + { + "from": "core.action_table", + "to": "core.action_reminder", + "kind": "calls", + "label": "IntentReminder", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions.go" + }, + { + "from": "core.action_table", + "to": "core.action_act", + "kind": "calls", + "label": "IntentAct", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions.go" + }, + { + "from": "core.action_table", + "to": "core.action_note", + "kind": "calls", + "label": "IntentNote", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions.go" + }, + { + "from": "core.action_table", + "to": "core.action_chat", + "kind": "calls", + "label": "IntentChat", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions.go" + }, + { + "from": "core.action_table", + "to": "core.action_system", + "kind": "calls", + "label": "IntentSystem", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions.go" + }, + { + "from": "core.action_fact", + "to": "core.query_chain", + "kind": "calls", + "label": "a question-shaped fact is answered as a query", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_fact.go return h.actionQuery(ctx, q)" + }, + { + "from": "core.action_fact", + "to": "core.action_chat", + "kind": "calls", + "label": "a complaint is answered as chat", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_fact.go return h.actionChat(ctx, c)" + }, + { + "from": "core.action_fact", + "to": "state.facts", + "kind": "writes", + "label": "WriteFact source tap:voice or tap:text", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_fact.go" + }, + { + "from": "core.action_fact", + "to": "state.memory_vectors", + "kind": "writes", + "label": "prune then insert one vector per key", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_fact.go pruneFactVectors" + }, + { + "from": "core.action_reminder", + "to": "state.reminders", + "kind": "writes", + "label": "CreateReminder", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_reminder.go h.api.CreateReminder" + }, + { + "from": "core.action_act", + "to": "state.tools", + "kind": "reads", + "label": "enabled allowlist only", + "confidence": "high", + "status": "implemented", + "evidence": "internal/tool/tool.go:164 t.Status != enabled" + }, + { + "from": "core.action_act", + "to": "core.risk_policy", + "kind": "calls", + "label": "PolicyFor(RiskOf(t)) before every local exec", + "confidence": "high", + "status": "implemented", + "evidence": "internal/tool/tool.go:181" + }, + { + "from": "core.action_act", + "to": "core.praxis_acts", + "kind": "calls", + "label": "intercepted BEFORE the local executor", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_act.go:44 handlePraxisAct" + }, + { + "from": "core.praxis_acts", + "to": "ext.praxis", + "kind": "http", + "label": "acknowledge / resolve / ignore / pin, no tier and no confirm", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/ecosystem_acts.go:158 a.call(ctx, px, id)" + }, + { + "from": "core.ecosystem_hexis_gate", + "to": "core.risk_policy", + "kind": "calls", + "label": "RiskOfCapability then PolicyFor, the same policy reused", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/ecosystem_acts.go:768" + }, + { + "from": "core.risk_policy", + "to": "core.action_act", + "kind": "gates", + "label": "ErrNeedsAuthedSurface / ErrNeedsConfirm", + "confidence": "high", + "status": "implemented", + "evidence": "internal/tool/tool.go:182-187" + }, + { + "from": "core.action_act", + "to": "ext.hexis", + "kind": "http", + "label": "confirmed mutating capability", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/ecosystem_acts.go handleHexisAct" + }, + { + "from": "core.action_note", + "to": "state.notes", + "kind": "writes", + "label": "WriteNote", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_note.go" + }, + { + "from": "core.action_chat", + "to": "core.phraser", + "kind": "calls", + "label": "PhraseChat with dialogue history", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions.go" + }, + { + "from": "core.query_chain", + "to": "core.q.factbykey", + "kind": "calls", + "label": "chain position: fact-by-key", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.dayplan", + "kind": "calls", + "label": "chain position: day-plan", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.habits", + "kind": "calls", + "label": "chain position: habits", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.tasks", + "kind": "calls", + "label": "chain position: tasks", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.attention", + "kind": "calls", + "label": "chain position: attention", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.list", + "kind": "calls", + "label": "chain position: list", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.money", + "kind": "calls", + "label": "chain position: money", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.history", + "kind": "calls", + "label": "chain position: history", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.feeds", + "kind": "calls", + "label": "chain position: feeds", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.home", + "kind": "calls", + "label": "chain position: home", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.network", + "kind": "calls", + "label": "chain position: network", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.calendar", + "kind": "calls", + "label": "chain position: calendar", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.weather", + "kind": "calls", + "label": "chain position: weather", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.self", + "kind": "calls", + "label": "chain position: self", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.embed", + "kind": "calls", + "label": "chain position: embed", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.memory", + "kind": "calls", + "label": "chain position: memory", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.notes", + "kind": "calls", + "label": "chain position: notes", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.personal", + "kind": "calls", + "label": "chain position: personal", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.search", + "kind": "calls", + "label": "chain position: search", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.kiwix", + "kind": "calls", + "label": "chain position: kiwix", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.web", + "kind": "calls", + "label": "chain position: web", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.query_chain", + "to": "core.q.general", + "kind": "calls", + "label": "chain position: general-knowledge", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySources" + }, + { + "from": "core.q.search", + "to": "ext.searxng", + "kind": "http", + "label": "query string only", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go querySearch" + }, + { + "from": "core.q.kiwix", + "to": "ext.kiwix", + "kind": "http", + "label": "ZIM search", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go queryKiwix" + }, + { + "from": "core.q.web", + "to": "core.crawl_worker", + "kind": "calls", + "label": "on-demand page fetch", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/crawls.go onDemandCrawler" + }, + { + "from": "core.q.general", + "to": "core.phraser", + "kind": "calls", + "label": "the model answers from its own weights, last", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go queryGeneral" + }, + { + "from": "core.q.attention", + "to": "ext.praxis", + "kind": "http", + "label": "ListAttention", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/attentionq.go" + }, + { + "from": "core.q.attention", + "to": "state.surfaced_items", + "kind": "writes", + "label": "the read-out order, for the next ordinal", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/attentionq.go" + }, + { + "from": "core.q.home", + "to": "ext.homeassistant", + "kind": "http", + "label": "device state", + "confidence": "high", + "status": "configured-off", + "evidence": "mavend.json smarthome.enabled = false" + }, + { + "from": "core.q.network", + "to": "core.netscan", + "kind": "calls", + "label": "bounded LAN scan", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/netscan.go" + }, + { + "from": "core.q.weather", + "to": "ext.openmeteo", + "kind": "http", + "label": "forecast", + "confidence": "high", + "status": "configured-off", + "evidence": "wireVoice picks NewStubProvider without a voice.weather block; the deployed config has none" + }, + { + "from": "core.q.calendar", + "to": "state.facts", + "kind": "reads", + "label": "facts(kind=env, source=caldav:*)", + "confidence": "high", + "status": "partially-wired", + "evidence": "docker-compose.yml: mavcaldav is commented out, so nobody writes them" + }, + { + "from": "core.q.memory", + "to": "state.memory_vectors", + "kind": "reads", + "label": "vector search over notes and facts", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go queryMemory" + }, + { + "from": "core.q.notes", + "to": "state.notes", + "kind": "reads", + "label": "notes-only recall pass", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go queryNotes" + }, + { + "from": "core.q.factbykey", + "to": "state.facts", + "kind": "reads", + "label": "current value for a key", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go queryFactByKey" + }, + { + "from": "core.q.money", + "to": "state.facts", + "kind": "reads", + "label": "facts(kind=env, source=poll:zenmoney)", + "confidence": "high", + "status": "configured-off", + "evidence": "compose does not mount the zenmoney token" + }, + { + "from": "core.q.tasks", + "to": "state.tasks", + "kind": "reads", + "label": "task list", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_task.go" + }, + { + "from": "core.q.list", + "to": "state.list_items", + "kind": "reads", + "label": "named lists", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_list.go" + }, + { + "from": "core.q.feeds", + "to": "state.notes", + "kind": "reads", + "label": "notes with source rss:*", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/actions_query.go queryFeeds" + }, + { + "from": "core.q.dayplan", + "to": "core.tick_loop", + "kind": "reads", + "label": "the plan is assembled by the tick loop, not read from a table", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick_morning.go dayPlan; voice.go upgradeAPI comment" + }, + { + "from": "core.q.habits", + "to": "state.events", + "kind": "reads", + "label": "behaviour profile over recorded events", + "confidence": "high", + "status": "implemented", + "evidence": "internal/memory/behavior.go BuildProfile" + }, + { + "from": "core.q.history", + "to": "state.facts", + "kind": "reads", + "label": "what he tapped in", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/historyq.go" + }, + { + "from": "core.tick_loop", + "to": "core.gatherer", + "kind": "calls", + "label": "GatherState, aborts the tick on error", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick.go" + }, + { + "from": "core.gatherer", + "to": "state.facts", + "kind": "reads", + "label": "the tick snapshot", + "confidence": "high", + "status": "implemented", + "evidence": "internal/loop/gather.go readFact" + }, + { + "from": "core.gatherer", + "to": "state.reminders", + "kind": "reads", + "label": "due and collapsed", + "confidence": "high", + "status": "implemented", + "evidence": "internal/loop/gather.go collapseReminders" + }, + { + "from": "core.gatherer", + "to": "state.nudges", + "kind": "reads", + "label": "restraint memory", + "confidence": "high", + "status": "implemented", + "evidence": "internal/loop/gather.go" + }, + { + "from": "core.tick_loop", + "to": "core.rules", + "kind": "calls", + "label": "ExplainTick, at most one candidate", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick.go loop.ExplainTick" + }, + { + "from": "core.tick_loop", + "to": "core.phraser", + "kind": "calls", + "label": "PhraseNudge / PhraseReminder", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick.go" + }, + { + "from": "core.tick_loop", + "to": "core.dispatcher", + "kind": "calls", + "label": "DispatchNudge, RepeatUnacked", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick.go" + }, + { + "from": "core.tick_loop", + "to": "core.pattern", + "kind": "calls", + "label": "detectPatterns every tick", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick_routines.go" + }, + { + "from": "core.tick_loop", + "to": "core.morning", + "kind": "calls", + "label": "fireMorningRoutines", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick_morning.go" + }, + { + "from": "core.tick_loop", + "to": "core.routines", + "kind": "calls", + "label": "fireRoutines and fireAcceptedRoutines", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick_routines.go" + }, + { + "from": "core.tick_loop", + "to": "state.presence_state", + "kind": "writes", + "label": "savePresence each tick", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick.go savePresence" + }, + { + "from": "core.tick_loop", + "to": "state.digest_entries", + "kind": "writes", + "label": "gate-suppressed candidates", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick_digest.go" + }, + { + "from": "core.tick_loop", + "to": "state.tick_memo", + "kind": "writes", + "label": "lastPhrase, lastTrace, digestQ", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick.go" + }, + { + "from": "core.tick_loop", + "to": "state.facts", + "kind": "writes", + "label": "the cooldown feedback fact from tune()", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/tick.go tune; internal/loop/feedback.go FeedbackKey" + }, + { + "from": "core.dispatcher", + "to": "core.sink_voice", + "kind": "calls", + "label": "present", + "confidence": "high", + "status": "implemented", + "evidence": "internal/delivery/channel.go ChannelsFor" + }, + { + "from": "core.dispatcher", + "to": "core.sink_ntfy", + "kind": "calls", + "label": "away, sev3+", + "confidence": "high", + "status": "configured-off", + "evidence": "wireNtfySink returns nil when disabled" + }, + { + "from": "core.dispatcher", + "to": "core.sink_telegram", + "kind": "calls", + "label": "away sev4, repeat til ack", + "confidence": "high", + "status": "implemented", + "evidence": "internal/delivery/channel.go" + }, + { + "from": "core.dispatcher", + "to": "state.delivery_attempts", + "kind": "writes", + "label": "begin before send, complete after", + "confidence": "high", + "status": "implemented", + "evidence": "internal/delivery/dispatcher.go beginOutbox" + }, + { + "from": "core.dispatcher", + "to": "state.nudges", + "kind": "writes", + "label": "recorded AFTER a successful send", + "confidence": "high", + "status": "implemented", + "evidence": "internal/delivery/dispatcher.go NudgeRecorder" + }, + { + "from": "core.sink_voice", + "to": "core.voice_server", + "kind": "calls", + "label": "PushToMostRecent on the request conn", + "confidence": "high", + "status": "implemented", + "evidence": "internal/delivery/voicesink/" + }, + { + "from": "core.sink_telegram", + "to": "ext.telegram", + "kind": "http", + "label": "sendMessage through the SOCKS relay", + "confidence": "high", + "status": "implemented", + "evidence": "mavend.json telegram.proxy" + }, + { + "from": "ext.telegram", + "to": "core.telegram_intake", + "kind": "http", + "label": "getUpdates long poll", + "confidence": "high", + "status": "implemented", + "evidence": "internal/delivery/telegramsink/intake.go" + }, + { + "from": "proc.mavend", + "to": "core.tick_loop", + "kind": "spawns", + "label": "named worker `tick`", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/boot.go backgroundWorkers" + }, + { + "from": "proc.mavend", + "to": "core.fact_enrichment", + "kind": "spawns", + "label": "named worker `fact-enrichment`", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/boot.go" + }, + { + "from": "proc.mavend", + "to": "core.memory_eval", + "kind": "spawns", + "label": "named worker `memory-eval`", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/boot.go, nil unless configured" + }, + { + "from": "proc.mavend", + "to": "core.feed_worker", + "kind": "spawns", + "label": "named worker `feed`", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/boot.go" + }, + { + "from": "proc.mavend", + "to": "core.crawl_worker", + "kind": "spawns", + "label": "named worker `crawl`", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/boot.go" + }, + { + "from": "proc.mavend", + "to": "core.mcp_worker", + "kind": "spawns", + "label": "named worker `mcp`", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/boot.go" + }, + { + "from": "proc.mavend", + "to": "core.home_worker", + "kind": "spawns", + "label": "named worker `home`", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/boot.go" + }, + { + "from": "proc.mavend", + "to": "core.voice_server", + "kind": "spawns", + "label": "named worker `voice`", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/boot.go" + }, + { + "from": "proc.mavend", + "to": "core.telegram_intake", + "kind": "spawns", + "label": "bare goroutine, NOT in backgroundWorkers", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/telegramintake.go wg.Add(1); go func(){...}()" + }, + { + "from": "core.fact_enrichment", + "to": "state.facts", + "kind": "writes", + "label": "entity_id and resolution_state", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/factenrichment.go resolveOne" + }, + { + "from": "core.fact_enrichment", + "to": "ext.nexus", + "kind": "http", + "label": "Resolve with backoff", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/factenrichment.go" + }, + { + "from": "core.feed_worker", + "to": "state.notes", + "kind": "writes", + "label": "one note per item, source rss:", + "confidence": "high", + "status": "implemented", + "evidence": "internal/rss/poller.go" + }, + { + "from": "core.feed_worker", + "to": "state.facts", + "kind": "writes", + "label": "the feed watermark", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/feeds.go" + }, + { + "from": "core.crawl_worker", + "to": "state.facts", + "kind": "writes", + "label": "crawl:hash: watermark", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/crawls.go hashKey" + }, + { + "from": "core.crawl_worker", + "to": "state.notes", + "kind": "writes", + "label": "changed page text", + "confidence": "high", + "status": "implemented", + "evidence": "internal/crawl/watch.go" + }, + { + "from": "core.mcp_worker", + "to": "state.tools", + "kind": "writes", + "label": "proposed rows", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/mcp.go propose" + }, + { + "from": "core.home_worker", + "to": "state.tools", + "kind": "writes", + "label": "proposed destructive rows", + "confidence": "high", + "status": "configured-off", + "evidence": "cmd/mavend/smarthome.go propose" + }, + { + "from": "core.netscan", + "to": "state.notes", + "kind": "writes", + "label": "one scan record", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/netscan.go writeScanRecord" + }, + { + "from": "proc.mavweb", + "to": "state.facts", + "kind": "writes", + "label": "POST /api/signal presence, POST /api/ambient meeting time", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavweb/ambient.go, cmd/mavweb/facts.go" + }, + { + "from": "proc.mavweb", + "to": "state.tools", + "kind": "writes", + "label": "POST /tools enable/disable", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavweb/pages.go handleTools" + }, + { + "from": "proc.mavweb", + "to": "state.proposed_routines", + "kind": "writes", + "label": "POST /routines accept/dismiss", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavweb/pages.go handleRoutines" + }, + { + "from": "proc.mavweb", + "to": "state.routing_labels", + "kind": "writes", + "label": "POST /api/correct", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavweb/main.go handleCorrectAPI" + }, + { + "from": "proc.mavweb", + "to": "state.facts", + "kind": "writes", + "label": "POST /api/revert voids the latest fact for a key", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavweb/main.go handleRevert" + }, + { + "from": "proc.mavweb", + "to": "state.passkey_file", + "kind": "writes", + "label": "WebAuthn credential store", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavweb/credentials.go -passkey-file" + }, + { + "from": "proc.mavweb", + "to": "state.wrapped_key", + "kind": "writes", + "label": "StoreEncryptionKey after an assertion", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavend/main.go srv.WrapKeyFn, called by mavweb" + }, + { + "from": "state.db", + "to": "state.db_file", + "kind": "persists", + "label": "sealed on Close", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/crypt.go" + }, + { + "from": "state.db", + "to": "state.db_tmpfs", + "kind": "persists", + "label": "decrypted working copy in RAM", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/crypt.go" + }, + { + "from": "proc.mavseal", + "to": "state.db_file", + "kind": "writes", + "label": "recovery re-seal", + "confidence": "high", + "status": "implemented", + "evidence": "cmd/mavseal/main.go" + }, + { + "from": "state.db", + "to": "state.facts", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.reminders", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.nudges", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.notes", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.memory_vectors", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.tools", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.presence_state", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.events", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.proposed_routines", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.digest_entries", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.delivery_attempts", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.ack_sends", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.dialogue_sessions", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.tasks", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.list_items", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.routing_traces", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.routing_labels", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.ecosystem_traces", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "state.db", + "to": "state.meta", + "kind": "contains", + "label": "table", + "confidence": "high", + "status": "implemented", + "evidence": "internal/store/schema.sql and internal/store/migrations.go" + }, + { + "from": "core.action_act", + "to": "ext.vikunja_mcp", + "kind": "http", + "label": "an enabled MCP tool executes through tool.Executor.WithMCP", + "confidence": "medium", + "status": "configured-off", + "evidence": "cmd/mavend/voicewire.go exec = exec.WithMCP(w.mcp.caller()); no MCP server is enabled, so no such row can exist today" + }, + { + "from": "core.action_act", + "to": "ext.homeassistant", + "kind": "http", + "label": "an enabled house tool executes through tool.Executor.WithHome", + "confidence": "medium", + "status": "configured-off", + "evidence": "cmd/mavend/voicewire.go exec = exec.WithHome(w.home.caller()); smarthome.enabled = false" + }, + { + "from": "core.ecosystem", + "to": "state.ecosystem_traces", + "kind": "writes", + "label": "one row per ecosystem call", + "confidence": "medium", + "status": "implemented", + "evidence": "internal/store/ecotraces.go:34 Store.WriteEcosystemTrace; the call site was not read in full" + }, + { + "from": "core.memory_eval", + "to": "state.notes", + "kind": "reads", + "label": "the recall fixture", + "confidence": "low", + "status": "configured-off", + "evidence": "internal/memeval/eval.go references WriteNote; the worker is nil on this deployment so the path was not traced" + }, + { + "from": "proc.mavcaldav", + "to": "state.reminders", + "kind": "reads", + "label": "the render side publishes pending reminders back as iCal", + "confidence": "medium", + "status": "built-not-deployed", + "evidence": "cmd/mavcaldav/render.go; -render-url is off and checkRenderTarget refuses reading its own writes" + }, + { + "from": "router.claim", + "to": "router.cascade", + "kind": "calls", + "label": "the arbiter that would read claims", + "confidence": "high", + "status": "planned-unwired", + "evidence": "internal/router/claim.go: 'Nothing in Route calls this yet.'" + } + ] +} \ No newline at end of file diff --git a/docs/architecture/pack_evidence.sh b/docs/architecture/pack_evidence.sh new file mode 100755 index 0000000..05f6c18 --- /dev/null +++ b/docs/architecture/pack_evidence.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Build maven-evidence.zip: the architecture package plus the source seams a +# reviewer needs to test its claims, and nothing else. +# +# sh docs/architecture/pack_evidence.sh +# +# Three rules this script exists to enforce: +# +# 1. Whole files, never snippets. A cut-down file loses the call path that +# makes a claim checkable, which is the whole point of sending source. +# 2. Allowlist, not denylist. Paths are named one by one below. A denylist +# ships whatever nobody thought to exclude, and this tree has a database +# key in it. +# 3. Refuse rather than warn. The scan at the end aborts on a hit instead of +# printing something a tired person scrolls past. +# +# The one file that is not verbatim is docker-compose.yml. It carries a live +# uptime-kuma API key, so a redacted copy goes in its place and the redaction is +# recorded in architecture-evidence.txt and printed here. +set -euo pipefail + +here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +root=$(CDPATH= cd -- "$here/../.." && pwd) +cd "$root" + +out=maven-evidence.zip +stage=$(mktemp -d) +trap 'rm -rf "$stage"' EXIT + +echo "== regenerating the architecture package" +python3 "$here/build_inventory.py" +python3 "$here/verify_anchors.py" # exits 1 if any claim no longer resolves +python3 "$here/build_evidence.py" +python3 "$here/build_viewer.py" + +echo "== structural context" +# `tree` here is an eza alias in the owner's shell and absent in a plain sh, so +# the listing is generated with find and does not depend on either. +{ + echo "# find -L internal cmd -maxdepth 3 -type d" + echo + find internal cmd -maxdepth 3 -type d | sort + echo + echo "# go files per package" + echo + find internal cmd -name '*.go' ! -name '*_test.go' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn + echo + echo "# test files per package" + echo + find internal cmd -name '*_test.go' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn +} > "$here/tree.txt" + +echo "== redacting the one credential in docker-compose.yml" +sed 's/"uk5_[^"]*"/""/' docker-compose.yml \ + > "$here/docker-compose.redacted.yml" +if grep -q 'uk5_' "$here/docker-compose.redacted.yml"; then + echo "pack_evidence.sh: redaction failed, refusing to build" >&2; exit 1 +fi +diff <(sed 's/"uk5_[^"]*"/X/' docker-compose.yml) \ + <(sed 's/""/X/' "$here/docker-compose.redacted.yml") \ + >/dev/null || { echo "pack_evidence.sh: redacted copy differs by more than the key" >&2; exit 1; } + +# ---- the allowlist ------------------------------------------------------- +# Requested and present. internal/session, internal/db and tests/ are absent +# from this repo; architecture-evidence.txt says where their contents live. +paths=( + docs/architecture + CLAUDE.md + docs/CLAUDE.md + go.mod + deploy/mavend.json # ${VAR} placeholders only; 16 off-claims read it + cmd/mavend + internal/auth + internal/tool + internal/claim + internal/modes + internal/router + internal/voice + internal/ipc # the boundary auth.Can actually runs on + internal/store + internal/dialogue # clarify + session state the turn path parks in + internal/decision # the arbitration record + internal/delivery/channel.go + internal/loop + internal/webauthn # the other half of the auth story + cmd/mavwaked/main.go # the client that sends Surface + cmd/mavweb/main.go # the six unguarded surfaces +) + +echo "== staging" +for p in "${paths[@]}"; do + if [ ! -e "$p" ]; then echo " MISSING $p (skipped)"; continue; fi + mkdir -p "$stage/$(dirname "$p")" + cp -r "$p" "$stage/$(dirname "$p")/" +done + +# Generated-in-place files that must not travel, and anything that is a secret, +# a model, a database or a build artefact regardless of how it got staged. +# +# The name filters skip .go on purpose: internal/router/singletoken.go matched +# '*token*' and was deleted out of the first build of this pack. That is exactly +# the silent hole an allowlist exists to prevent, and a Go source file is never +# the thing this clause is for. +find "$stage" ! -name '*.go' \( \ + -name '*.db' -o -name '*.sqlite*' -o -name '*.enc' \ + -o -name '*.pem' -o -name '*.key' -o -name '*.crt' -o -name '*.p12' \ + -o -name '.env*' -o -name '*.token' -o -name '*.secret' -o -name '*.password' \ + -o -name '*.onnx' -o -name '*.gguf' -o -name '*.bin' -o -name '*.wav' \ + -o -name '*.zip' -o -name '*.log' -o -name '.git' \ + \) -print -exec rm -rf {} + 2>/dev/null || true + +echo "== scanning the staged tree" +# A value-shaped assignment: a credential word, a delimiter, then twelve or more +# characters of value. The value must NOT begin with a slash or a dot, because a +# docker volume line pairs a host path with a container path and both halves end +# in the same secret-sounding filename while containing no secret. Three of those +# in docker-compose.yml tripped the first version of this scan. +hits=$(grep -rInE '(api[_-]?key|secret|passwo?r?d|bearer|token)["'"'"' ]*[:=]["'"'"' ]*[A-Za-z0-9+_-][A-Za-z0-9/+_-]{11,}' "$stage" \ + | grep -vE '\$\{|&2 + echo "$hits" >&2 + exit 1 +fi + +echo "== building $out" +rm -f "$out" +( cd "$stage" && zip -qr "$root/$out" . ) + +echo +printf '%s %s %s files\n' "$out" \ + "$(du -h "$out" 2>/dev/null | cut -f1)" \ + "$(unzip -l "$out" | tail -1 | awk '{print $2}')" +echo +echo "redacted: docker-compose.yml -> docs/architecture/docker-compose.redacted.yml" +echo " one uptime-kuma api key, nothing else" +echo "excluded: .git, deploy/telegram.env, deploy/db_key.env, models, deps, databases" diff --git a/docs/architecture/render.sh b/docs/architecture/render.sh new file mode 100755 index 0000000..f1b4adc --- /dev/null +++ b/docs/architecture/render.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# Re-render every diagram in diagrams/*.mmd to a committed SVG beside it, then +# rebuild index.html so the viewer carries the new pictures. +# +# mermaid-cli drives a real browser through puppeteer. It downloads its own +# chrome-headless-shell by default, which fails behind a proxy and wastes +# 150 MB; PUPPETEER_EXECUTABLE_PATH points it at the system chromium instead. +# --no-sandbox is required because that chromium is not the one puppeteer +# provisioned and has no sandbox helper of its own here. +# +# sh docs/architecture/render.sh +# +# Run it from anywhere. A parse error in one file leaves the others alone and +# prints FAIL with the mermaid error, which is the only way this repo has to +# syntax-check a .mmd. +set -eu + +here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +dia="$here/diagrams" +cfg=$(mktemp) +trap 'rm -f "$cfg"' EXIT +printf '{"args":["--no-sandbox","--disable-gpu"]}' > "$cfg" + +: "${PUPPETEER_EXECUTABLE_PATH:=$(command -v chromium || command -v chromium-browser || command -v google-chrome-stable || true)}" +if [ -z "$PUPPETEER_EXECUTABLE_PATH" ]; then + echo "render.sh: no chromium found. Install one, or set PUPPETEER_EXECUTABLE_PATH." >&2 + exit 1 +fi +export PUPPETEER_EXECUTABLE_PATH + +for f in "$dia"/*.mmd; do + n=$(basename "$f" .mmd) + err=$(mktemp) + if npx --yes @mermaid-js/mermaid-cli@11 -p "$cfg" -t dark -b '#0e1116' \ + -i "$f" -o "$dia/$n.svg" >/dev/null 2>"$err" && [ -s "$dia/$n.svg" ]; then + echo "OK $n" + else + echo "FAIL $n" + grep -m1 -A3 'Parse error' "$err" || tail -3 "$err" + fi + rm -f "$err" +done + +python3 "$here/build_viewer.py" diff --git a/docs/architecture/verify_anchors.py b/docs/architecture/verify_anchors.py new file mode 100644 index 0000000..9b6c01f --- /dev/null +++ b/docs/architecture/verify_anchors.py @@ -0,0 +1,155 @@ +#!/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()) diff --git a/docs/architecture/viewer.template.html b/docs/architecture/viewer.template.html new file mode 100644 index 0000000..633214d --- /dev/null +++ b/docs/architecture/viewer.template.html @@ -0,0 +1,494 @@ + + + + + +Maven architecture — as built + + + +
+
+

Maven — architecture as built

+
commit · ·
+
+
+ + + +
+ +
+ + + +