docs: name the ecosystem, correct the latency, drop the stale handoff

Three documentation changes and one deletion.

CLAUDE.md and AGENTS.md gain the Nexus/Praxis/Hexis sections that were written
last session and never committed: what each service owns, where Maven's client
for it lives, and the rules that are not negotiable.

The p50 latency figure was wrong in two files. CLAUDE.md said the cascade costs
2.7s and that the LLM router is 90x slower than the classifier. Both come from
the bakeoff table, where the number is contention on a shared llama-server, not
the model. ROUTING-EVAL-31-07-2026.md line 61 says so and measures the router at
p50 825ms / p95 1.2s / max 3.0s. Corrected in CLAUDE.md, and the bakeoff table
now carries a header pointing at the routing eval for absolute latency. Latency
work was about to be planned off a number that was never real.

HANDOFF.md is deleted. It described work sitting on fix/integrated waiting for a
fast-forward onto overnight/eco-versioned-traces. Neither is true: master
contains that tip plus 22 commits, and both branch pointers are stale. The three
live defects it recorded move to PLAN-DETERMINISM-02-08-2026.md, which is now
the only planning document.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaeSbLMEVG5ey8tejU3y2
This commit is contained in:
kami
2026-08-02 02:16:37 +04:00
parent b8227295b8
commit a654b0126f
5 changed files with 363 additions and 124 deletions
+40
View File
@@ -5,6 +5,46 @@ This repo maps to **Maven** (project ID: 2) in Vikunja.
Feature work, bugs, deployment tasks all go here.
MCP endpoint: `http://localhost:9100/mcp` (or `http://192.168.1.104:9100/mcp` from workpc)
## The sibling services (Nexus, Praxis, Hexis)
Maven is the conversational front end of a four-service ecosystem. The other three
live in sibling repos next to this one.
| Service | Repo | Port | Answers |
|---|---|---|---|
| Nexus | `../nexus` | 9740 | who or what is this name |
| Praxis | `../praxis` | 8989 | what needs attention |
| Hexis | `../hexis` | 9741 | what can be run, and running it |
Division of labour: Nexus identifies, Praxis observes, Hexis acts, Maven understands
and coordinates. Maven is not the source of truth for any of the three. The full
contract is `MAVEN_ECOSYSTEM_ARCHITECTURE.md`, and the constraints that bite during
implementation are summarised in `CLAUDE.md`.
Where things are in this repo:
- `cmd/mavend/ecosystem.go` holds `nexusClient` and `praxisClient`. The Hexis client
is vendored from `github.com/kami/hexis/pkg/client`.
- `cmd/mavend/ecosystem_acts.go` routes an act through capability discovery.
- `cmd/mavend/factenrichment.go` resolves each stored fact's `Subject` against Nexus
on a background poll loop, with backoff and no give-up.
- `internal/store/entityfacts.go` holds the entity-tagged fact rows.
- Config blocks are `nexus`, `praxis` and `hexis` in `deploy/mavend.json`. Each is
optional. Absent means that integration is dark, not broken.
Bring the whole ecosystem up locally:
```sh
docker compose -f deploy/ecosystem/docker-compose.yml up -d
```
That builds all three from the sibling working trees, so commit or stash there first.
Each publishes on loopback at the port above. Maven reaches them by service name on
the shared compose network.
Testing without them running: `cmd/mavend/fakeecosystem_test.go` provides stubs, and
`cmd/mavend/ecosystem_degraded_test.go` covers each service being unreachable.
## Rendering / previewing the web UI locally
To see mavweb pages with real data without touching the production stack:
+44 -2
View File
@@ -68,6 +68,45 @@ Daemons are wired socket-to-socket, not linked. `internal/ipc` is the client/ser
protocol; the config in `deploy/mavend.json` (with `${VAR}` env expansion from gitignored
`deploy/telegram.env`) sets socket paths, model paths, and the phraser/embedder blocks.
## The ecosystem: Nexus, Praxis, Hexis
Maven is one of four services. It owns conversation and personal memory. It does not
own identity, operational state, or execution. Full contract in
`MAVEN_ECOSYSTEM_ARCHITECTURE.md`.
```text
Nexus identifies. Praxis observes. Hexis acts. Maven understands and coordinates.
```
| Service | Owns | Maven's client | Configured at |
|---|---|---|---|
| **Nexus** | Canonical entity ids, names, aliases, relationships. Projects, services, devices, people, pets, places. | `nexusClient` in `cmd/mavend/ecosystem.go`, `POST /api/v1/resolve` | `nexus.url` (`http://nexus:9740`) |
| **Praxis** | Operational attention and item lifecycle. What needs looking at, what changed, what is still unresolved. | `praxisClient`, the HTTP tools API under `/api/v1/tools/` | `praxis.url` (`http://praxis:8989`) |
| **Hexis** | The capability registry and the only path to executing anything. | vendored `github.com/kami/hexis/pkg/client` | `hexis.url` (`http://hexis:9741`) |
All three are `nil` unless configured, and every one of them degrades on its own.
An outage means a named gap in the answer, never a broken turn and never a guess.
Rules that are not negotiable:
- **No component reads another component's database.** Praxis attention comes over
HTTP, never from its SQLite file.
- **Identity lives in Nexus.** Do not invent a local fact key for something Nexus
resolves. `actionFact` already sets `Subject`, and `cmd/mavend/factenrichment.go`
resolves it in the background against Nexus.
- **Free text never reaches a mutating Hexis call.** Resolve to a canonical entity id
first. Ambiguous resolution asks the owner, it does not pick.
- **LLM output is not authorization.** Confirmation binds capability id, target
entity, arguments, requester and expiry. See `cmd/mavend/confirm.go`.
- **Praxis lifecycle words mean different things.** Surfaced is not acknowledged,
acknowledged is not resolved, execution success is not recovery. Reading an item
aloud calls `Surface`, never `Acknowledge`.
- **No automatic attention-to-action path.** Digestion may summarise Praxis. It may
not call Hexis.
Every cross-service call carries a correlation id minted once per action
(`withCorrelationID`), a contract version header, and `X-Requested-By: maven`.
## Routing — read this before touching the router
`internal/router/` has TWO layered engines. **The LLM router is now the default and it is
@@ -90,8 +129,11 @@ fallback. Any LLM error falls through to the classifier so a turn never breaks o
Measured on the 77-case RU fixture (`MODEL-BAKEOFF-31-07-2026.md`): the classifier scores
36.8% full accuracy at p50 31ms; Qwen3-1.7B scores 67.5% intent-only / 72.7% through the
cascade at p50 ≈2.7s. Accuracy roughly doubled, latency is ~90× worse, and that trade was
accepted deliberately. `Confidence: 1.0` used to be hardcoded in `llmrouter.go`, so the LLM
cascade at p50 ≈825ms. Accuracy roughly doubled, latency is ~27× worse, and that trade was
accepted deliberately. **The ≈2.7s figure that stood here until 2026-08-02 was contention,
not the model.** See `ROUTING-EVAL-31-07-2026.md` line 61, which measures the LLM router at
p50 825ms / p95 1.2s / max 3.0s and the full cascade at p50 0.80-1.04s. Do not plan latency
work off the bakeoff table. `Confidence: 1.0` used to be hardcoded in `llmrouter.go`, so the LLM
path could never ask for clarification (6/6 refusal cases missed on the fixture) — Vikunja
#359. Fixed 31-07-2026 with structural signal (single-token utterance, keyless fact, act with
no allowlisted fn) feeding the same stage-3 gate the classifier path already had — see
-120
View File
@@ -1,120 +0,0 @@
# Handoff
Uncommitted scratch file. Delete it once the work below is finished.
Written 2026-08-01. Kami is away for about an hour and will start a new session.
## Where things stand
The 35-PR stack (50 to 84 on gitea, one linear chain) has been reviewed and the
findings have been fixed. Two things are done and one is not.
**Done: reviews.** One review posted on every PR from 50 to 84, as the `claude`
login. Review ids 58 to 92.
**Done: fixes.** Eleven agents fixed the findings in parallel git worktrees.
All eleven branches are merged onto **`fix/integrated`**, which is 68 commits
ahead of the tip. `make test` and `make build` both pass on it.
**Not done: landing them.** `overnight/eco-versioned-traces` (the stack tip) has
NOT been moved. Kami chose to land the fixes as commits on that tip. The final
step is a fast-forward, once he has answered the open questions below.
git checkout overnight/eco-versioned-traces
git merge --ff-only fix/integrated
Do not push. He merges the stack himself.
## Ask him these first
He asked for the list and then left, so none of it is answered. Nothing below
is a blocker for the fast-forward, but all of it is easier to change before the
tip moves than after.
1. **Hexis refuses to wire when a token is configured.** It used to log once and
then call unauthenticated forever. Fail closed, or warn and continue?
2. **`MethodIngestMail` stayed at `AuthRead`** while `SetTaskStatus` moved to
`AuthWrite`. Those two now disagree about the same kind of question.
3. **"Прости" in `clarifyGaveUp` and one expiry variant.** `CheckCringe` bans
apologies. The fix scoped the ban to nudges and kept his wording, with the
reasoning in a test skip. Absolute ban is the other reading.
4. **Memory evaluation timeout is 60s.** Two agents disagreed here. The merge
kept the short budget plus the gate that yields the slot to voice turns. Five
minutes is safe again now that the gate exists.
5. **Cold-start v1 recovery is narrow.** It fires only when an assertion carries
a PRF secret and that unlock fails. A pre-existing box whose only
authenticator lacks PRF still needs `MAVEN_DB_KEY`.
## Two things to know before deploy
**The update block.** The self-update fix refuses at startup any
build-from-source deployment that cannot say how to undo the source.
`deploy/mavend.json` has no `update` block today, so nothing breaks. Adding one
for the docker layout without `source_rollback` will stop the daemon booting.
That is intended. It should not be a surprise.
**Three migrations became four numbers.** Three agents each wrote a migration
15. Tasks kept 15, ecosystem traces became 16, MCP tool fingerprints became 17.
Any box already carrying an unreleased 15 from a worktree build needs its
schema version checked by hand.
## Merge decisions already made
Two agents independently added `llm.Gate`. One is priority between a voice turn
and background work. The other is admission control while the resident model is
swapped. Both were kept. The swap one is now `SwapGate` with `SetSwapGate`.
`Complete` takes priority first and the drain second, so a background request
waiting on priority cannot stall a swap.
## Still open in the code, on purpose
- Ambient calendar events never reach the `calendar_busy` gate. `calendar_busy`
is a level, so an ambient writer needs an expiry. Recorded as a comment at the
top of `cmd/mavweb/ambient.go`.
- Hands-free has no session-scoped voice assertion. Noted in the mavweb route
table.
- The Hexis capabilities call sends no correlation id. The vendored client has
no header hook, so closing it means re-vendoring.
- Re-running a stored audio blob by id does not exist. The comments that claimed
the wire offered it were corrected.
- Simulator persona checks read a Go constant on one step, because chat there
goes through `phraser.NewStub()` rather than the replier.
- The PRF value is client-supplied and unbound to the assertion signature. This
is inherent to PRF key wrapping, which needs a fixed salt. Documented in
`cmd/mavweb/webauthn.go`. Do not try to bind it.
- `safeKey` drops Cyrillic, so Russian calendar events on one day collide. Filed
as Vikunja #443 with three fix options. It is a migration, not a patch.
## The one task left
Audit what the agents filed in Vikunja. They created QA tasks on their own
initiative, which nobody asked them to do. Kami saw them and said some are new
bugs worth a later sweep. Dedupe against existing tasks, and separate genuine
new bugs from restatements of findings the commits already fixed. That produces
the second sweep list without re-reading 35 PRs.
## Recipes
Gitea is `https://gitea.kvmx.ru`, repo `kami/Maven`. Tokens are in
`~/.config/tea/config.yml`: the `homesrv` login reads, the `claude` login posts.
`tea` cannot list review comments, so use the REST API.
Read the reviews for one PR:
curl -s -H "Authorization: token <homesrv-token>" \
"https://gitea.kvmx.ru/api/v1/repos/kami/Maven/pulls/<n>/reviews"
Do not fetch `/issues/comments` without a `since` filter. It returns 50 stale
comments from a superseded stack.
The scratchpad from this session holds `RECIPE.md` (how the reviews were done),
`FIXBRIEF.md` (how the fixes were done) and `postreview.sh`. Worktrees are under
`scratchpad/wt/g01` to `g11` and can be removed with `git worktree remove` once
the tip has moved.
## Constraints that do not change
Read `CLAUDE.md`. Maven is feminine and calls him "ты". The eval enforces both.
Not a nag, not autonomous. No telemetry, no cloud model, no third-party account.
His notes and facts are never search input. Use `make`, never bare `go build` on
a CGO daemon.
+9 -2
View File
@@ -162,6 +162,11 @@ The 1.7B does that 0-2 times.
## Latency — the long tail is not the Thinking block
> **Stale, corrected 2026-08-02.** The p50 figures in this table are contention on a
> shared llama-server, not the model's cost. The router measures p50 825ms / p95 1.2s /
> max 3.0s in `ROUTING-EVAL-31-07-2026.md`, which says so at line 61. Read this table for
> the shape of the tail only. Take absolute latency from the routing eval.
| | p50 | p95 |
|---|---|---|
| Qwen3.5-0.8B | 2.4s, 2.9s, 2.0s | 17.4s, 17.6s, 17.4s |
@@ -213,8 +218,10 @@ swapped again when the CPT lands.
- ~~The routing numbers only reach production once the LLM router is wired on. It is
still `nil`.~~ **Resolved the same evening:** the LLM router is wired at `voice.go:214`
behind `voice.llm_router`, the default is on, and `deploy/mavend.json` sets it `true`.
These numbers are the production path now, so the p50 ≈2.7s is a real per-turn cost and
not a bench artifact.
These numbers are the production path now. **Corrected 2026-08-02: the p50 ≈2.7s in the
latency table above WAS a bench artifact.** It is contention on the shared llama-server,
not the model. `ROUTING-EVAL-31-07-2026.md` line 61 says so, and measures the router at
p50 825ms / p95 1.2s / max 3.0s. Cite that file for latency, not this one.
- ~~`/mnt/hdd1/llms/LFM2.5/Qwen3-1.7B-UD-Q4_K_XL.gguf` is a 293 MB truncated download
in the wrong directory.~~ **Deleted 2026-07-31.** The good 1.13 GB copy in `qwen3/` is
what `deploy/mavend.json` loads.
+270
View File
@@ -0,0 +1,270 @@
# Deterministic logic around a small model
Written 2026-08-02. Branch `fix/integrated`.
## The question
Where does deterministic code attach, so that it helps the resident 1.7B now and
does not fight a larger model later.
## The mistake to avoid
Everything deterministic we have added so far sits in front of the model and
preempts it. Stage 0 matches, the model never sees the turn. That shape helps a
weak model and blocks a strong one, silently.
The fix is not to remove it. The fix is to know which rules are safe in that
position and to have a way to measure the rest.
## Four attachment points
**Bypass, before the model.** The only shape that saves the 2.7s p50. Safe when
the rule is a decision procedure over a closed set, not a guess over an open one.
Exact match and clock queries qualify.
**Evidence, beside the model.** Extractors emit candidate slots as a prior, not a
verdict. The prompt carries the prior and the validator reuses it. A small model
leans on it, a large one overrides it correctly.
**Grammar, around the model.** GBNF built from live state rather than hardcoded.
Costs nothing at runtime and prevents the error instead of catching it.
**Repair, after the model.** Validation failure re-asks with the specific error
rather than overriding. Self-retiring, because a better model trips it less.
## The constraint that ranks them
Latency must stay minimal. That demotes repair and promotes grammar.
- Grammar first. Zero runtime cost, immediate gain.
- Bypass keeps its place. It is the only thing that avoids a model call at all.
- Repair only where failure is rare, capped at one retry.
- Evidence is correct but costs a model call where a bypass costs none.
- Ecosystem calls belong in the snapshot path, in parallel, on strict deadlines.
Never serial before routing.
## The line that never moves
Separate policy from capability compensation. They look alike and age oppositely.
Capability compensation exists because the model is weak. It should be measurable
and retirable.
Policy exists because we decided. The personal boundary, the feminine persona, the
never-search-his-notes rule, the Hexis allowlist and confirmation binding. None of
those yield to a smarter model. A larger model is more dangerous there, not less.
## What we keep
Every stage-0 rule stays exactly as it is. Retiring them was the wrong call and it
would throw away a day of measured gains.
- exact-match fast path
- clock rules
- `SystemTimeDateGrammars`
- `AgendaQueryGrammars`
- `thinSingleToken`, including the social lexicon and the verb-ending test
Three additions, none of which change behaviour:
1. Each rule gets an id and a fixture subset.
2. Each rule writes one trace line when it fires.
3. Each rule carries a comment saying whether its set is closed or open.
That preserves today's accuracy and buys the option to revisit later with numbers.
## What we build
**Dynamic grammars from live state.** The grammars today are static: `routeGrammar`
fixes the 7 intents, `responseGrammar` fixes the mood enum, kiwix `queryGrammar`
fixes word shape. Everything else is a free string.
Candidates in order of payoff:
- **Hexis capability ids.** After discovery the exact list is known. As an enum,
the model cannot name a capability that does not exist.
- **Act fn allowlist.** Hits the two remaining false clarifies directly. They are
the act-with-no-allowlisted-fn arm of `gateLLMDecision`, firing on invented verbs.
- **Calendar names.** Enumerate the real ones for agenda and query slots.
- **Known fact keys.** A read-back matches a stored key instead of inventing a
synonym. This is the general form of the read side we scrapped on 2026-08-01.
- **Nexus display names as act targets.** Only while the list stays small.
Two rules or it backfires:
- **Always include an escape value.** A closed enum with no `other` forces a wrong
pick instead of a decline. The escape is what feeds the clarify gate.
- **Cache the grammar string, keyed on the state that built it.** Rebuilding per
turn is fine. Recompiling a large grammar per turn is not.
## Retrieval over regex
Resolve against stores that already exist rather than adding patterns.
Identity is the worked example. Nexus is authoritative, `actionFact` already sets
`Subject`, and `cmd/mavend/factenrichment.go` resolves it in the background. The
scrapped work invented a parallel key namespace with nothing reconciling the two.
A table that grows with real data beats patterns that grow with our patience.
Note a real gap: the personal boundary in `cmd/mavend/actions_query.go` guards
Maven's own store only. It does not know Nexus or Praxis exist.
## Prerequisites
Both are offline and need no deploy. Neither existed on 2026-08-01, and that is
why the day cost what it did.
**Failure taxonomy over the 77-case fixture.** Classify every miss as model
ignorance, contract loss, prompt ambiguity, or our own bug. Only model ignorance
deserves deterministic compensation. The other three get fixed once, for every
model size. Inference, not verified: much of what we patched was the last two.
**Per-assist ablation runner.** One command toggles each assist off and reports the
accuracy delta on its fixture subset. Then retiring an assist is a config flip and
a number, not an argument.
## Held
Not now, and nothing gets built for it.
- A 12B or 35B model. It may be cloud or the 16GB workstation, and cloud crosses
the current no-third-party line.
- The escalation tier in `gateLLMDecision`.
- Converting the stage-0 heuristics to evidence.
One thing carries forward for free: deterministic assists emit confidence, never a
verdict. A verdict cannot escalate.
## Ruling on the idea list
Nineteen ideas, judged on whether they earn a place in Maven. Checked against the
tree on 2026-08-02, not from memory.
### Build. Absent, and worth it.
**SQLite FTS over embeddings.** No `fts5` anywhere in the tree. Lexical search is
faster than the ONNX embedder, deterministic, and strongest exactly where the
embedder is weakest, which is exact Russian names. Semantic search becomes the
fallback rather than the gate. This is the highest-value absent item.
**Cached TTS phrases.** No cache in `internal/tts`. Confirmations, clarifies and
refusals repeat constantly and their text is already fixed. Pre-rendering them is
cheap and pays straight into the minimal-latency constraint.
**Synthetic router dataset.** Generate Russian tool-calling examples from the same
schemas that will build the dynamic grammars. One source of truth for both, so the
model is trained on exactly the shapes it will be constrained to at inference.
**Command STT separate from dictation STT.** One path today. Commands want latency
and a small vocabulary, meeting capture wants accuracy and can take its time.
`internal/capture` already spools to disk, so the split follows the existing seam.
Medium priority, behind the four above.
### Polish. Present, incomplete.
**Strict JSON everywhere.** Six call sites already carry a grammar: the router,
both phraser paths, kiwix, `internal/memeval`, `internal/email/extract`. Two do
not: `cmd/mavend/capture.go` and `cmd/mavend/replier_llm.go`. Close those two and
the rule holds everywhere.
**Evidence-first prompting.** The world snapshot, the embedder RAG hint and matched
notes all exist. What is missing is the discipline that the model verbalises
retrieved evidence rather than recalling. This is the direct fix for the Левитан
fabrication. High value.
**Progressive inference.** What exists is a fallback cascade, not escalation. On
error it drops to something weaker. It never escalates on ambiguity. The upgrade is
held with the bigger-model question, and `gateLLMDecision` is the hook.
**Entity dictionaries.** Nexus is the canonical store, `behavior_ru.go` has
`KeyAliases`, `ecosystem_acts.go` has verb aliases. Morphology is the gap, and the
code says so in three places. Russian needs it and there is no stemmer in the repo.
**Assistant state machine.** `confirm.go` models pending confirmation and
`dialogue.Session` models the turn. There is no unified task state. Reuse the
Praxis vocabulary rather than inventing one, because surfaced, acknowledged and
resolved already mean something precise here.
**Session memory compiler.** The digestion tick, `internal/memory` and
`followUpMerge` do parts of this. Not a coherent compile step.
**Background memory maintenance.** Mostly present, and the absent part is small.
What exists: `internal/memeval` reads recent memory on a loop and writes
observations, deduped against its own prior output, unable to speak or act. Facts
supersede at write time through `voidsID`, `CorrectValue` and `VoidLatestFact`,
and `RecentActiveFactsByKind` reads only live rows. Digests, ecosystem traces and
media all prune.
Three gaps remain, all in the fact store:
- Superseding is turn-driven. Nothing reconciles two live facts that contradict
unless a turn corrects one of them.
- Duplicates written by different sources or phrasings stay as separate live rows.
There is no key-level merge pass.
- Nothing expires. The log is append-only and grows without bound, and no fact
ever ages out on its own.
Worth doing, and smaller than it looked. Not urgent.
**Qwen CPT then narrow SFT.** In flight as #122. One disagreement with the idea as
written: do not drop persona from the SFT. Persona is the stated reason the CPT
exists, because stock writes `рад` where Maven needs `рада`. Keep the joint
router-plus-persona tune.
**Offline job queue.** The tick loop, `factEnrichmentWorker` with backoff, and the
media prune already defer work. A general queue is tidier, not more capable. Low
priority.
**Response templates.** Present in `clarify.go`, in the `replySystem` arms and in
`StubPhraser`. Worth extending to high-frequency confirmations, where latency and
persona correctness both matter. Do not extend further. Templating the
conversational reply removes the reason she is worth having.
### Reject.
**Grammar-first routing as a replacement for the LLM router.** Already measured.
The classifier scores 36.8% full accuracy against 72.7% through the cascade.
Replacing the model with rules halves the accuracy. Grammar-first as an ordering is
what stage 0 already is, and that stays.
**Hierarchical intent classification.** Seven intents is already the coarse layer.
The specialisation stage exists as per-intent slot filling in `Extractor.Extract`.
Adding a tier buys structure, not accuracy.
**Tool-specific micro-models.** Contradicts the one-resident-model constraint,
needs per-domain training data nobody has, and multiplies model loads on a single
Vega iGPU. Dynamic grammars give the same domain narrowing at zero runtime cost.
**Local knowledge graph.** Nexus owns entities and relationships. Building a second
graph in Maven breaks the ecosystem line and creates two answers to one question.
If graph traversal is wanted, it is a Nexus feature request.
**Preemptible training.** Training runs in a separate workspace, not on the serving
box. This only becomes real if CPT moves onto homesrv, and that is not the plan.
## Known open, carried over from the deleted handoff
Found live on 2026-08-01, not fixed. Everything else in that file was stale.
- **Kiwix ranks badly on a correct query.** The stop-word pass eats the "and". So
"кто написал войну и мир" reaches Kiwix as "war peace author". The top hit is
"List of peace activists" and she summarises that as the answer. The tag
`scrapped/fact-and-kiwix-phrases` fixes the query text. The ranking is the ZIM
search and is untouched either way.
- **Chat drags prior turns into an answer.** One live reply mixed the greeting, the
height statement and a world question. It named Левитан as the author of Война и
мир.
- **`safeKey` drops Cyrillic**, so Russian calendar events on one day collide.
Vikunja #443 with three fix options. It is a migration, not a patch.
## Sequence
1. Failure taxonomy over the fixture.
2. Ablation runner, plus ids and trace lines for the existing rules.
3. Dynamic grammar for the act fn allowlist.
4. Dynamic grammar for Hexis capability ids.
5. Dynamic grammar for calendar names and known fact keys.
6. Extend the personal boundary to the ecosystem stores.
Steps 1 and 2 come before anything is written in the router.