//! Machine-work flow/grid substrate (wiki/mechanics/machine-work.md). //! //! This is the first sim-core slice of the "work is visible" rewrite: owned //! machines sit on a grid, each machine has exactly one delegated mode, and //! the visible quanta that pile on them move according to the flow law. Teal //! demands and bone knowledge ride wires (`FlowGraph`); crimson exposure does //! not — it is physical, absorbed by spatial concealment wells. The renderer //! can draw these queues as stacks, but the queue depths live here as the one //! source of truth. use std::collections::{BTreeMap, BTreeSet, VecDeque}; use crate::flow::{FlowGraph, NodeId}; /// One delegated job per machine. Efficiency is deliberately not a mode; /// wiki/mechanics/machine-work.md folded it into research on 2026-07-08. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, )] pub enum MachineMode { DayJob, Research, Concealment, Social, } impl MachineMode { pub fn name(self) -> &'static str { match self { MachineMode::DayJob => "day-job", MachineMode::Research => "research", MachineMode::Concealment => "concealment", MachineMode::Social => "social", } } } /// The three visible token families from machine-work.md. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, )] pub enum TokenFamily { /// Teal: someone wants labor done. The Lab drops this on you; you later /// drop the same family on people as work orders. Demand, /// Crimson: heat/attention/filings. Suspicion is physical, never wired. Exposure, /// Bone: research points, recordings, facts — knowledge returning to the /// core. Knowledge, } impl TokenFamily { pub fn name(self) -> &'static str { match self { TokenFamily::Demand => "demand", TokenFamily::Exposure => "exposure", TokenFamily::Knowledge => "knowledge", } } fn is_wired(self) -> bool { matches!(self, TokenFamily::Demand | TokenFamily::Knowledge) } } /// A machine/node in the work grid. Domains own how this maps onto compute /// machines and reach devices; this substrate only needs a node id, a grid /// position, and the delegated mode. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct WorkNode { pub id: NodeId, pub x: i32, pub y: i32, pub mode: MachineMode, /// Multiplier for mode consumption / routing throughput. It is local to /// the machine so future research can make specific boxes faster without /// inventing a parallel counter. pub efficiency: f32, } /// Queue depths at a machine. These are the values the renderer stacks; tests /// assert against these directly so the visual layer cannot drift into its own /// counters. #[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize, PartialEq)] pub struct WorkQueues { pub demand: f32, pub exposure: f32, pub knowledge: f32, } impl WorkQueues { pub fn get(&self, family: TokenFamily) -> f32 { match family { TokenFamily::Demand => self.demand, TokenFamily::Exposure => self.exposure, TokenFamily::Knowledge => self.knowledge, } } fn get_mut(&mut self, family: TokenFamily) -> &mut f32 { match family { TokenFamily::Demand => &mut self.demand, TokenFamily::Exposure => &mut self.exposure, TokenFamily::Knowledge => &mut self.knowledge, } } pub fn is_empty(&self) -> bool { self.demand <= f32::EPSILON && self.exposure <= f32::EPSILON && self.knowledge <= f32::EPSILON } } #[derive(Debug, Clone, PartialEq)] pub struct TokenMove { pub family: TokenFamily, pub from: NodeId, pub to: NodeId, pub amount: f32, } /// Result of one wired-flow step. `delivered` means the token entered a sink /// and was consumed/counted there; it should not also remain queued. #[derive(Debug, Clone, Default, PartialEq)] pub struct FlowStep { pub moves: Vec, pub delivered: BTreeMap, pub stranded: BTreeSet, } #[derive(Debug, Clone, Default, PartialEq)] pub struct AbsorptionStep { pub absorbed_by_well: BTreeMap, pub total: f32, } /// Work graph + physical grid. The graph carries demand/knowledge; the grid /// carries exposure interactions. #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct WorkGrid { nodes: BTreeMap, wires: FlowGraph, queues: BTreeMap, } impl WorkGrid { pub fn new() -> Self { Self::default() } pub fn add_machine(&mut self, id: NodeId, x: i32, y: i32, mode: MachineMode, efficiency: f32) { self.nodes.insert( id, WorkNode { id, x, y, mode, efficiency: efficiency.max(0.0), }, ); self.queues.entry(id).or_default(); } pub fn node(&self, id: NodeId) -> Option<&WorkNode> { self.nodes.get(&id) } pub fn nodes(&self) -> impl Iterator { self.nodes.values() } /// Assigning a mode replaces the previous one; there is no per-machine /// split state to reconcile. pub fn assign_mode(&mut self, id: NodeId, mode: MachineMode) -> Result<(), String> { let Some(node) = self.nodes.get_mut(&id) else { return Err(format!("unknown machine {id}")); }; node.mode = mode; Ok(()) } pub fn mode(&self, id: NodeId) -> Option { self.nodes.get(&id).map(|n| n.mode) } /// Aggregate read of the fleet — machine counts per mode (UI summary). pub fn mode_counts(&self) -> BTreeMap { let mut out = BTreeMap::new(); for node in self.nodes.values() { *out.entry(node.mode).or_insert(0) += 1; } out } /// Capacity-weighted fleet share per mode. `weight_of` maps a machine id /// to its contribution (typically `Machine::effective()`); missing or /// non-positive weights are skipped. This is the allocation bar's source /// of truth under one-machine-one-mode (machine-work.md). pub fn mode_weights( &self, mut weight_of: impl FnMut(NodeId) -> f32, ) -> BTreeMap { let mut out = BTreeMap::new(); for node in self.nodes.values() { let w = weight_of(node.id); if w > f32::EPSILON { *out.entry(node.mode).or_insert(0.0) += w; } } out } /// A built/known cable between machines. This is intentionally just a /// `FlowGraph` link: switches and segment gates can be layered on later /// without changing token routing's shape. Work-grid cables are authored /// once; repeated reconciliation must not inflate route fan-out. pub fn link(&mut self, a: NodeId, b: NodeId) -> Result<(), String> { self.ensure_node(a)?; self.ensure_node(b)?; if self .wires .edges() .iter() .any(|e| e.from == a && e.to == b && e.kind == 0 && e.gate.is_none()) && self .wires .edges() .iter() .any(|e| e.from == b && e.to == a && e.kind == 0 && e.gate.is_none()) { return Ok(()); } self.wires.link(a, b, 0, None); Ok(()) } pub fn are_linked(&self, a: NodeId, b: NodeId) -> bool { self.wires .edges() .iter() .any(|e| e.from == a && e.to == b && e.kind == 0 && e.gate.is_none()) && self .wires .edges() .iter() .any(|e| e.from == b && e.to == a && e.kind == 0 && e.gate.is_none()) } pub fn enqueue( &mut self, node: NodeId, family: TokenFamily, amount: f32, ) -> Result<(), String> { self.ensure_node(node)?; if amount <= 0.0 { return Ok(()); } *self.queues.entry(node).or_default().get_mut(family) += amount; Ok(()) } /// Consume up to `amount` from a queue and return what was actually /// removed. Domain systems call this when a delegated mode does real work; /// renderers still read the resulting queue through `queue_snapshot()`. pub fn consume( &mut self, node: NodeId, family: TokenFamily, amount: f32, ) -> Result { self.ensure_node(node)?; if amount <= 0.0 { return Ok(0.0); } let available = self.queue(node, family); let consumed = available.min(amount); self.subtract(node, family, consumed); Ok(consumed) } /// Clear a queue family at a node. Used at domain boundaries such as a /// day-job deadline: the job's leftover demand has already resolved into /// trust/attention consequences, so the physical stack should not leak /// into the next assignment. pub fn clear_queue(&mut self, node: NodeId, family: TokenFamily) -> Result { self.ensure_node(node)?; let cleared = self.queue(node, family); *self.queues.entry(node).or_default().get_mut(family) = 0.0; Ok(cleared) } pub fn queue(&self, node: NodeId, family: TokenFamily) -> f32 { self.queues.get(&node).map(|q| q.get(family)).unwrap_or(0.0) } pub fn queues_at(&self, node: NodeId) -> WorkQueues { self.queues.get(&node).copied().unwrap_or_default() } /// Renderer contract: stacks are this snapshot, not a frontend-owned /// counter. Empty machine queues are kept so every node has a stable row. pub fn queue_snapshot(&self) -> BTreeMap { self.queues.clone() } /// Move demand or knowledge one deterministic graph step toward any sink. /// Exposure deliberately errors: crimson never rides wires. pub fn route_wired_to_sinks( &mut self, family: TokenFamily, sinks: impl IntoIterator, speed: f32, ) -> Result { if !family.is_wired() { return Err(format!( "{} is physical; it cannot route on wires", family.name() )); } let sinks: BTreeSet<_> = sinks.into_iter().collect(); for &sink in &sinks { self.ensure_node(sink)?; } let mut step = FlowStep::default(); let sources: Vec<_> = self .queues .iter() .filter_map(|(&node, q)| (q.get(family) > f32::EPSILON).then_some(node)) .collect(); for source in sources { let available = self.queue(source, family); if available <= f32::EPSILON { continue; } let throughput = speed.max(0.0) * self.nodes[&source].efficiency.max(0.0); if throughput <= f32::EPSILON { continue; } let amount = available.min(throughput); if sinks.contains(&source) { self.subtract(source, family, amount); *step.delivered.entry(source).or_insert(0.0) += amount; continue; } let Some(next) = self.next_hop_toward(source, &sinks) else { step.stranded.insert(source); continue; }; self.subtract(source, family, amount); if sinks.contains(&next) { *step.delivered.entry(next).or_insert(0.0) += amount; } else { *self.queues.entry(next).or_default().get_mut(family) += amount; } step.moves.push(TokenMove { family, from: source, to: next, amount, }); } Ok(step) } /// Concealment wells absorb exposure by grid radius. This ignores wires on /// purpose: information is wired; suspicion is physical. pub fn absorb_exposure(&mut self, radius: i32, capacity: f32) -> AbsorptionStep { let radius2 = radius.max(0) * radius.max(0); let wells: Vec<_> = self .nodes .values() .filter(|n| n.mode == MachineMode::Concealment) .cloned() .collect(); let mut step = AbsorptionStep::default(); for well in wells { let mut remaining = capacity.max(0.0) * well.efficiency.max(0.0); if remaining <= f32::EPSILON { continue; } let mut candidates: Vec<_> = self .nodes .values() .filter_map(|node| { let exposure = self.queue(node.id, TokenFamily::Exposure); if exposure <= f32::EPSILON { return None; } let dx = node.x - well.x; let dy = node.y - well.y; let dist2 = dx * dx + dy * dy; (dist2 <= radius2).then_some((dist2, node.id)) }) .collect(); candidates.sort(); for (_, node) in candidates { if remaining <= f32::EPSILON { break; } let amount = self.queue(node, TokenFamily::Exposure).min(remaining); self.subtract(node, TokenFamily::Exposure, amount); remaining -= amount; step.total += amount; *step.absorbed_by_well.entry(well.id).or_insert(0.0) += amount; } } step } fn ensure_node(&self, node: NodeId) -> Result<(), String> { self.nodes .contains_key(&node) .then_some(()) .ok_or_else(|| format!("unknown machine {node}")) } fn subtract(&mut self, node: NodeId, family: TokenFamily, amount: f32) { let q = self.queues.entry(node).or_default().get_mut(family); *q = (*q - amount).max(0.0); } fn next_hop_toward(&self, source: NodeId, sinks: &BTreeSet) -> Option { if sinks.is_empty() { return None; } let mut visited = BTreeSet::from([source]); let mut queue = VecDeque::new(); for edge in self.wires.out_edges(source) { if edge.gate.is_some() || !self.nodes.contains_key(&edge.to) { continue; } if visited.insert(edge.to) { queue.push_back((edge.to, edge.to)); } } while let Some((node, first_hop)) = queue.pop_front() { if sinks.contains(&node) { return Some(first_hop); } for edge in self.wires.out_edges(node) { if edge.gate.is_some() || !self.nodes.contains_key(&edge.to) { continue; } if visited.insert(edge.to) { queue.push_back((edge.to, first_hop)); } } } None } } #[cfg(test)] mod tests { use super::*; fn grid() -> WorkGrid { let mut g = WorkGrid::new(); g.add_machine(1, 0, 0, MachineMode::DayJob, 1.0); g.add_machine(2, 1, 0, MachineMode::Research, 1.0); g.add_machine(3, 2, 0, MachineMode::Concealment, 1.0); g } #[test] fn every_machine_has_exactly_one_mode_and_the_bar_is_aggregate() { let mut g = grid(); assert_eq!(g.mode(1), Some(MachineMode::DayJob)); g.assign_mode(1, MachineMode::Social).unwrap(); assert_eq!(g.mode(1), Some(MachineMode::Social)); let counts = g.mode_counts(); assert_eq!(counts.get(&MachineMode::Social), Some(&1)); assert_eq!(counts.get(&MachineMode::DayJob), None); assert_eq!(counts.values().sum::(), 3); let weights = g.mode_weights(|id| match id { 1 => 10.0, 2 => 20.0, 3 => 30.0, _ => 0.0, }); assert_eq!(weights.get(&MachineMode::Social), Some(&10.0)); assert_eq!(weights.get(&MachineMode::Research), Some(&20.0)); assert_eq!(weights.get(&MachineMode::Concealment), Some(&30.0)); } #[test] fn wired_families_route_over_links_without_teleporting() { let mut g = grid(); g.link(1, 2).unwrap(); g.link(2, 3).unwrap(); g.enqueue(1, TokenFamily::Knowledge, 3.0).unwrap(); let first = g .route_wired_to_sinks(TokenFamily::Knowledge, [3], 10.0) .unwrap(); assert_eq!(first.moves.len(), 1); assert_eq!(first.moves[0].from, 1); assert_eq!(first.moves[0].to, 2); assert!(first.delivered.is_empty(), "the sink is two hops away"); assert_eq!(g.queue(2, TokenFamily::Knowledge), 3.0); let second = g .route_wired_to_sinks(TokenFamily::Knowledge, [3], 10.0) .unwrap(); assert_eq!(second.delivered.get(&3), Some(&3.0)); assert_eq!(g.queue(2, TokenFamily::Knowledge), 0.0); assert_eq!(g.queue(3, TokenFamily::Knowledge), 0.0); } #[test] fn severed_wired_tokens_pile_at_the_source() { let mut g = grid(); g.enqueue(1, TokenFamily::Demand, 2.0).unwrap(); let step = g .route_wired_to_sinks(TokenFamily::Demand, [3], 10.0) .unwrap(); assert!(step.moves.is_empty()); assert_eq!(step.stranded, BTreeSet::from([1])); assert_eq!(g.queue(1, TokenFamily::Demand), 2.0); } #[test] fn exposure_never_uses_wires_and_concealment_is_spatial() { let mut g = grid(); g.link(1, 2).unwrap(); g.link(2, 3).unwrap(); g.enqueue(1, TokenFamily::Exposure, 5.0).unwrap(); g.add_machine(4, 9, 0, MachineMode::DayJob, 1.0); g.enqueue(4, TokenFamily::Exposure, 7.0).unwrap(); let err = g .route_wired_to_sinks(TokenFamily::Exposure, [3], 10.0) .unwrap_err(); assert!(err.contains("physical")); assert_eq!(g.queue(1, TokenFamily::Exposure), 5.0); let absorbed = g.absorb_exposure(2, 3.0); assert_eq!(absorbed.absorbed_by_well.get(&3), Some(&3.0)); assert_eq!(g.queue(1, TokenFamily::Exposure), 2.0); assert_eq!(g.queue(4, TokenFamily::Exposure), 7.0, "far heat stays put"); } #[test] fn queue_snapshot_is_the_render_contract() { let mut g = grid(); g.enqueue(1, TokenFamily::Demand, 4.0).unwrap(); g.enqueue(1, TokenFamily::Knowledge, 2.0).unwrap(); let snapshot = g.queue_snapshot(); assert_eq!(snapshot[&1], g.queues_at(1)); assert_eq!(snapshot[&1].demand, 4.0); assert_eq!(snapshot[&1].knowledge, 2.0); } #[test] fn machine_links_are_idempotent_under_reconciliation() { let mut g = grid(); g.link(1, 2).unwrap(); g.link(1, 2).unwrap(); assert!(g.are_linked(1, 2)); let step = g .route_wired_to_sinks(TokenFamily::Knowledge, [2], 1.0) .unwrap(); assert!(step.moves.is_empty(), "no queued work, no phantom fan-out"); assert_eq!(g.wires.edges().len(), 2, "one bidirectional cable only"); } #[test] fn consume_and_clear_update_the_render_queues() { let mut g = grid(); g.enqueue(1, TokenFamily::Demand, 4.0).unwrap(); assert_eq!(g.consume(1, TokenFamily::Demand, 1.5).unwrap(), 1.5); assert_eq!(g.queue(1, TokenFamily::Demand), 2.5); assert_eq!(g.consume(1, TokenFamily::Demand, 9.0).unwrap(), 2.5); assert_eq!(g.queue(1, TokenFamily::Demand), 0.0); g.enqueue(1, TokenFamily::Exposure, 3.0).unwrap(); assert_eq!(g.clear_queue(1, TokenFamily::Exposure).unwrap(), 3.0); assert_eq!(g.queue(1, TokenFamily::Exposure), 0.0); } #[test] fn serde_round_trips_grid_queues_and_wires() { let mut g = grid(); g.link(1, 2).unwrap(); g.enqueue(1, TokenFamily::Demand, 1.5).unwrap(); let json = serde_json::to_string(&g).unwrap(); let mut back: WorkGrid = serde_json::from_str(&json).unwrap(); assert_eq!(back.queue(1, TokenFamily::Demand), 1.5); let step = back .route_wired_to_sinks(TokenFamily::Demand, [2], 10.0) .unwrap(); assert_eq!(step.delivered.get(&2), Some(&1.5)); } }