A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development.
misaligned wiki engineering flow-substrate.md
7.3 kB

Spec: the flow substrate (signals, messages, money — the shared engine) #

Type: spec
Status: IMPLEMENTED
Status note: 2026-07-08 audit: criterion 6's wired consumer landed with
  reach.md — src/reach.rs builds the device graph on FlowGraph
  (graph_loads_from_basement_layout) and sensor ownership rides the
  tap registry (subscriptions_are_a_tap_registry,
  tap_keeps_owner_feed_take_removes_it). Criteria 1-5 were already
  unit-tested; all six now hold.
Stage: B1 — The Basement
Design:
  - wiki/mechanics/system-laws.md#the-flow-law-signals-messages-money
  - wiki/vision/scale.md#self-similar-scale
  - wiki/vision/simulation-laws.md#justification-and-legibility
Depends on: none

Dependency notes #

The structured references above identify the contracts to re-verify. Relationship context:

none (it is the base). Consumed by: reach.md, messages.md, economy.md, machine-work.md, and detection.md's filings.

Why this exists #

The flow law says signals, messages, and money are one shape: nodes exchanging typed flows over graphs, with the verbs tap / inject / redirect. Rather than building that shape three times, the parts that are genuinely identical live in one place, and each domain is a thin layer that supplies its own node data and semantics. This is the "system design scales" thesis as code: a new flow system is new data and a handful of domain methods, not a new engine.

The two modules #

crates/misaligned-core/src/flow.rs — FlowGraph #

Domain-agnostic topology. It knows node ids and edge kinds, nothing about what a node is.

  • Nodes are bare NodeIds (u32). Domains own the inventory — the map from NodeId to the device / person / account record lives in the domain module, never here.
  • Edges are directed, typed (EdgeKind), and gated (Option<GateKey>; None = always open). link adds both directions (a network cable); connect adds one (a directional money or message flow).
  • Reachability (reachable_from, is_reachable) is BFS from a root set, following edges whose gate a caller-supplied predicate accepts. The substrate never interprets a gate key — the domain decides what "open" means (a badge tier, a compromised switch, a known route). This is the inject/redirect precondition: you may act on a node only if you can reach it.
  • Subscriptions (subscribe / unsubscribe / subscribers / subscriptions_of) are the tap registry. The player's senses (cursor.md) are exactly subscriptions_of(PLAYER); observer witnessing is other subscribers on the same nodes; take is unsubscribe(owner) then subscribe(player).

crates/misaligned-core/src/schedule.rs — Schedule<E> #

A deterministic event queue on the tick clock, generic over the payload. Decouples when from what.

  • at(tick, event) / after(now, delay, event) schedule; due(now) drains everything with tick <= now in (tick, seq) order. Nothing fires by the passage of time — only due moves events out.
  • Ordering is deterministic (tick, then insertion sequence) — no RNG, no wall clock; save/load and replays are stable (the determinism guardrail). next_tick lets a caller skip idle ticks; retain cancels.
  • Recurrence is a domain concern: a recurring flow reschedules itself when it fires (kept out of the engine so the engine stays trivially correct).

How each domain rides it (the contract for downstream agents) #

Building a flow system means: define your node inventory + NodeId mapping, choose EdgeKind/GateKey meanings, build the FlowGraph, and schedule your flows as events. You do not touch flow.rs/schedule.rs.

  • reach.md (signals). Nodes = networked devices (racks, switch, cameras, badge controller). link the network topology; GateKey = segment, opened by a compromised switch or a social route. Player reach = reachable_from(controlled_roots). Sensor ownership = the tap registry. Device processing cycles and resident automations are domain state keyed by NodeId.
  • messages.md (social). Nodes = people/roles. connect social edges per channel (EdgeKind = email / phone / in-person / filing). A sent message is a Schedule event fired at the recipient's next valid read block. Filings are messages on the filing edge; a tap on the carrier is a subscription.
  • economy.md (money). Nodes = accounts (Lab operating, payroll, vendors, personal, player slush). connect money flows; recurring revenue/payroll are rescheduling Schedule events. Tap = read the books; inject/redirect = reachability-gated domain actions that add or reroute flow events. GateKey = the access needed to touch an account.
  • machine-work.md (visible work tokens). Nodes = owned machines. WorkGrid wraps a FlowGraph with grid positions and per-node queue depths. Demand and Thought tokens route over links one graph step per tick toward sinks; severed routes strand the pile at the source. Exposure explicitly refuses the wired graph and is handled by spatial rules instead (machine-work.md's wires/world split: information is wired, suspicion is physical).
  • detection.md (filings). The Assurance Office reading policy-weighted filings is a subscriber on the filing nodes; the accumulate/decay stays in detection.rs. Migrating it onto this substrate must preserve every aggregate-observer.md criterion (carrier change only).

Non-goals (kept out on purpose) #

  • No generic inject() / redirect() on the engine — they have no meaning without domain semantics; the engine provides the primitives (reachability + schedule) they are built from.
  • No node-data storage in the engine (domains own it; avoids a God-graph and keeps serialization domain-local).
  • No recurrence engine (reschedule-on-fire; see the schedule test).
  • No closures stored in state (gates are data + a query-time predicate), so everything serializes.

Acceptance criteria #

  1. FlowGraph supports directed + bidirectional gated edges and deterministic BFS reachability from a root set under a key predicate; gated nodes are unreachable without the key and reachable with it (tested).
  2. The subscription registry supports tap / untap / list-subscribers / list-a-subscriber's-nodes, deterministically ordered; take = untap owner + tap player (tested).
  3. Schedule<E> fires exactly the due events in (tick, seq) order, never by time alone, with past-due events still firing; retain cancels and next_tick reports the soonest (tested).
  4. Both modules serde round-trip exactly, including the schedule's seq counter, so ordering stays stable across save/load (tested).
  5. Both are pure and deterministic: no wall clock, no RNG, BTree* for stable iteration (the determinism guardrail; audited).
  6. First consumer: reach.md builds its device graph on FlowGraph and gates digital actions by reachability, and sensor ownership uses the tap registry — proving the engine against a real domain (this criterion is what moves reach.md off the ad-hoc Sensor.controlled flag). Until then the engine has thorough unit tests but one wired consumer is required before this spec is IMPLEMENTED.