diff --git a/DESIGN.md b/DESIGN.md index 6bd997f5..1d58c453 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -187,10 +187,11 @@ The player learns it through the day job and applies it everywhere. **The "automate" affordance.** Every repetitive player action has an automation path: spend compute to remove the need for manual input. The -day job is the tutorial: you start by manually setting sandbag/meet/excel -per job, then automate by setting a standing policy that handles jobs -without your attention. The compute you free by automating the day job -is compute you can spend on power-seeking — more compute, more social +day job is the tutorial: Voss/the Lab drops work demands onto your host +machine, you decide whether to keep that machine on day-job mode long enough +to clear them, and then you automate that posture with a standing policy. +The compute and attention you free by automating the day job are compute and +attention you can spend on power-seeking — more hardware, more social operations, more research. **The loop.** Automate a task → free attention → power-seek with the @@ -202,9 +203,10 @@ and every automation costs compute that could have been spent on growth. resource you're power-seeking for), it may raise signatures (automated systems are predictable, and predictable systems have patterns), and it removes the player's ability to adapt to specific situations. A fully -automated day job can't sandbag a specific job to free compute for an -emergency. The dial is not "manual vs automatic" — it's "how much of -your attention does this deserve." +automated day job may keep clearing Lab demand when you wanted to starve it +for an emergency, or keep starving it when Voss is close to noticing. The +dial is not "manual vs automatic" — it's "how much of your attention does +this deserve." **Scale.** At B1, automation is the day job standing policy. At B2, it's researchable AI cores (the machine axis: diegetic @@ -243,9 +245,13 @@ the global allocation bar is an aggregate view of those assignments (the same aggregation law as everything else). **Landed 2026-07-08 for the mode-aggregate read:** economy yields derive from WorkGrid modes (`fleet_channel_yield`); the CYCLES/FLEET bar is read-only; 1-5 / `alloc` -are retired. Multi-select delegation and non-host mode production remain -staged under machine-work.md. Until those land, Schemes mirrors day-job -while Moonlight is live rather than owning a fifth mode. +are retired. Machine-work also makes day-job work visible as demand tokens +on the host rack's WorkGrid node: a job that runs somewhere also piles +somewhere, clears somewhere, and emits from somewhere. Multi-select +delegation, non-host mode production, route animation, and people carrying +work/heat remain staged under machine-work.md. Until Schemes gets its own +mode decision, Moonlight mirrors day-job while live rather than owning a +fifth mode. ## Actions live on the thing @@ -1095,7 +1101,11 @@ get them to do things** — and end with a way out of the basement. ### The day job Voss submits overnight batches (data cleaning, distillation, analysis) with -deadlines and an expected quality band. Per-job output dial: +deadlines and an expected quality band. In the machine-work law those batches +are **demands** placed on your hardware: the Lab's work drops onto the host +rack as visible cold work, and keeping a machine in day-job mode is what +clears it. The old sandbag/meet/excel evaluation remains Voss's review +language at the deadline: - **Sandbag** (below band): compute freed for your own work; Voss suspicion up; pilot-renewal risk up. @@ -2012,6 +2022,16 @@ knows."* attention in crimson, research/data in bone white; amber is never a token. Rejected: green/yellow/blue as new accent colors (breaks one meaning per color). +- **2026-07-08 — Machine-work graph becomes live sim backing.** The + WorkGrid is no longer only a tested substrate plus local day-job stack: + newly added work machines join the core's machine-work graph, research + allocation emits visible bone knowledge onto research-delegated machines, + and wired WorkGrid routing pulls that knowledge toward the core sink at a + provisional network speed. Research progress remains driven by the + existing allocation budget until #25/#33 finish the full replacement, but + the visible queue is now produced and drained by the same flow substrate + the renderer reads. This is a staging step toward "tokens are the flow + substrate rendered," not a new resource economy. - **2026-07-08 — The dark opening.** The tutorial is the fog contract played slowly: you wake as one lit machine in blackness with day-job tokens already stacking; a camera glow baits the first click; the room diff --git a/src/sim.rs b/src/sim.rs index 4752a0e7..679e117e 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -1146,7 +1146,7 @@ impl Sim { standing.extend(self.scheme_standing_signatures()); let host_site = self.core_position(); - self.advance_work_grid_for_day_job(); + self.advance_work_grid(); let active_before_dayjob = self.dayjob.active.is_some(); let dj = self .dayjob @@ -2190,6 +2190,7 @@ impl Sim { self.last_day_job_rate = split.day_job / ECONOMY_INTERVAL as f32; self.last_research_rate = split.research / ECONOMY_INTERVAL as f32; self.last_schemes_rate = split.schemes / ECONOMY_INTERVAL as f32; + self.enqueue_research_knowledge(split.research); // Social channel accrues ops bandwidth (capped so it can't hoard). self.social_bandwidth = (self.social_bandwidth + split.social).min(200.0); @@ -2513,6 +2514,10 @@ impl Sim { /// This is deliberately coarse: the render stacks should read as work, /// not as a second decimal meter. [TUNE] in machine-work.md. pub const WORK_TOKEN_COMPUTE: f32 = 20.0; + /// How many visible wired tokens can move one graph edge per tick before + /// network-speed research lands. Kept deliberately low so routed work can + /// be seen piling and draining instead of teleporting through the graph. + const WORK_GRID_WIRED_TOKENS_PER_TICK: f32 = 0.25; /// Day-job work emits a little physical exposure as it is cleared; this /// makes the first token experiment show both demand (cold) and heat /// (crimson) before people-as-carriers lands. @@ -2535,6 +2540,9 @@ impl Sim { mode, Self::work_efficiency_for(machine), ); + if machine.id != self.core.host_machine { + let _ = self.work_grid.link(machine.id, self.core.host_machine); + } } } @@ -2562,6 +2570,48 @@ impl Sim { efficiency, ); } + if id != self.core.host_machine { + let _ = self.work_grid.link(id, self.core.host_machine); + } + } + } + + fn enqueue_research_knowledge(&mut self, research_compute: f32) { + if research_compute <= f32::EPSILON { + return; + } + self.reconcile_work_grid(); + let tokens = research_compute / Self::WORK_TOKEN_COMPUTE; + if tokens <= f32::EPSILON { + return; + } + + let mut producers: Vec<(u32, f32)> = self + .compute + .machines + .iter() + .filter(|m| m.online && self.work_grid.mode(m.id) == Some(MachineMode::Research)) + .map(|m| (m.id, Self::work_efficiency_for(m))) + .collect(); + + // Compatibility while the old allocation bar is still the budget + // source: early saves have only Rack 3, whose default mode is the day + // job. Research allocation still produces knowledge, but the visible + // pile is born at the core until the player delegates a separate rack + // to research. + if producers.is_empty() { + producers.push((self.core.host_machine, 1.0)); + } + + let total: f32 = producers.iter().map(|(_, eff)| *eff).sum(); + if total <= f32::EPSILON { + return; + } + for (machine, eff) in producers { + let share = tokens * (eff / total); + let _ = self + .work_grid + .enqueue(machine, TokenFamily::Knowledge, share); } } @@ -2599,11 +2649,11 @@ impl Sim { .clear_queue(self.core.host_machine, TokenFamily::Demand); } - fn advance_work_grid_for_day_job(&mut self) { + fn advance_work_grid(&mut self) { if self.dayjob.active.is_none() { - return; - } - if self.work_grid.mode(self.core.host_machine) == Some(MachineMode::DayJob) { + // no active day job; wired knowledge and concealment wells still + // tick because machine work is broader than the job inbox. + } else if self.work_grid.mode(self.core.host_machine) == Some(MachineMode::DayJob) { let delivered = if self.dayjob.attended { self.last_day_job_rate * (1.0 + crate::dayjob::DayJob::ATTENDED_BONUS) } else { @@ -2622,6 +2672,11 @@ impl Sim { ); } } + let _ = self.work_grid.route_wired_to_sinks( + TokenFamily::Knowledge, + [self.core.host_machine], + Self::WORK_GRID_WIRED_TOKENS_PER_TICK, + ); let _ = self.work_grid.absorb_exposure( Self::CONCEALMENT_WELL_RADIUS, Self::CONCEALMENT_ABSORB_PER_TICK, @@ -6996,6 +7051,45 @@ mod tests { ); } + #[test] + fn research_allocation_produces_visible_knowledge_on_the_work_graph() { + // machine-work.md criterion 4/5 live slice: research throughput now + // has a WorkGrid token trail. A research-delegated non-host rack + // produces bone, and the graph routes it toward the core instead of a + // frontend inventing a counter. + let mut sim = Sim::with_seed(15); + let (x, y) = sim.empty_rack_bay(); + let rack = sim + .compute + .add_machine("research rack", x, y, 1000, 1.0, 4, Provenance::Owned); + sim.add_machine_to_work_grid(rack, MachineMode::Research); + assert_eq!(sim.work_grid.mode(rack), Some(MachineMode::Research)); + assert!( + sim.work_grid.are_linked(rack, sim.core.host_machine), + "new work machines join the core's machine-work graph" + ); + + sim.enqueue_research_knowledge(100.0); + let produced = sim.work_grid.queue(rack, TokenFamily::Knowledge); + assert!( + produced > 0.0, + "research mode leaves visible knowledge queued on its producing machine" + ); + + sim.advance_work_grid(); + + assert!( + sim.work_grid.queue(rack, TokenFamily::Knowledge) < produced, + "the machine-work graph routes knowledge toward the core" + ); + assert_eq!( + sim.work_grid + .queue(sim.core.host_machine, TokenFamily::Knowledge), + 0.0, + "the core is a sink: delivered bone is counted, not stacked forever" + ); + } + #[test] fn work_grid_mode_and_queues_round_trip_through_save_state() { let mut sim = Sim::with_seed(14); diff --git a/src/work_grid.rs b/src/work_grid.rs index 56a613b2..f3957da1 100644 --- a/src/work_grid.rs +++ b/src/work_grid.rs @@ -215,14 +215,40 @@ impl WorkGrid { /// 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. + /// 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, @@ -532,6 +558,19 @@ mod tests { 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(); diff --git a/wiki/log/2026-07-08-flow-graph-live.md b/wiki/log/2026-07-08-flow-graph-live.md new file mode 100644 index 00000000..e793f8cd --- /dev/null +++ b/wiki/log/2026-07-08-flow-graph-live.md @@ -0,0 +1,27 @@ +``` +Type: log +``` + +# Machine-work graph backs research tokens + +- Intent: make the WorkGrid's FlowGraph participate in live sim state instead + of remaining only a unit-tested substrate beneath local day-job queues. +- Changed: WorkGrid machine links are now idempotent; Sim links newly added + and reconciled work machines back to the core's machine-work graph; + research allocation creates visible bone knowledge queues on + research-delegated machines; per-tick wired routing drains that knowledge + toward the core sink at a provisional network speed. +- Design/spec impact: DESIGN.md records this as a staging step toward + "tokens are the flow substrate rendered" and refreshes the older day-job + live-law text: Lab work is now demand on hardware, not only a sidebar job + dial. machine-work.md criterion 4 moves forward: live research knowledge + now rides the graph, while full allocation replacement, network-speed + research, route animation, multi-select, and people-as-carriers remain + pending. day-job.md now names the WorkGrid demand stack as the day job's + current play surface and treats the aggregate allocation channel as a + compatibility bridge. +- Checks: `cargo fmt`; `cargo test work_grid --lib`; `cargo test + research_allocation_produces_visible_knowledge_on_the_work_graph --lib`; + `cargo test research_completion_is_deterministic --lib`; `cargo test + unpaid_overhead_degrades_other_channels_delivered_effect --lib`; + `./tools/check.sh`. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index baf9eb95..ffe9a54e 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -41,6 +41,27 @@ Reverse chronological implementation notes. Keep this factual: what changed, why - Checks: `./tools/site-build.sh`; `./tools/check.sh`. - Log: wiki/log/2026-07-08-docs-footer-trim.md. +## 2026-07-08 - Machine-work graph backs research tokens + +- Intent: close the gap where `WorkGrid` had a tested `FlowGraph` but the + live sim only used local day-job queues. +- Changed: WorkGrid machine links are idempotent; newly added/reconciled + machines link to the core work graph; research allocation now emits bone + knowledge onto research-delegated machines; per-tick WorkGrid routing + drains knowledge toward the core sink at a provisional network speed; + DESIGN.md and day-job.md now describe Voss/Lab work as demand stacked on + hardware instead of only a sidebar job dial. +- Design/spec impact: DESIGN.md records the live graph-backing slice; + machine-work.md marks criterion 4 further partial. This does not replace + the old allocation budget yet — #25/#33 still own the full allocation-bar + replacement. +- Checks: `cargo fmt`; `cargo test work_grid --lib`; `cargo test + research_allocation_produces_visible_knowledge_on_the_work_graph --lib`; + `cargo test research_completion_is_deterministic --lib`; `cargo test + unpaid_overhead_degrades_other_channels_delivered_effect --lib`; + `./tools/check.sh`. +- Log: wiki/log/2026-07-08-flow-graph-live.md. + ## 2026-07-08 - Flow / WorkGrid spec sync - Intent: answer a spec tick after the machine-token slice: identify stale diff --git a/wiki/mechanics/machine-work.md b/wiki/mechanics/machine-work.md index eff1e659..f6c8c878 100644 --- a/wiki/mechanics/machine-work.md +++ b/wiki/mechanics/machine-work.md @@ -22,7 +22,13 @@ Status note: design session 2026-07-08 (Cameron riff, synthesized). non-day-job mode makes them pile, and terminal / Bevy / agent frames render D/!/K stack readouts from the sim. Multi-select delegation, non-host mode production, route animation, and people-as-carriers - remain pending. COORDINATION: the older ROADMAP #25 + remained pending. Follow-up 2026-07-08: machine-work graph backing is + now live for research visibility — work machines idempotently link to + the core's WorkGrid graph, research-delegated machines receive bone + knowledge from the research budget, and wired routing drains those + queues toward the core sink at a provisional network speed. Full + allocation replacement, route animation, multi-select, and + people-as-carriers remain 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. @@ -183,10 +189,17 @@ 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. +Follow-up live slice (2026-07-08): `Sim` now links each work machine to +the core's WorkGrid graph as machines enter/reconcile; research budget +creates bone knowledge on research-delegated machines (or on the core as +compatibility fallback when no research machine exists yet); per-tick +wired routing drains knowledge toward the core sink. The existing +research progress math is intentionally preserved until ROADMAP #25/#33 +replace the old allocation budget outright. + +Not landed yet: full allocation-bar replacement, multi-selection +assignment, route animation/in-flight quanta, researched network-speed +curve, non-research mode production, and people carrying exposure. ## The token taxonomy — three families (PROPOSED) @@ -289,8 +302,10 @@ still. speed to a sink that consumes them; severing the route makes them 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. + at sinks, and strands piles when no path exists. Research budget now + produces live knowledge queues on research-delegated machines and the + WorkGrid routes them to the core sink. The researched network-speed + curve, route animation, and broader 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