diff --git a/docs/spec.md b/docs/spec.md new file mode 100644 index 0000000..f56ee91 --- /dev/null +++ b/docs/spec.md @@ -0,0 +1,617 @@ +# Spec: every capability, and what done means for each + +*Last verified: 2026-08-15 @ 231248a* + +What Maven is for, capability by capability, with a definition of done that can +be observed on the running box. `CLAUDE.md` carries the rules. `docs/roadmap.md` +carries the order. This file carries the target. + +The capability list is the union of two sources. The 39 rows measured in +`docs/evals/2026-08-13-capability-audit.md`, and the 18 items the owner named as +v1 scope. Twelve of the owner's items had no audit row, so this file has 51. + +## How to read an entry + +**State** is a reference, never a word. It points at the living doc that holds +the current or the desired implementation. Where no such doc exists, the entry +says so, and that absence is a finding this file is surfacing on purpose. + +**DoD** is a plain list of observable conditions. Each item is checkable on the +running stack. An utterance and its answer, a log line, an outbox row, an HTTP +response, or a store row. + +**Scenario** names a file in `cmd/mavend/testdata/scenarios/`, which scopes the +flow from utterance to output. Five exist today: `act_degraded`, +`assistant_workday`, `conversation_anaphora`, `evening_degraded`, +`morning_missed`. A name marked *(to write)* does not exist yet, and writing it +is part of that capability's work. + +Every clock time in this file is `Europe/Samara`, declared once in +`docker-compose.yml` with `/etc/localtime` pointed at it (V-545). Every path +reads `time.Local`. + +## What v1 means + +v1 is a voice assistant, minimum viable. Voice is the spine and the capability +list is the surface. Each DoD below is written at "voice-reachable and honest", +not at "feature-complete". Honest means she names the gap when she cannot do the +job, and never fills it with a guess. + +Seven capabilities in v1 have no design yet. They keep a DoD here, written at +what done would look like, and their state entry names the missing doc. The gap +stays visible instead of implied-done. + +## The turn + +### Route an utterance + +- **State**: `docs/routing.md`. +- **DoD** + - Seven intents score separately from the source, on every turn. + - A question about what Maven remembers routes to query, not remember. The + audit's two misrouted probes of seven pass. + - Any resident-model error falls through to the classifier and the turn still + answers. + - Every turn writes a decision trace naming the stage that decided. +- **Scenario**: `conversation_anaphora`, `assistant_workday`. + +### Ask instead of guessing + +- **State**: `docs/routing.md`, the clarify head and the parked clarify ride. +- **DoD** + - An incomplete reminder parks and asks for the missing slot, not for all of them. + - "отмена" during a parked clarify drops the parked turn and says so. + - A parked clarify survives an interleaved unrelated turn and resumes. +- **Scenario**: `conversation_anaphora`. + +### Speak as herself + +- **State**: `docs/language.md`. +- **DoD** + - No masculine self-reference reaches the wire. The audit caught "Хорошо, + сохранил" live while `CheckFeminine` passed in the eval, so the check runs on + the outbound path and not only in the eval. + - She says "ты" throughout, and no pet name appears. + - A rejected reply is logged with what failed, not silently rephrased. +- **Scenario**: `morning_missed` pins the constant. `persona_wire` *(to write)* + pins model output on the outbound path. + +### Answer from your own data + +- **State**: `docs/routing.md`, `queryWalk` in `cmd/mavend/actions_query.go`. +- **DoD** + - "что у меня сегодня по плану?" returns the real checklist and its open items. + - The owner's sources are asked before anything outside, every time. + - A source that looks rather than guesses is asked even when a destination is named. +- **Scenario**: `assistant_workday`, `morning_missed`. + +### Answer from the world + +- **State**: `docs/world.md`. +- **DoD** + - A general-knowledge question returns a Russian summary that does not invent + physics. The audit's "почему небо голубое?" answer is the failing case. + - Only the utterance leaves the box. No persona block, no history, no matched notes. + - Deleting the `search` block turns the capability off with a named gap, not an error. +- **Scenario**: `world_summary_quality` *(to write)*. + +### Read an encyclopedia + +- **State**: `docs/world.md`, the Kiwix section. +- **DoD** + - A Russian question lands on the Russian book and an English one on the English book. + - Kiwix answers when the line is down. + - The retrieved article is on the question's topic, not merely a lexical match. +- **Scenario**: `kiwix_language_pick` *(to write)*. + +### Weather + +- **State**: `internal/weather`. No living doc covers it. **Finding**: the + provider seam, the home city and the clarify path have no written reasoning + anywhere. The audit found the capability broken on configuration alone. +- **DoD** + - A `weather` block in `deploy/mavend.json` names a provider and a home city. + - "какая сейчас погода?" answers for the home city without asking. + - Naming another city answers for that city. The audit's follow-up "Самара" + died with "Я тебя не разобрала", so the follow-up parks as a clarify instead + of emitting a question through the answer path. + - With no provider configured she names the gap and does not guess a forecast. +- **Scenario**: `weather_followup` *(to write)*. + +### See an image + +- **State**: `internal/vision`, the seam that stores the image and says so. No + living doc. **Finding**: V-667 has the gemma-4 mmproj on the box and no + written contract for what a vision call returns. +- **DoD** + - An image sent through Telegram gets a Russian description. + - With no vision model configured she says she cannot look, and the image is stored. + - The vision call goes to the workstation and falls back silently when it is down. +- **Scenario**: `vision_degraded` *(to write)*. + +## Memory + +The four memory rows below have no living doc. **Finding**: `docs/design.md` +sketches the store and `docs/routing.md` covers recall's routing. Nothing owns +the fact and note contracts, the supersede rule, or the digestion worker's +consolidation pass. This is the largest documentation gap in the list. + +### Facts + +- **State**: `internal/store/facts.go`, `cmd/mavend/factenrichment.go`. No living doc. +- **DoD** + - A stated fact is written and confirmed in his own words, in the feminine. + - A confirmation that says she wrote something is never emitted without the + row existing. The audit's "я записала информацию о тебе" wrote nothing. + - A superseding fact retires the old value and both are readable. + - `actionFact.Subject` resolves through Nexus, never through a local key. +- **Scenario**: `morning_missed`. + +### Notes + +- **State**: `internal/memory`. No living doc. +- **DoD** + - A note captured from any reach is recallable by question. + - A note can be deleted by voice and from the web UI. `/api/revert` voids facts + by key and nothing voids a note today (V-494). + - A question is not stored as a statement. The audit's "я рассказывал тебе про + байкал?" became a junk note. +- **Scenario**: `note_delete` *(to write)*. + +### Recall + +- **State**: `docs/routing.md` for the query walk and the personal boundary. +- **DoD** + - The embedder loads at 384 dimensions with the marker check passing, on every start. + - `EmbedQuery` and `EmbedPassage` carry their prefixes. A plain `Embed` on a note fails the build or the test. + - The personal boundary scores a question about him as personal. + `TestONNXPersonalBoundary` is green. + - A recall miss says she does not remember, and does not answer from the world instead. +- **Scenario**: `assistant_workday`. + +### Memory evaluation + +- **State**: `internal/memeval`. No living doc. **Finding**: the evaluator ships, + writes notes and cannot speak, and nothing records what its conclusions mean (V-248). +- **DoD** + - One evaluation run is observed on the box and its notes are read back. + - What it concluded is checkable against the notes it read. +- **Scenario**: `memeval_run` *(to write)*. + +## Proactive + +### Reminders + +- **State**: `internal/store`, `internal/delivery`. No living doc covers the + reminder lifecycle. **Finding**: parking, firing, delivery, retry and + cancellation are spread across three packages with no written contract. +- **DoD** + - A one-shot reminder set by voice fires at its time and is delivered. + - **Recurring works from speech**: meetings, pills, the dog, the vet and the + kibble. `store.Reminder` carries `Cron` and `ipc.CreateReminder` takes a cron + argument, and no caller in `cmd/mavend` passes one. Recurring is unbuilt with + its storage and delivery already finished under it. + - A recurring reminder states its schedule back when it is set, and again when + asked. + - Cancellation works by voice and from `/reminders`, and a refusal is honoured. + - A failed delivery retries into another reach rather than looping. The audit + watched the ntfy failure run once a minute until 03:05. +- **Scenario**: `recurring_reminders` *(to write)*. Cancellation is covered by the + V-719 eval. + +### Interruption policy + +- **State**: `docs/handler-wiring.md` for the dispatch decision. **Finding**: the + four presence-and-severity outcomes have never been written down as intended + behaviour, only as code (V-281). +- **DoD** + - The four outcomes are named in a doc before any of them changes. + - A severity-1 item with him present at the desk reaches him through some reach. + The audit logged `dropped morning:утро (sev1, presence=present)`. + - Nothing unprompted arrives during a quiet tick. +- **Scenario**: `morning_missed`, `evening_degraded`. + +### Digest of held nudges + +- **State**: `internal/worker`, the digestion worker. No living doc. +- **DoD** + - A suppressed nudge candidate is observed surfacing in a later digest, on the box. + - The semantic fingerprint is checked before the phraser is paid. + - Digestion never calls Hexis. +- **Scenario**: `evening_degraded`. + +### Morning routine + +- **State**: `internal/morning`, `internal/routine`. No living doc. +- **DoD** + - The morning plan reaches him inside its 08:00-11:00 window, `Europe/Samara`. + - When the voice reach has no session the plan falls back to the non-voice + reaches rather than being dropped (owner's call; related V-281). + - A missed window is stated as missed, not silently swallowed. +- **Scenario**: `morning_missed`. + +### Routine proposals + +- **State**: `internal/routine`. No living doc. **Finding**: the proposer reads a + hand-written Russian verb list, which the language rules forbid as a route or + fact source (V-606). +- **DoD** + - One proposal is observed on the box from real repeated behaviour. + - The verb list is replaced by `internal/lexicon` or the embedder. + - A proposal is a nudge he can decline, and declining it is stored. +- **Scenario**: `routine_proposal` *(to write)*. + +### Tasks + +- **State**: `internal/tasks`. No living doc. +- **DoD** + - Open tasks are read back ordered by deadline and urgency. + - A task captured by voice appears in the list and on the web UI. + - Mail-derived candidates are never spoken as tasks until he accepts one (V-130). +- **Scenario**: `assistant_workday`, `morning_missed`. + +### RSS and news + +- **State**: `internal/rss`. No living doc. +- **DoD** + - Feed items are read when asked and never announced unprompted. That is the + intended shape, not a defect. + - A question about a topic finds the matching item across the configured feeds. + - A dead feed names itself as dead, and the others still answer. +- **Scenario**: `morning_missed`. + +## Reach + +### Telegram + +- **State**: `docs/deployment.md`, `internal/delivery/telegramsink`. +- **DoD** + - An outbound message is delivered and the outbox row records the delivery. + - His chat is read continuously from restart. + - The socks relay being down names the gap and holds the message. +- **Scenario**: `evening_degraded`. + +### ntfy + +- **State**: `internal/delivery/ntfysink`, disabled in the committed config. +- **DoD** + - A write-scoped `NTFY_TOKEN` exists before re-enabling. It was switched off + after a 403 storm. + - A 403 stops retrying instead of looping once a minute. +- **Scenario**: `ntfy_403` *(to write)*. + +### Voice + +- **State**: `docs/protocol.md` for the wire, `internal/delivery/voicesink` for the sink. + **Finding**: the wire is documented and the listener is not. Nothing describes + what holds a live voice session open. +- **DoD** + - A proactive message reaches him by speech without him speaking first. Every + proactive message during the audit fell through with "no live voice session". + - The port stays on homesrv loopback and reaches workpc over ssh. + - `SurfaceVoice` caps acts at L0, and reading is not capped. +- **Scenario**: `voice_push` *(to write)*. + +### Web UI + +- **State**: `docs/deployment.md`. +- **DoD** + - All pages answer 200. + - Every capability with a surface has a page: reminders, notes, tasks, facts. + - A destructive action on a page is gated by step-up. +- **Scenario**: covered by `cmd/mavweb` tests, not by a scenario. + +### Desk notifications + +- **State**: `cmd/mavweb/ambient.go`, `internal/event`. No living doc. + **Finding**: the inbound direction exists as the `ambient:notif` source and the + outbound direction does not exist at all. Which one the owner means is an open + product decision. +- **DoD** + - Inbound: a desktop notification becomes a fact at the ambient path's own + confidence, filed low, and never spoken back unprompted. + - Outbound: a nudge can appear on the workpc desktop as a fourth reach, or the + outbound half is explicitly dropped from v1. +- **Scenario**: `morning_missed` covers inbound. Outbound has none. + +## Speech and senses + +### Speech to text + +- **State**: `docs/offload.md`, `docs/deployment.md`. +- **DoD** + - Russian speech transcribes accurately enough to route. `mavsttd` is the local + floor and the workstation transcriber is the better path. + - The workstation being down falls back to `mavsttd` silently. + - The floor arm is exercised on its own, not only behind the workstation. +- **Scenario**: covered by `docs/evals/2026-08-09-crisperwhisper2-russian-wer.md`. + +### Text to speech + +- **State**: `docs/offload.md`, `docs/deployment.md`. +- **DoD** + - A reply is spoken in Russian with correct number and abbreviation expansion. + - `internal/ttsnorm` expands times and dates into `Europe/Samara` phrasing. +- **Scenario**: `tts_normalisation` *(to write)*. + +### Wake word + +- **State**: `docs/deployment.md`, `mavwaked` under systemd on workpc. +- **DoD** + - "Мэйвен" wakes her and a near-miss does not. + - Waking her opens a voice session the proactive path can push into. Waking is + not the same as having a session (V-515). +- **Scenario**: `voice_push` *(to write)*. + +### Hearing + +- **State**: `internal/capture`, `internal/audio`. No capture client ships (V-514). +- **DoD** + - A capture client runs on workpc and streams to `mavsttd` or the workstation. + - The path is reachable end to end from microphone to reply. +- **Scenario**: `voice_push` *(to write)*. + +### Speaker recognition + +- **State**: `internal/speaker`. No living doc (V-255). +- **DoD** + - The owner's voice is distinguished from another voice. + - A voice that is not his cannot reach the act path. +- **Scenario**: `speaker_gate` *(to write)*. + +## The ecosystem + +### Nexus + +- **State**: `docs/ecosystem.md`. +- **DoD** + - Entities exist. Nexus answers "no entities yet" today, so every act naming a + target has nothing to resolve against. Seeding is a Nexus-side job. + - Free text resolves to a canonical entity id before any mutating call. + - Ambiguous resolution asks him and does not pick. + - Nexus down produces a named gap, not a broken turn. +- **Scenario**: `act_degraded`. + +### Praxis + +- **State**: `docs/ecosystem.md`. +- **DoD** + - An item needing attention is read back on request. + - Reading an item aloud calls `Surface`, never `Acknowledge`. + - Attention arrives over HTTP, never from its SQLite file. + - Digestion may summarise Praxis and may not call Hexis. +- **Scenario**: `morning_missed`, `evening_degraded`. + +### Hexis + +- **State**: `docs/ecosystem.md`. +- **DoD** + - An act runs against a real target once Nexus has entities. + - Confirmation binds capability id, target entity, arguments, requester and expiry. + - LLM output alone never authorizes. + - Every call carries a correlation id, a contract version and `X-Requested-By: maven`. +- **Scenario**: `act_degraded`. + +### Smart home + +- **State**: `internal/smarthome`, disabled in config (V-256). +- **DoD** + - A device is controlled through Hexis, resolved through Nexus, never by free text. + - Disabled means a named gap. +- **Scenario**: `act_degraded`. + +### Network scans + +- **State**: `internal/netscan`, `internal/netaddr`. No living doc. +- **DoD** + - A scan of the configured subnets returns hosts and open ports on request. + - The result is read back as prose, not as a table dump. + - The rate limit in the config is honoured. +- **Scenario**: `netscan_query` *(to write)*. + +### Bluetooth control + +- **State**: no package. **Finding**: nothing exists, and the box has no bluez + (V-257). This is the only v1 item blocked on the host rather than on code. +- **DoD** + - bluez is present on the box that owns the radio. + - A paired device is connected and disconnected by voice, through Hexis. +- **Scenario**: `bluetooth_control` *(to write)*. + +### MCPs + +- **State**: `internal/mcp`. No living doc. **Finding**: the allowlist, the + stdio and http transports and the webfetch door all exist. Nothing records + which servers may run, or why. +- **DoD** + - A configured MCP server's tools are callable through the act path. + - The allowlist is the only path to a tool, and a tool outside it is refused. + - A server that dies is a named gap, and the rest of the registry still answers. +- **Scenario**: `mcp_tool_call` *(to write)*. + +## Operations + +### The deployed stack + +- **State**: `docs/deployment.md`. +- **DoD** + - All five compose services run the current build. The audit found three on a + four-day-old image. + - `mavwaked` on workpc is on the current build too, or its drift is stated. + - A restart loses nothing. +- **Scenario**: none. This is checked by `docker compose ps` and the startup log. + +### Encrypted database + +- **State**: `docs/deployment.md`, `docs/caveats/storage.md`. +- **DoD** + - The database is encrypted at rest with the working copy in tmpfs. + - The key comes from the environment and is held only by `mavend`. + - Passwords are read from files, never taken as flag values. +- **Scenario**: none. Checked by `mavseal` and the startup log. + +### Passkey and step-up + +- **State**: `docs/caveats/security.md`, `internal/webauthn`, `internal/auth`. +- **DoD** + - WebAuthn is configured, so no step-up gate is fail-open. Today every one is, + including `POST /api/chat`, which reaches the act path (V-683). + - Enrollment requires an existing credential once the first one exists. + - Step-up is per-request, not process-global. +- **Scenario**: `stepup_gate` *(to write)*. + +### Model swap + +- **State**: `docs/deployment.md` (V-250). +- **DoD** + - `phraser.swap_models` lists the allowed gguf paths, or the page is removed. + - A swap survives a restart, or the page states that it will not. +- **Scenario**: none. + +### Self-update + +- **State**: `cmd/mavupdate`, `internal/update`. Blocked at step 3: it cannot + reach the containerized socket (V-477). +- **DoD** + - An update runs to completion from inside the deployment. + - A failed update rolls back and says so. +- **Scenario**: none. + +### Tests and analyzers + +- **State**: `docs/qa.md`, `docs/workflow.md`. +- **DoD** + - `make test` is green with `-race` and `MAVEN_ONNX_LIB` set, so the four + `TestONNX*` measurements run instead of self-skipping. + - `make analyze` passes against its baselines, and a fix deletes its entry. + - The one latency test that fails only under coverage is fixed or filed (V-718). +- **Scenario**: none. This gate is the suite itself. + +## Undesigned in v1 + +These seven have no design and no living doc. Each keeps a DoD written at what +done would look like, so the gap is visible instead of implied-done. A design +pass comes before any of them is built. + +### Email triage + +- **State**: `internal/email`, `cmd/mavmaild`. Built and **not in + `docker-compose.yml`**. **Finding**: the product decision comes first. What she + does with his mail is undecided, and deploying the daemon before deciding + writes the decision by accident. +- **DoD** + - The owner has decided what triage means: read-only summary, task extraction, or reply drafting. + - Mail-derived task candidates stay candidates until he accepts one (V-130). + - No mail content leaves the box. + - `mavmaild` is in compose, or its absence is deliberate and recorded. +- **Scenario**: `email_triage` *(to write)*. + +### Calendar management + +- **State**: `internal/calendar`, `cmd/mavcaldav`. Built and **not in + `docker-compose.yml`**. Same product decision as email. +- **DoD** + - Today's and tomorrow's events are read back, times in `Europe/Samara`. + - An event is created by voice, with the slot asked for rather than guessed. + - A conflicting event is stated as a conflict. + - `mavcaldav` is in compose, or its absence is deliberate and recorded. +- **Scenario**: `calendar_create` *(to write)*. + +### Web crawling + +- **State**: `internal/crawl` with `robots.go` and `watch.go`, `internal/webfetch`. + No living doc. **Finding**: politeness and robots are implemented. The + scheduling policy is not written anywhere. +- **DoD** + - A page is fetched on request and summarised in Russian. + - A watched page reports what changed, on a schedule he set by voice. + - robots and the politeness delay are honoured, observable in the log. + - Only the URL and the utterance leave the box. +- **Scenario**: `crawl_watch` *(to write)*. + +### Summaries + +- **State**: no package. **Finding**: summarisation exists inside the world chain + and inside digestion, and nothing owns it as a capability he can ask for. +- **DoD** + - "перескажи" over a note, a feed item, a page or a mail returns a Russian summary. + - The summary names its source. + - A summary that would invent content is refused, the way `Response.Empty()` + gates a world answer. +- **Scenario**: `summarise` *(to write)*. + +### Webhooks + +- **State**: only `internal/delivery/telegramsink/intake.go`, which is Telegram's + own inbound webhook. **Finding**: there is no general webhook capability in + either direction, and no doc says which direction is wanted. +- **DoD** + - Inbound: an authenticated external event becomes a fact or a nudge candidate. + - Outbound: a nudge can post to a configured URL as a reach. + - Neither direction is reachable without authentication. +- **Scenario**: `webhook_inbound` *(to write)*. + +### Cron jobs + +- **State**: `internal/routine`, `cmd/mavend/tick_routines.go`. **Finding**: + routines carry a `Cron` and are a separate mechanism from reminders. Whether + "cron jobs" means user-defined scheduled acts or the existing routines is + undecided. +- **DoD** + - A scheduled job is created by voice, with its schedule stated back. + - It runs on schedule in `Europe/Samara` and its run is recorded. + - A job that runs an act is bound by the same confirmation rules as any act. + - Its relationship to routines and to recurring reminders is written down, so + three schedulers do not exist. +- **Scenario**: `cron_job` *(to write)*. + +### Learning the style + +- **State**: no package. **Finding**: nothing exists beyond + `internal/phraser/eval/checks.go`, which scores style and does not learn it. + Learning means behavioral, not weights: stored outcomes, no adapter, no + training set. +- **DoD** + - A corrected phrasing is stored as an outcome and changes a later reply. + - No model weights change and no training set is built. + - What was learned is readable on a page and can be deleted. +- **Scenario**: `style_correction` *(to write)*. + +### Learning from mistakes + +- **State**: no package. Same behavioral rule as above. +- **DoD** + - A dismissed nudge, a corrected phrasing and a repaired route are each stored + as an outcome. + - A repeated dismissal suppresses that nudge shape. + - A repaired route changes the next routing of the same utterance. + - Every stored outcome is readable and deletable. +- **Scenario**: `learn_from_dismissal` *(to write)*. + +### Command chaining + +- **State**: no package. The `chain` in `internal/router` is the world chain and + the source chain, not command chaining. **Finding**: nothing exists. +- **DoD** + - "напомни мне и запиши это" performs both, or asks which one. + - A chain containing an act confirms each act separately. + - A failed step stops the chain and names the step that failed. +- **Scenario**: `command_chain` *(to write)*. + +## What this file rules out + +A definition of done that a test suite can score. The suite was green during the +audit and 22 of 39 capabilities were not live. Every criterion above is +observable on the running box. + +A roadmap ordered by code work. None of the four broken capabilities is a code +defect. Weather has no configuration block. Nexus has no data. The voice reach +has no listener. Step-up has no WebAuthn credential. + +## The documentation gaps this file surfaced + +Applying the "state is a reference" rule found seventeen capabilities with no +living doc. Memory is the largest cluster. Facts, notes and the digestion worker +have no owning document at all. Proactive is the second. The reminder lifecycle +spans three packages with no written contract. `docs/roadmap.md` orders the work, +and these gaps are part of it.