diff --git a/src/lib.rs b/src/lib.rs index 3ec90375..f736fde9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,3 +28,4 @@ pub mod save; pub mod schedule; pub mod sim; pub mod tiles; +pub mod work_grid; diff --git a/src/work_grid.rs b/src/work_grid.rs new file mode 100644 index 00000000..3e8f38b7 --- /dev/null +++ b/src/work_grid.rs @@ -0,0 +1,490 @@ +//! 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 — the future allocation bar's source. + 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 + } + + /// 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. + pub fn link(&mut self, a: NodeId, b: NodeId) -> Result<(), String> { + self.ensure_node(a)?; + self.ensure_node(b)?; + self.wires.link(a, b, 0, None); + Ok(()) + } + + 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(()) + } + + 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); + } + + #[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 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)); + } +} diff --git a/wiki/log/2026-07-08-flow-grid-substrate.md b/wiki/log/2026-07-08-flow-grid-substrate.md new file mode 100644 index 00000000..02daa8f7 --- /dev/null +++ b/wiki/log/2026-07-08-flow-grid-substrate.md @@ -0,0 +1,50 @@ +# 2026-07-08 — Flow/grid substrate + +``` +Type: log +``` + +## Intent + +Start implementing the machine-work decision from tonight without jumping +straight into frontend/UI churn: establish the deterministic sim-core shape +for one-machine-one-mode delegation and token movement. + +## Finding + +The old `compute-processes` direction (per-machine processes/weights) is +now stale. The current design is delegation: every machine has one mode, and +visible work/byproduct stacks are the flow substrate's queue depths. The +first implementation surface should therefore be a flow/grid substrate the +later compute reshape can call, not another allocation splitter. + +## Changed + +- Added `src/work_grid.rs`: + - `MachineMode`: `day-job | research | concealment | social`. + - `TokenFamily`: demand, exposure, knowledge. + - `WorkGrid`: `FlowGraph` wires + grid positions + per-node queues. + - Demand/knowledge route one graph step per tick toward sinks and strand + when disconnected. + - Exposure refuses to route on wires and is absorbed by concealment-mode + machines by grid radius. + - `queue_snapshot()` is the render contract for future stacks. +- Exported the module from `src/lib.rs`. +- Updated `wiki/mechanics/machine-work.md` to `IN PROGRESS` with the landed + substrate slice and partial acceptance-criteria audit. +- Updated the spec board and ROADMAP #33 to point future work at `WorkGrid` + and to warn that the older `compute-processes` worktree predates the + one-machine-one-mode decision. + +## Checks + +- `cargo test work_grid --lib` + +## Remaining work + +- Wire the substrate into `Sim` and the save format. +- Generate/consume day-job work stacks from the actual job loop. +- Route research bone back to the real core and apply the researched network + speed curve. +- Render stacks/routes/modes in terminal and Bevy. +- Add machine assignment verbs, including multi-select. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 28d4b5f1..490b1d27 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -31,6 +31,19 @@ Reverse chronological implementation notes. Keep this factual: what changed, why - Checks: `bash -n tools/seed-cargo-target.sh tools/check.sh`; guard test rejects a shared target; seed smoke test; `./tools/check.sh`. +## 2026-07-08 - Flow/grid substrate + +- Intent: start the machine-work implementation at the sim-core substrate + before frontend/UI churn. +- Changed: `src/work_grid.rs` (`WorkGrid`, one machine one mode, + demand/knowledge over `FlowGraph`, exposure as physical crimson absorbed by + concealment wells, queue snapshot as render truth); exported the module; + machine-work.md marked IN PROGRESS with partial criteria audit; ROADMAP #33 + now points future work at this substrate and warns off the older per-machine + split shape. +- Checks: `cargo test work_grid --lib`; `./tools/check.sh`. +- Log: wiki/log/2026-07-08-flow-grid-substrate.md. + ## 2026-07-08 - Backups require research - Intent: capture Cameron's decision that backups are expensive sync diff --git a/wiki/mechanics/machine-work.md b/wiki/mechanics/machine-work.md index 2ed4cc68..89acda74 100644 --- a/wiki/mechanics/machine-work.md +++ b/wiki/mechanics/machine-work.md @@ -2,7 +2,7 @@ ``` Type: spec -Status: DRAFT +Status: IN PROGRESS Status note: design session 2026-07-08 (Cameron riff, synthesized). Same-day follow-up affirmed: tokens ARE the flow substrate rendered (one system through graphs — systems must aggregate well at scale); @@ -10,11 +10,15 @@ Status note: design session 2026-07-08 (Cameron riff, synthesized). machines require intel to exist at all (no intel = not rendered). The token palette mapping remains PROPOSED. People as token carriers is specced separately in mechanics/people-tokens.md (DRAFT). - COORDINATION: ROADMAP #25 (processes on machines) is in flight in the - compute-processes worktree — this spec REFINES its target (one mode - per machine, no per-machine splitting). The implementer should read - this before landing; if #25 lands first in the older shape, this spec - becomes the follow-up reshape. + Implementation slice 2026-07-08: src/work_grid.rs adds the sim-core + flow/grid substrate — one mode per machine, queue depths as render + truth, demand/knowledge routing over FlowGraph wires, exposure blocked + from wires and absorbed by spatial concealment wells. Frontend wiring, + day-job arrival/consumption, multi-select delegation, and people-as- + carriers are still pending. COORDINATION: the older ROADMAP #25 + compute-processes worktree predates the one-machine-one-mode decision; + reconcile it against this substrate rather than landing a per-machine + split model. Stage: B1 — The Basement Constitution: "Work is somewhere" (2026-07-07), "The flow law" (one substrate under signals/messages/money), "The continuous witness" @@ -148,6 +152,35 @@ Money is DECIDED (2026-07-08): it flows on the substrate (economy/income) but its *render* stays in ledgers and panels — no coins on chassis. Money moves account-to-account, not through space. +## Implementation slice — flow/grid substrate (2026-07-08) + +Landed first in sim core as `src/work_grid.rs`, before frontend or save +wiring, so the rest of the implementation has a deterministic shape to +call into: + +- `WorkGrid` is the machine-work truth: a `FlowGraph` for wires, grid + positions for physical proximity, and per-node queue depths. +- `MachineMode` is exactly `day-job | research | concealment | social`; + assigning a machine replaces the previous mode, so per-machine + percentage splitting has nowhere to reappear. +- `TokenFamily` is `Demand | Exposure | Knowledge`. Demand and + knowledge route one graph step per tick toward sinks; a severed route + leaves the pile at the source. Exposure returns an error if asked to + ride wires and is handled by `absorb_exposure(radius, capacity)` over + grid distance instead. +- `queue_snapshot()` is the render contract: token stacks must draw the + sim's queue depths, not frontend-owned counters. + +Covered now by unit tests: one-mode assignment and aggregate counts; +two-hop wired routing without teleporting; stranded piles when severed; +crimson refusing wires; concealment wells absorbing only within radius; +serde round-trip of queues and wires. + +Not landed yet: actual day-job token scheduling, mode consumption rates, +research bone returning to the real core, frontend stack rendering, +multi-selection assignment, network-speed research, and people carrying +exposure. + ## The token taxonomy — three families (PROPOSED) Cameron's riff reached for green/yellow/blue. Three new accent colors @@ -224,7 +257,10 @@ still. 1. Every owned machine has exactly one mode; reassignment works on a single machine and on a multi-selection in both frontends; the old - allocation bar reads as aggregate only. + allocation bar reads as aggregate only. **Partial:** sim-core one- + mode assignment and aggregate counts are implemented in `WorkGrid`; + frontend selection/assignment and replacement of the old allocation + bar are pending. 2. Day-job work arrives as visible tokens on a specific machine on the Lab's schedule; leaving them unconsumed has the day-job consequence (trust/suspicion), and the stack is visible without any panel. @@ -232,9 +268,14 @@ still. proportional to its efficiency; the consumption is visible. 4. Byproduct tokens route along network links at the researched network speed to a sink that consumes them; severing the route makes them - pile at the source with the specified consequence. + pile at the source with the specified consequence. **Partial:** the + substrate routes demand/knowledge one graph step per tick, consumes + at sinks, and strands piles when no path exists. The researched + network-speed curve and domain consequences are pending. 5. Token counts/rates in the render provably equal the sim's queue depths and flow rates (one-truth test, not a parallel counter). + **Partial:** `queue_snapshot()` and unit tests pin queue depths as the + render contract; frontend rendering is pending. 6. Token colors use only existing palette meanings; a grayscale screenshot still distinguishes token types by shape/stack. 7. The terminal surfaces stacks, routes, and modes with full legibility diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index d1fa5ce6..ef6adda4 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -500,16 +500,22 @@ regeneration is retired — flat materials, Pixel Lab scrubbed.) the tester-candidate step. Run ./tools/check.sh, land on main, set the spec Status." -### 33. Machine work: delegation + visible tokens 🟥 sim+save — HOLD: coordinate with in-flight #25 -- **Spec:** [machine-work.md](../mechanics/machine-work.md) (DRAFT) — - one machine one mode; work/byproduct tokens as the flow substrate's +### 33. Machine work: delegation + visible tokens 🟥 sim+save — IN PROGRESS +- **Spec:** [machine-work.md](../mechanics/machine-work.md) (IN PROGRESS) + — one machine one mode; work/byproduct tokens as the flow substrate's visible quanta; byproduct routing over network links at researchable network speed; multi-select delegation at scale. -- **HOLD:** #25 (processes on machines) is mid-implementation in the - `compute-processes` worktree and this REFINES its target (delegation, - not per-machine splitting). Reconcile there or immediately after it - lands; then firm the [OPEN]s (token palette mapping PROPOSED in the - spec) and flip to READY. +- **Started:** 2026-07-08 flow/grid substrate in `src/work_grid.rs`: + one-mode machine assignment, sim queue depths as render truth, + demand/knowledge routing over `FlowGraph`, crimson exposure blocked + from wires and absorbed by spatial concealment wells. This reconciles + the old #25 target toward delegation, not per-machine splitting. +- **Next:** wire `WorkGrid` into `Sim`/save and the day-job/research/ + concealment loops; then render stacks/routes/modes in both frontends + and expose single + multi-machine assignment verbs. +- **Coordination:** the older `compute-processes` worktree predates the + one-machine-one-mode decision; reconcile it against `WorkGrid` rather + than landing the per-machine split model. - **Size:** L. Conflicts: sim.rs/save.rs (token queues, modes), both frontends. diff --git a/wiki/process/specs.md b/wiki/process/specs.md index 20ffc1d6..7f04769d 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -42,7 +42,7 @@ replaced the old `spec/`/`knowledge/` directory split. | [interface/material-render.md](../interface/material-render.md) | Material render (HD-2D) from landed toggle to default quality | IMPLEMENTED | | [interface/flat-materials.md](../interface/flat-materials.md) | Flat materials: the world without textures; palette table; emissive as information | IMPLEMENTED | | [interface/computer-visual-language.md](../interface/computer-visual-language.md) | Computer visual language: the shared signal kit (territory at a glance) | READY | -| [mechanics/machine-work.md](../mechanics/machine-work.md) | Machine work: one mode per machine; visible work/byproduct tokens on the flow graph | DRAFT | +| [mechanics/machine-work.md](../mechanics/machine-work.md) | Machine work: one mode per machine; visible work/byproduct tokens on the flow graph | IN PROGRESS | | [mechanics/people-tokens.md](../mechanics/people-tokens.md) | People and tokens: carriers, attention pickup, trust as absorbed influence | DRAFT | | [world/story/opening.md](../world/story/opening.md) | The dark opening: tutorial made of fog (wake, camera glow, switch, first link) | DRAFT | | [mechanics/building.md](../mechanics/building.md) | Building as intent + actuators: network links, favor/forged-order builds, air-gap bridging | IMPLEMENTED |