MeridiansMeridians

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_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. Adding an action → follow the standards ACTION_META checklist and regenerate the MCP manual.