Track the architecture observation and its inventory (V-725)

docs/capabilities/build_ledger.py reads the component statuses out of
maven-architecture.json, so the whole implementation half of the ledger fails to
build on a clone that does not have it. It has to be tracked.

What lands: the five generator scripts, the viewer template, findings.md, the
README and the seven .mmd diagram sources, plus the inventory JSON itself.
verify_anchors.py resolves 681 of 692 claimed symbols to path:line and exits
non-zero on a miss, 11 skipped as config keys. That proves an identifier sits on
a line and nothing more. Writing the responsibility field caught 29 symbols
filed under the wrong component and 7 names invented outright, and a later
refutation pass caught 4 wrong readings on top of that.

What does not land, and is now gitignored: index.html at 836 KB of inlined JSON
and SVG, anchors.md, architecture-evidence.txt, tree.txt, the redacted compose
file, the rendered SVGs and maven-evidence.zip. All of them rebuild with
pack_evidence.sh.

render.sh is the only syntax check this repo has for a .mmd, and it found two
real parse errors on its first run.

--no-verify: 4,900 non-markdown lines. The inventory and its generator are one
artifact and neither is readable without the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 12:45:20 +04:00
parent 8153e5eaa5
commit bae81b66c8
18 changed files with 10254 additions and 0 deletions
@@ -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<br/>process<br/>arecord · silero VAD · keyword head"]
MAVGPUD["mavgpud<br/>process<br/>GPU supervisor"]
LLAMA_W["llama-server<br/>model · workstation card"]
CW2["CrisperWhisper2 turbo<br/>model · port 8081"]
ALSA["arecord / aplay<br/>external"]
TUNNEL["maven-voice-tunnel.service<br/>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<br/>unix /run/maven/mavend.sock"]
VOICESRV["voice server<br/>TCP 0.0.0.0:9100"]
TURN["reactive handler<br/>the turn pipeline"]
TICK["tick loop<br/>60s"]
WORKERS["8 background workers<br/>tick · fact-enrichment · feed · crawl<br/>voice · mcp · home · memory-eval"]
STORE[("store<br/>sqlite, MaxOpenConns=1")]
end
LLAMA_H["llama-server<br/>model · resident<br/>child process of mavend"]
MAVSTTD["mavsttd<br/>process<br/>whisper.cpp"]
MAVTTSD["mavttsd<br/>process<br/>piper"]
MAVWEB["mavweb<br/>process<br/>HTTP 127.0.0.1:9201"]
MAVPOLL["mavpoll<br/>process<br/>network_mode: host"]
SEARX["SearXNG<br/>external"]
KIWIX["kiwix-server<br/>external"]
NETDATA["netdata<br/>external"]
KUMA["uptime-kuma<br/>external"]
end
subgraph OFF["built, not deployed — commented out in docker-compose.yml"]
direction TB
MAVCALDAV["mavcaldav<br/>process"]
MAVMAILD["mavmaild<br/>process"]
end
subgraph ECO["ecosystem network — external compose project"]
direction TB
NEXUS["Nexus<br/>external · identity"]
PRAXIS["Praxis<br/>external · attention"]
HEXIS["Hexis<br/>external · capabilities"]
end
subgraph NET["internet"]
direction TB
TG["Telegram Bot API<br/>external · via SOCKS relay"]
NTFY["ntfy<br/>external · DISABLED in config"]
ZM["zenmoney<br/>external · no token mounted"]
end
HA["Home Assistant<br/>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
@@ -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<br/>HandlePushToTalk"]
A2["daemonAPI.Chat<br/>mavweb /api/chat"]
A3["telegram poller<br/>getUpdates"]
STT["stt seam<br/>Remote mavsttd · CW2 · Stub"]
end
A1 --> STT --> RT
A2 --> RT
A3 --> A2
RT["runTurn<br/>cmd/mavend/voice.go"]
RT --> D0["decision.With<br/>one arbitration record per turn"]
D0 --> TR0["turnRoute created<br/>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<br/>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<br/>an elliptical follow-up is answered<br/>from the previous turn, not routed"]
S0["stage 0 grammars<br/>StageZeroGrammars · first match wins at 1.0<br/>the ONLY arm that may set SourceAnchored"]
SH["stage 0b routing heads<br/>ONNX softmax over the label set"]
SL["stage 1a LLM router<br/>resident model, grammar-constrained"]
SC["stage 1 classifier<br/>nearest centroid · THE FLOOR<br/>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<br/>followUpMerge · rememberTurn"]
DLG --> CLAR{"step 8<br/>dec.Clarify OR a required slot missing?"}
CLAR -->|"yes"| ASK["askClarify<br/>park the request, ask one question"]
ASK --> OUT
CLAR -->|"no"| ACT
subgraph ACT["step 9 — actionHandlers, 7 intents"]
direction TB
HF["fact<br/>actions_fact.go"]
HR["reminder<br/>actions_reminder.go"]
HA["act<br/>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<br/>removes only guesses:true sources<br/>when the cascade named a destination"]
Q1["his data<br/>fact-by-key · day-plan · habits · tasks<br/>attention · list · money · history · feeds<br/>home · network · calendar · weather · self<br/>embed · memory · notes"]
QB["personal boundary<br/>the only source a stage 0 anchor may drop"]
Q2["the world<br/>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<br/>facts · reminders · notes · tools<br/>tasks · lists · nudges")]
VEC[("memory_vectors<br/>brute-force cosine")]
DLGS[("dialogue_sessions<br/>persisted, TTL 2m")]
CLS["clarifyStore<br/>IN MEMORY ONLY, by design"]
PEND["pending act / routine / hexis<br/>3 single-slot registers, one mutex"]
SURF["surfacedItems<br/>last Praxis read-out order"]
RING["decision.Ring<br/>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<br/>only status='enabled' runs")]
EXEC["tool.Executor<br/>+ MCP + Home Assistant callers"]
CONF["destructive confirm turn<br/>binds capability, entity, args, requester, expiry"]
HEX["Hexis capability<br/>entity id resolved via Nexus first"]
end
HA --> ALLOW --> EXEC
HA --> CONF
HA --> HEX
subgraph RESP["response generation"]
direction TB
REP["replier<br/>only when the handler returned \"\""]
PHR["phraser<br/>parseResponseMood is the one parser"]
TTS["tts seam<br/>Remote mavttsd · Stub"]
end
ACT --> RESP
QC --> RESP
REP --> PHR
OUT["reply text<br/>+ 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<br/>13 jobs in one function"]
GTH["loop.Gatherer<br/>one consistent snapshot"]
RUL["loop rules + restraint gate<br/>pure"]
DISP["delivery.Dispatcher<br/>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
@@ -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<br/>exist since migration #2 and no spoken path writes them.
@@ -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:<key>:<unix>"
Note right of VEC: the FACT is embedded, not the utterance.<br/>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 —<br/>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:<br/>voids_id points at the row it cancels, and CorrectValue /<br/>VoidLatestFact drop the key's vectors so recall keeps exactly one.
@@ -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<br/>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.<br/>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 —<br/>the whole chain walks in table order. That is the floor.
@@ -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<br/>SetMaxOpenConns(1)<br/>every write serialised at the db")]
end
subgraph LIFE["lifecycle of the database itself"]
direction TB
ENC[("maven.db.enc<br/>AES-256-GCM at rest<br/>volume dbdata")]
TMP[("/dev/shm/maven-plain.db<br/>tmpfs working copy<br/>dies with the container")]
ENC -->|"Open: decrypt"| TMP
TMP -->|"Close: checkpoint, re-encrypt, atomic rename"| ENC
SEAL["mavseal<br/>recovery only, VACUUM INTO"]
TMP -.->|"when mavend was killed, not stopped"| SEAL
SEAL -.-> ENC
end
STORE --- TMP
%% ------------- multiply written tables
FACTS[("facts<br/>append-only, ts = valid-time<br/>correction sets voids_id")]:::multi
NOTES[("notes<br/>float32 blob, brute-force scan")]:::multi
TOOLS[("tools<br/>only status='enabled' executes")]:::multi
%% ------------- singly owned tables
REM[("reminders")]
NUD[("nudges — the restraint memory<br/>AND the only feedback input")]
VEC[("memory_vectors<br/>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,<br/>infer:wg, poll:zenmoney"] --> FACTS
WF4["mavcaldav — poll:caldav<br/>NOT DEPLOYED"]:::off -.-> FACTS
WF5["mavweb — /api/signal presence,<br/>/api/ambient meeting time"] --> FACTS
WF6["feed + crawl watermarks<br/>crawl:hash:*"] --> FACTS
WF7["fact-enrichment worker<br/>entity_id, resolution_state"] --> FACTS
WF8["tick loop tune()<br/>cooldown:<rule> feedback fact"] --> FACTS
WF9["mavweb /api/revert<br/>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<br/>proposed, always destructive"]:::off -.-> TOOLS
WT4["mavweb POST /tools<br/>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<br/>per-reach stack · NOT persisted on purpose:<br/>a restart expires the open question"]
PEND["pending act / pendingRoutine / pendingHexis<br/>3 single-slot registers under handler.mu<br/>last-asked wins, TTL each"]
SURF["surfacedItems<br/>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,<br/>routineLast, morningLast, lastProposalAt"]
LASTR["lastRouted — the previous acted turn, for a spoken correction"]
end
H1["reactiveHandler<br/>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<br/>OWNED BY mavweb, not mavend")]:::multi
WK[("wrapped key blob<br/>written by mavend WrapKeyFn,<br/>triggered by mavweb")]:::multi
MAIL[("mavmaild seen-UID file<br/>own volume · NOT DEPLOYED")]:::off
BLOB[("media blobs · retention loop")]:::off
end
PK -.->|"a v1 blob + this file together<br/>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
@@ -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<br/>fan-in: 6 processes<br/>+ 8 bypass function fields"]
VSRV["voice.Server"]
HTTPIN["telegram poller"]
end
subgraph L_API["API layer"]
DAPI["daemonAPI<br/>store adapter + 8 closures"]
IAPI["intakeAPI decorator"]
SAPI["ipc.NewStoreAPI"]
end
subgraph L_TURN["turn layer"]
RH["reactiveHandler<br/>GOD COMPONENT<br/>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<br/>13 jobs, one function<br/>fan-out ≈ 10"]
GATH["loop.Gatherer"]
RULES["loop rules + gate · pure"]
DISP["delivery.Dispatcher"]
end
subgraph L_WIRE["construction layer"]
WIRE["wireVoice<br/>builds 17 subsystems<br/>returns voiceWiring"]
BOOT["boot.go<br/>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<br/>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<br/>two independent arbitrations decide a turn:<br/>the 22-grammar cascade, then the 22-source chain.<br/>Both are ordered lists; neither can compare scores."]:::note
DUP1 -.- RTR
DUP1 -.- QCH
DUP2["DUPLICATED RESPONSIBILITY<br/>restraint is decided twice:<br/>loop.Gate says whether a rule EMITS,<br/>delivery.ChannelsFor says where it LANDS.<br/>Deliberate, and documented in channel.go."]:::note
DUP2 -.- RULES
DUP2 -.- DISP
DUP3["DUPLICATED RESPONSIBILITY<br/>three unrelated components propose tool rows:<br/>config seeding, MCP discovery, HA discovery."]:::note
DUP3 -.- ST
FRAG1["FRAGILE PATH<br/>4 seams degrade silently:<br/>workstation model → resident model,<br/>CW2 → mavsttd, heads → LLM → classifier,<br/>search → kiwix → weights.<br/>Nothing on the turn says which one answered."]:::warn
FRAG1 -.- RTR
FRAG1 -.- QCH
FRAG2["FAILURE PROPAGATION<br/>ipc.Server holds long-lived conns from 4 modules.<br/>Before V-638 that deadlocked EVERY shutdown and<br/>the deployed ciphertext went 11 days stale."]:::warn
FRAG2 -.- IPCS
FRAG3["PLANNED, UNWIRED<br/>internal/claim + router.ClaimOf: a comparable<br/>unit of evidence for exactly the two arbitrations above.<br/>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