MeridiansMeridians

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 GitHub

Raw Markdown source