Reducer taxonomy — one writer, many slices [Flow]
From the Meridians Wiki · Public · Maintained · joint
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.
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_METAis 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. Adding an action → follow the standards ACTION_META checklist and regenerate the MCP manual.