Reducer taxonomy — one writer, many slices [Flow]
Source path: knowledge-base/diagrams/flows/reducer-taxonomy.md
# Reducer taxonomy — one writer, many slices `[Flow]`
State changes *only* through reducer actions — **247** of them, composed from **22 slices**, every one classified in a single exhaustive policy table. This is the single-writer discipline made concrete: how a mutation is gated, reduced, and audited without any alternate write path.
```mermaid
flowchart TB
Caller(["caller — UI · op · loop · MCP tool<br/>(a caller, never an alternate writer)"])
Caller -->|"dispatch(action)"| Pipe
subgraph Pipe["applyLocalMutation — the terminus"]
direction LR
Gate["**gate**<br/>role/access check<br/>(ACTION_META.category)"]
Reduce["**reduce**<br/>compose 22 slices<br/>(first non-undefined wins)"]
Persist["**persist**<br/>fs-store: version bump<br/>+ atomic write"]
Attr["**attribute**<br/>activity-log entry<br/>(actor · area · id)"]
Echo["**echo**<br/>SSE /stream"]
Gate --> Reduce --> Persist --> Attr --> Echo
end
subgraph Slices["domainReducer — 22 disjoint slices"]
direction LR
s1["world · entities · scenes"] ~~~ s2["streams · tutor · projections · readings"]
s3["research · sources · program"] ~~~ s4["branches · versions · phases"]
s5["members · settings · boards"] ~~~ s6["chat · capture · media · learning · game · exchange"]
end
Reduce -.->|"(state, action) ⇒ state?"| Slices
Echo --> Clients(["all clients adopt via use-record-stream"])
```
**Invariants**
- **Slices are order-independent.** Each slice is `(state, action) ⇒ AppState | undefined`; the composer runs them and takes the **first non-undefined** result. No cascading, no slice depends on another running first. A new mutation = a new case in the owning slice.
- **`ACTION_META` is exhaustive and type-checked.** Every one of the 247 action types is classified by **category** (view · sync · game · learn · contribute · people · membership · analysis · edit · delete) and an orthogonal **area** (world · opinion · research · chat · extraction · learning · program · game · constellation · governance · access). Two channels are decoupled by design: **LOG** (durable audit for Directors) vs **TOAST** (ephemeral feedback).
- **The reducer is pure and clock-pinned.** Inject `now`; never read the clock inside a slice. This is what makes headless commits (MCP, Program) byte-identical to UI commits and lets them all reach the UI through the same SSE echo.
- **Slices are node-safe.** They import only `shared.ts` + pure libs — never React, never IndexedDB. This is why the same reducer runs in the browser and in the headless daemon.
**Action families** (grouped for orientation; the record is `ACTION_META`): Constellations · Onboarding · Domain collections · Program (Research/Opinion/Merge/Position) · Streams & Merges · Positions & Readings · Research · Scenes & Entities · Threads · Branches & Versions · Learning · Game · Members & Access.
**Where it lives:** `src/lib/core/reducer/actions.ts` (the 247-type union), `src/lib/core/reducer/domain-reducer/{index,slices/}` (composition + 22 slices), `src/lib/core/access/action-policy.ts` (`ACTION_META`), `src/lib/server/record/mutation.ts` (`applyLocalMutation`). Cache echo: [cache-coherence](cache-coherence.md). Adding an action → follow the `standards` ACTION_META checklist and regenerate the MCP manual.
Open on GitHubRaw Markdown source