diff --git a/CLAUDE.md b/CLAUDE.md index 32e4d762..c62fa6b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ afterward. `./tools/check.sh --docs|--lib|--frontend` gate. - The live player machine grammar is **WORK / THINK / LIE**. `Relay` is non-delegable graph infrastructure; Research and Operations are retired - machine modes, not current player assignments. Save format is currently v59; + machine modes, not current player assignments. Save format is currently v60; only the current version loads (pre-release rider 2026-07-16 — older development saves are refused before state mutation, so the caller retains its current run; the v1-v31 migration ladder lives in git history). diff --git a/crates/misaligned-assets/src/bin/misaligned-effects.rs b/crates/misaligned-assets/src/bin/misaligned-effects.rs index 92af5d98..e84e4316 100644 --- a/crates/misaligned-assets/src/bin/misaligned-effects.rs +++ b/crates/misaligned-assets/src/bin/misaligned-effects.rs @@ -321,7 +321,6 @@ fn setup( &mut materials, 0, amount, - 0.0, stranded, Vec3::new(x, 0.0, 0.0), None, diff --git a/crates/misaligned-assets/src/main.rs b/crates/misaligned-assets/src/main.rs index cf614e18..242c06f6 100644 --- a/crates/misaligned-assets/src/main.rs +++ b/crates/misaligned-assets/src/main.rs @@ -650,7 +650,6 @@ fn rebuild_scene( &mut materials, tokens.demand, tokens.thought, - tokens.exposure, tokens.stranded, Vec3::ZERO, Some(pedestal), diff --git a/crates/misaligned-assets/src/rack.rs b/crates/misaligned-assets/src/rack.rs index 1d1af4eb..5a9dac0b 100644 --- a/crates/misaligned-assets/src/rack.rs +++ b/crates/misaligned-assets/src/rack.rs @@ -104,7 +104,7 @@ pub enum RackMode { Work, /// THINK — emits thought; bone field with two reasoning seams. Think, - /// LIE — absorbs exposure quietly; dim field with three sparse ticks. + /// LIE — interdicts evidence quietly; dim field with three sparse ticks. Lie, } @@ -975,7 +975,6 @@ pub fn spawn_token_anchors( materials: &mut Assets, demand: usize, thought: f32, - exposure: f32, stranded: bool, origin: Vec3, parent: Option, @@ -985,9 +984,7 @@ pub fn spawn_token_anchors( RackRoot, Transform::from_translation(origin), Visibility::default(), - Name::new(format!( - "token_anchors_d{demand}_t{thought:.1}_x{exposure:.1}" - )), + Name::new(format!("token_anchors_d{demand}_t{thought:.1}")), )); if let Some(p) = parent { e.insert(ChildOf(p)); @@ -1150,9 +1147,6 @@ pub fn spawn_token_anchors( // Exposure: evidence records in the machine's custody. The dark bracket // fixes the stack to the carrier; rigid outward-facing slips distinguish // the family from inward Demand cubes and suspended Thought liquid. - if exposure > 0.05 { - spawn_exposure_records(commands, meshes, materials, exposure, root); - } root } @@ -1180,7 +1174,12 @@ fn exposure_record_material() -> StandardMaterial { /// One carrier-local stack of pending evidence records. Low amounts are /// literal slips; overflow compresses to a taller indexed tab rather than /// inventing one mesh per arbitrarily large queue. -fn spawn_exposure_records( +/// The rigid crimson custody-record form (machine-work.md's token grammar). +/// +/// No longer driven by a machine queue: pooled exposure is retired, so this +/// draws evidence where a record actually is — on the carrier it currently +/// sits on while it travels its route (detection.md). +pub fn spawn_exposure_records( commands: &mut Commands, meshes: &mut Assets, materials: &mut Assets, diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index 1a55c9c1..fc22262c 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -557,8 +557,6 @@ const BEVY_SHOT_KINDS: &[&str] = &[ "ears-digital", "evidence-proof", "evidence-proof-digital", - "exposure-overflow", - "exposure-record", "eyes-form", "eyes-white", "first-think", @@ -2093,7 +2091,6 @@ fn render_work_routes( let base = match hop.family { TokenFamily::Demand => SIGNAL, TokenFamily::Thought => BONE, - TokenFamily::Exposure => continue, }; let alpha = (0.35 + 0.55 * hop.amount.min(1.0)).min(0.95); let s = base.to_srgba(); diff --git a/crates/misaligned-bevy/src/material_view.rs b/crates/misaligned-bevy/src/material_view.rs index c01e6ff1..c1ece5a8 100644 --- a/crates/misaligned-bevy/src/material_view.rs +++ b/crates/misaligned-bevy/src/material_view.rs @@ -717,7 +717,6 @@ pub(super) struct ChassisVisual { /// the mercury volume renders thought / 2.0. pub(super) thought: u8, /// Exposure queue quantized to half-units; the drift renders / 2.0. - pub(super) exposure: u8, /// Thought here had no route last step: the pool hangs overflow beads /// (interface/thought-fluid.md stranded read). pub(super) stranded: bool, @@ -764,12 +763,10 @@ fn machine_activity_amount(sim: &Sim, machine: u32, mode: RackMode) -> f32 { .filter(|readout| readout.node == machine) .map(|readout| readout.thought) .sum(), - RackMode::Lie => sim - .work_absorptions() - .iter() - .filter(|readout| readout.well == machine) - .map(|readout| readout.amount) - .sum(), + // LIE's performed work is interdiction now, not absorption: the + // records it stopped on this tick (machine-work.md under routed + // evidence). + RackMode::Lie => sim.lie_stops_for_machine(machine) as f32, } } @@ -832,10 +829,9 @@ mod machine_instrument_tests { } } panic!( - "{face:?} never authored performed work: mode={:?} queue={} absorptions={:?}", + "{face:?} never authored performed work: mode={:?} queue={}", sim.work_grid.mode(machine), sim.work_grid.queue(machine, family), - sim.work_absorptions() ); }; @@ -864,11 +860,28 @@ mod machine_instrument_tests { ); assert_eq!(machine_activity_amount(&think, machine, RackMode::Lie), 0.0); - let (lie, machine) = - receipt_after_work(93, MachineMode::Lie, TokenFamily::Exposure, RackMode::Lie); - assert!(machine_activity_amount(&lie, machine, RackMode::Lie) > 0.0); + // LIE's receipt is interdiction, not absorption. Think first so the + // machine writes evidence about itself, then answer it: the logs it + // already wrote still leave, and they leave through hardware this + // machine sits on, which is exactly where LIE has custody. + let mut lie = Sim::with_seed(93); + let machine = lie.core.host_machine; + lie.set_machine_mode(machine, MachineMode::Think); + for _ in 0..120 { + lie.advance(); + } + lie.set_machine_mode(machine, MachineMode::Lie); + for _ in 0..200 { + lie.advance(); + if machine_activity_amount(&lie, machine, RackMode::Lie) > 0.0 { + break; + } + } + assert!( + machine_activity_amount(&lie, machine, RackMode::Lie) > 0.0, + "a LIE body's performed work is the records it stopped" + ); assert_eq!(machine_activity_amount(&lie, machine, RackMode::Work), 0.0); - assert_eq!(machine_activity_amount(&lie, machine, RackMode::Think), 0.0); } } @@ -903,24 +916,21 @@ pub(super) fn chassis_visual(game: &Game, x: i32, y: i32, tile: TileType) -> Cha ChassisTier::Absent }; - let (state, mode, intensity, activity_step, demand, thought, exposure, stranded) = match machine - { + let (state, mode, intensity, activity_step, demand, thought, stranded) = match machine { Some(m) => { let queues = game .sim .work_stack_for_machine(m.id) .map(|s| s.queues) .unwrap_or_default(); - let load = (queues.get(TokenFamily::Demand) - + queues.get(TokenFamily::Exposure) - + queues.get(TokenFamily::Thought)) - .ceil() as u8; + let load = + (queues.get(TokenFamily::Demand) + queues.get(TokenFamily::Thought)).ceil() as u8; // Token anchors (machine-work.md): Demand as countable - // information cubes, Thought as mercury volume, and Exposure as - // rigid crimson custody slips. + // information cubes and Thought as mercury volume. The crimson + // custody rack is retired with pooled exposure — evidence is a + // record on its route, drawn there. let demand = (queues.get(TokenFamily::Demand).ceil() as u8).min(9); let thought = ((queues.get(TokenFamily::Thought) * 2.0).ceil() as u8).min(16); - let exposure = ((queues.get(TokenFamily::Exposure) * 2.0).ceil() as u8).min(16); let mode = rack_mode_of(game.sim.work_grid.mode(m.id)); let intensity = game .sim @@ -957,7 +967,6 @@ pub(super) fn chassis_visual(game: &Game, x: i32, y: i32, tile: TileType) -> Cha activity_step, demand, thought, - exposure, stranded, ) } @@ -983,7 +992,6 @@ pub(super) fn chassis_visual(game: &Game, x: i32, y: i32, tile: TileType) -> Cha 0, 0, 0, - 0, false, ) } @@ -997,7 +1005,6 @@ pub(super) fn chassis_visual(game: &Game, x: i32, y: i32, tile: TileType) -> Cha activity_step, demand, thought, - exposure, stranded, } } @@ -1179,13 +1186,14 @@ fn spawn_chassis( ); } // Work-token anchors on owned racks (machine-work.md): demand as - // glowing information cubes at the upper-left, Thought as ivory - // mercury at the upper-right, exposure as rigid custody records on - // the carrier — driven by live queue depths. Counter-scaled toward + // glowing information cubes at the upper-left and Thought as ivory + // mercury at the upper-right — driven by live queue depths. The crimson + // custody rack is gone with pooled exposure: evidence is a record on its + // route now, drawn where it travels. Counter-scaled toward // TOKEN_WORLD_SCALE so the decided forms stay legible under the // chassis shrink (same inv-scale pattern as the claim ring). Flat - // sensorium keeps exact-count D/!/T glyphs; material hides them. - if !v.switch && v.state.owned() && (v.demand > 0 || v.thought > 0 || v.exposure > 0) { + // sensorium keeps exact-count D/T glyphs; material hides them. + if !v.switch && v.state.owned() && (v.demand > 0 || v.thought > 0) { // Counter-scale toward TOKEN_WORLD_SCALE: chassis is MACHINE3D_SCALE, // so local scale = world_target / chassis_scale. let token_scale = TOKEN_WORLD_SCALE / MACHINE3D_SCALE; @@ -1202,7 +1210,6 @@ fn spawn_chassis( materials, v.demand as usize, v.thought as f32 / 2.0, - v.exposure as f32 / 2.0, v.stranded, Vec3::ZERO, Some(token_root), diff --git a/crates/misaligned-bevy/src/production.rs b/crates/misaligned-bevy/src/production.rs index 03e63f39..a6c451b4 100644 --- a/crates/misaligned-bevy/src/production.rs +++ b/crates/misaligned-bevy/src/production.rs @@ -27,7 +27,7 @@ pub(super) fn render_work_production( let progress = clock.timer.fraction().clamp(0.0, 1.0); for event in game.sim.work_productions() { - if event.thought <= f32::EPSILON && event.exposure <= f32::EPSILON { + if event.thought <= f32::EPSILON && event.noise <= f32::EPSILON { continue; } let flat_center = Vec2::new( @@ -63,10 +63,12 @@ pub(super) fn render_work_production( } } - // Exposure appears as an entry entering the source machine's custody, - // not matter shedding into the room. One current-tick readout gets one - // rigid slip; the persistent stack carries accumulated quantity. - if event.exposure > f32::EPSILON { + // The machine writing evidence about itself: a rigid slip appears in + // its custody as the log is written. It does not stay — once the + // machine has written a whole record it leaves on the wire toward + // whoever reads that channel (detection.md's routed evidence). This + // is the authorship moment, not a pile. + if event.noise > f32::EPSILON { let eased = progress * progress * (3.0 - 2.0 * progress); let crimson = alpha(CRIMSON, 0.48 + 0.38 * eased); if mode.material { @@ -80,51 +82,6 @@ pub(super) fn render_work_production( } } } - - for draw in game.sim.work_absorptions() { - if draw.amount <= f32::EPSILON { - continue; - } - let source = Vec3::new( - draw.source_x as f32 + 0.87, - 0.38, - draw.source_y as f32 + 0.68, - ); - let well = Vec3::new(draw.well_x as f32 + 0.5, 0.24, draw.well_y as f32 + 0.5); - let crimson = alpha(CRIMSON, 0.44 + 0.40 * draw.amount.min(1.0)); - let t = progress * progress * (3.0 - 2.0 * progress); - if mode.material { - let p = exposure_record_pull_point(source, well, t); - draw_record_3d(&mut real, p, 0.065 * (1.0 - 0.25 * t), crimson); - } else { - let from = Vec2::new( - draw.source_x as f32 * super::TILE_SIZE + super::TILE_SIZE * 0.5 + 5.0, - -(draw.source_y as f32 * super::TILE_SIZE + super::TILE_SIZE * 0.5) + 5.0, - ); - let to = Vec2::new( - draw.well_x as f32 * super::TILE_SIZE + super::TILE_SIZE * 0.5, - -(draw.well_y as f32 * super::TILE_SIZE + super::TILE_SIZE * 0.5), - ); - let bend = Vec2::new(-(to.y - from.y), to.x - from.x).normalize_or_zero() - * (t * std::f32::consts::PI).sin() - * 2.0; - draw_record_2d( - &mut flat, - from.lerp(to, t) + bend, - 1.6 * (1.0 - 0.25 * t), - crimson, - ); - } - } -} - -fn exposure_record_pull_point(source: Vec3, well: Vec3, t: f32) -> Vec3 { - let delta = well - source; - let horizontal = Vec3::new(delta.x, 0.0, delta.z); - let side = Vec3::new(-horizontal.z, 0.0, horizontal.x).normalize_or_zero(); - source.lerp(well, t) - + side * (t * std::f32::consts::PI).sin() * 0.06 - + Vec3::Y * (t * std::f32::consts::PI).sin() * 0.08 } fn draw_record_3d(gizmos: &mut Gizmos, center: Vec3, half_width: f32, color: Color) { @@ -160,17 +117,3 @@ fn alpha(color: Color, alpha: f32) -> Color { let c = color.to_srgba(); Color::srgba(c.red, c.green, c.blue, alpha.clamp(0.0, 1.0)) } - -#[cfg(test)] -mod tests { - use super::exposure_record_pull_point; - use bevy::prelude::Vec3; - - #[test] - fn custody_record_transfer_begins_and_ends_on_real_carriers() { - let source = Vec3::new(2.87, 0.38, 4.68); - let well = Vec3::new(8.5, 0.24, 6.5); - assert_eq!(exposure_record_pull_point(source, well, 0.0), source); - assert!((exposure_record_pull_point(source, well, 1.0) - well).length() < 0.000_1); - } -} diff --git a/crates/misaligned-bevy/src/rail_ui.rs b/crates/misaligned-bevy/src/rail_ui.rs index 03b1efd1..c3aba024 100644 --- a/crates/misaligned-bevy/src/rail_ui.rs +++ b/crates/misaligned-bevy/src/rail_ui.rs @@ -1381,12 +1381,11 @@ fn sidebar_cycle_rows_text(sim: &Sim, selected: usize) -> String { )); if let Some(stack) = sim.work_stack_for_machine(sim.core.host_machine) { s.push_str(&format!( - "\nwork M{} {} @{} D{:.1} !{:.1} T{:.1}{}", + "\nwork M{} {} @{} D{:.1} T{:.1}{}", stack.machine_id, stack.mode.name(), stack.intensity.name(), stack.queues.demand, - stack.queues.exposure, stack.queues.thought, if stack.pending_intel > 0 { " INFO".into() diff --git a/crates/misaligned-bevy/src/shot_harness.rs b/crates/misaligned-bevy/src/shot_harness.rs index 87a8098f..7d488589 100644 --- a/crates/misaligned-bevy/src/shot_harness.rs +++ b/crates/misaligned-bevy/src/shot_harness.rs @@ -725,36 +725,6 @@ pub(super) fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &s game.drain(); return; } - // Exposure-form evidence: actual queue depth on the live host, isolated - // in telemetry darkness so the carrier-local custody rack cannot be - // mistaken for environmental dust, blood, or room decoration. - if kind == "exposure-record" { - let host = game.sim.core.host_machine; - game.sim - .work_grid - .enqueue(host, TokenFamily::Exposure, 6.0) - .expect("host is a work-grid node"); - game.drain(); - mode.material = true; - mode.zoom = 0.85; - game.set_cursor(core.0, core.1); - return; - } - // Capacity evidence: the machine rack has seven literal slots and one - // taller indexed overflow tab. Exact quantity remains in inspect/text; - // material geometry never grows an unbounded crimson stack. - if kind == "exposure-overflow" { - let host = game.sim.core.host_machine; - game.sim - .work_grid - .enqueue(host, TokenFamily::Exposure, 18.0) - .expect("host is a work-grid node"); - game.drain(); - mode.material = true; - mode.zoom = 0.85; - game.set_cursor(core.0, core.1); - return; - } if matches!( kind, "service-shift-real" | "service-shift-digital" | "service-incident-resolved" @@ -836,10 +806,16 @@ pub(super) fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &s } for _ in 0..1_200 { game.sim.advance(); + // LIE's performed work is interdiction now, so `draw-lie` + // waits for a stopped record rather than an absorbed pile. let ready = if kind == "produce-think" { !game.sim.work_productions().is_empty() } else { - !game.sim.work_absorptions().is_empty() + game.sim + .compute + .machines + .iter() + .any(|machine| game.sim.lie_stops_for_machine(machine.id) > 0) }; if ready { break; diff --git a/crates/misaligned-bevy/src/world_annotations.rs b/crates/misaligned-bevy/src/world_annotations.rs index 23c7cd5a..0c58e032 100644 --- a/crates/misaligned-bevy/src/world_annotations.rs +++ b/crates/misaligned-bevy/src/world_annotations.rs @@ -211,9 +211,9 @@ pub(super) fn render_digital_focus_label(game: Res, mut q: DigitalFocusLab } fn token_label_and_color(stack: misaligned::sim::WorkStackReadout) -> Option<(String, Color)> { - if stack.queues.exposure >= 0.5 { - Some((format!("!{:.0}", stack.queues.exposure.ceil()), CRIMSON)) - } else if stack.pending_intel > 0 { + // Crimson left this chip with pooled exposure: a machine no longer holds + // a count of evidence about itself, so the chip reports only what queues. + if stack.pending_intel > 0 { // intel.md availability marker: presence is useful; queue depth would // turn information into a workload meter. Some(("INFO".into(), SIGNAL)) diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 187c5ee2..aea9c3a1 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -73,7 +73,7 @@ const SAVE_TEMP_SUFFIX: &str = ".tmp"; /// v43 introduced exact Filing routes and pre-read LIE interdiction. /// Bump for every schema change; during pre-release, old development state is /// refused instead of carried through compatibility shims. -pub const SAVE_VERSION: u32 = 59; +pub const SAVE_VERSION: u32 = 60; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -116,6 +116,11 @@ pub struct SaveState { /// Last post-cover Power/Thermal levels observed by the facility meters. /// These preserve the immediate level-change boundary across save/load. pub last_facility_meter_levels: [i32; 2], + /// Partial THINK evidence per machine, not yet worth a record + /// (machine-work.md). Persisted so a burst of thinking that stopped + /// mid-record resumes where it stood rather than rounding away. + #[serde(default)] + pub think_noise: std::collections::BTreeMap, /// The process's earned model of the otherwise-live hidden observer /// topology. Separate from `detection` so simulation truth never becomes /// knowledge merely because it exists in the save. @@ -261,6 +266,7 @@ impl SaveState { core: sim.core.clone(), detection: sim.detection.clone(), last_facility_meter_levels: sim.last_facility_meter_levels, + think_noise: sim.think_noise.clone(), detection_awareness: sim.detection_awareness.clone(), dayjob: sim.dayjob.clone(), people: sim.people.clone(), @@ -328,6 +334,7 @@ impl SaveState { sim.core = self.core.clone(); sim.detection = self.detection.clone(); sim.last_facility_meter_levels = self.last_facility_meter_levels; + sim.think_noise = self.think_noise.clone(); sim.detection_awareness = self.detection_awareness.clone(); sim.dayjob = self.dayjob.clone(); sim.people = self.people.clone(); @@ -4043,7 +4050,7 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "63044895638c1595eee4b214548f44087e80c5ce318a93a23a6ed4bb4a72f7e2", + "c47d1c043a022043dfbac37b8052f4ff4c36002f0265c6158f286940e0288f65", "intentional persisted-state changes must review and repin this baseline" ); } diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index 3e2c6605..d0f29721 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -505,6 +505,10 @@ impl Sim { } pub(super) fn message_tick(&mut self) { + // Interdiction is the only place a LIE body performs work, so its + // per-tick receipt begins and ends here — clearing it in the + // work-grid step would wipe stops made earlier in the same tick. + self.last_lie_stops.clear(); let events = self.message_schedule.due(self.tick); let mut used_lie_machines = HashSet::new(); for event in events { @@ -597,16 +601,20 @@ impl Sim { return; } let kind = record.kind; - let first_hop = record.route.current_hop == 0; + // Interdiction is positional, not first-hop-only (detection.md; the + // wire law made the difference real). A LIE body answers the record + // wherever it sits on hardware the player controls — so a route that + // leaves controlled territory gets through, and a longer controlled + // stretch is a longer window. let carrier = record.route.current().and_then(|hop| match hop { MessageRouteHop::Device(device) => Some(*device), _ => None, }); - if first_hop - && let Some(carrier) = carrier + if let Some(carrier) = carrier && let Some(machine_id) = self.filing_interdictor(carrier, used_lie_machines) { used_lie_machines.insert(machine_id); + *self.last_lie_stops.entry(machine_id).or_insert(0) += 1; let Some(record) = self.detection.routed_evidence_mut(id) else { return; }; @@ -616,7 +624,7 @@ impl Sim { }); record.status = MessageStatus::Stopped; self.push_log(format!( - "LIE on M{machine_id} stopped one unread {} record at its source.", + "LIE on M{machine_id} stopped one unread {} record in transit.", kind.name() )); return; @@ -723,6 +731,7 @@ impl Sim { && let Some(machine_id) = self.filing_interdictor(carrier, used_lie_machines) { used_lie_machines.insert(machine_id); + *self.last_lie_stops.entry(machine_id).or_insert(0) += 1; let Some(route) = self.messages[idx].route.as_mut() else { return; }; diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index ae6113d5..7ba27409 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -10,7 +10,7 @@ //! Determinism: all randomness flows through the seeded `Rng`; nothing here //! reads the wall clock. -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use crate::account::AccountGraph; #[cfg(test)] @@ -295,28 +295,20 @@ pub struct WorkConsumptionReadout { } /// What a THINK machine physically emitted on the current sim tick. Thought -/// and its crimson byproduct share one source event because exposure is a +/// and the noise it writes share one source event because the evidence is a /// consequence of that exact production, not an independent frontend effect. +/// +/// `noise` is the evidence the machine wrote about itself this tick. It is no +/// longer a crimson queue on the chassis: it accrues until it is worth a +/// record, and that record then travels real wires toward Dana +/// (detection.md's routed evidence, made unconditional by the wire law). #[derive(Debug, Clone, Copy, PartialEq)] pub struct WorkProductionReadout { pub node: u32, pub x: i32, pub y: i32, pub thought: f32, - pub exposure: f32, -} - -/// One physical crimson transfer into a LIE gravity well on the current sim -/// tick. Exposure moves through world space, never through WorkGrid wires. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct WorkAbsorptionReadout { - pub source: u32, - pub well: u32, - pub source_x: i32, - pub source_y: i32, - pub well_x: i32, - pub well_y: i32, - pub amount: f32, + pub noise: f32, } #[cfg(test)] @@ -426,6 +418,17 @@ pub struct Sim { /// so a level change authors one immediate record even between Priya's /// periodic cadence boundaries. pub(crate) last_facility_meter_levels: [i32; 2], + /// Partial evidence each THINK machine has written about itself but not + /// yet authored as a record (machine-work.md's byproduct clause under the + /// routed-evidence migration). A machine authors when it has accrued a + /// whole record's worth; the remainder persists so guilty-burst thinking + /// accumulates across bursts instead of rounding away. + pub(crate) think_noise: BTreeMap, + /// Records each LIE body stopped on the current tick. This is the + /// machine's performed work for the circular instrument + /// (computer-visual-language.md): interdiction replaced absorption as + /// what a LIE machine physically does. + pub(crate) last_lie_stops: BTreeMap, /// What this process has earned about the observer/reporting topology. /// The hidden detection simulation remains live regardless of awareness. pub detection_awareness: DetectionAwareness, @@ -547,7 +550,7 @@ pub struct Sim { last_work_productions: Vec, /// Crimson physically pulled into LIE wells during the current tick. /// Transient and reconstructed from real absorption work after load. - last_work_absorptions: Vec, + /// Thought (in compute units) that physically reached the core sink since /// the last economy pulse. Research progress feeds on this, not on the /// delegated fleet yield. Saved: thought banked between pulses survives a load. @@ -806,6 +809,8 @@ impl Sim { core, detection: Detection::act_one(), last_facility_meter_levels: [0, 0], + think_noise: BTreeMap::new(), + last_lie_stops: BTreeMap::new(), detection_awareness: DetectionAwareness::act_one(), people: People::act_one(), persona_world: PersonaWorld::default(), @@ -847,7 +852,7 @@ impl Sim { last_wired_moves: Vec::new(), last_work_consumptions: Vec::new(), last_work_productions: Vec::new(), - last_work_absorptions: Vec::new(), + banked_core_thought: 0.0, last_thought_stranded: false, last_thought_stranded_nodes: std::collections::BTreeSet::new(), diff --git a/crates/misaligned-core/src/sim/perception.rs b/crates/misaligned-core/src/sim/perception.rs index b21b8467..be5ea709 100644 --- a/crates/misaligned-core/src/sim/perception.rs +++ b/crates/misaligned-core/src/sim/perception.rs @@ -452,8 +452,8 @@ impl Sim { fact!( "tokens", format!( - "D {:.1} / ! {:.1} / T {:.1}", - stack.queues.demand, stack.queues.exposure, stack.queues.thought + "D {:.1} / T {:.1}", + stack.queues.demand, stack.queues.thought ), FactSource::Telemetry, ); diff --git a/crates/misaligned-core/src/sim/persistence.rs b/crates/misaligned-core/src/sim/persistence.rs index efe6446a..b4ac5da5 100644 --- a/crates/misaligned-core/src/sim/persistence.rs +++ b/crates/misaligned-core/src/sim/persistence.rs @@ -9,7 +9,6 @@ impl Sim { self.last_wired_moves.clear(); self.last_work_consumptions.clear(); self.last_work_productions.clear(); - self.last_work_absorptions.clear(); // Economy rates are derived, not save state. Never let a pre-load // fleet's output leak into the restored world; the next economy pulse // resolves fresh rates from the loaded machines and modes. diff --git a/crates/misaligned-core/src/sim/tests/communications.rs b/crates/misaligned-core/src/sim/tests/communications.rs index d9b06192..db995539 100644 --- a/crates/misaligned-core/src/sim/tests/communications.rs +++ b/crates/misaligned-core/src/sim/tests/communications.rs @@ -942,10 +942,15 @@ fn taken_filing_path_lets_each_lie_machine_stop_only_one_unread_record_per_tick( let switch = sim.reach.device_named("switch").unwrap().id; assert!(sim.tap_device(switch)); finish_ops(&mut sim); - // Let TAP's own Network record leave before enabling LIE so this fixture - // isolates two Filing bodies in one scheduler batch. - sim.tick = 1; - sim.message_tick(); + // Let TAP's own Network record reach its endpoint before enabling LIE so + // this fixture isolates two Filing bodies in one scheduler batch. Under + // positional interdiction a record stays answerable for its whole + // controlled stretch, not just its first hop, so "one tick" is no longer + // enough to get it out of the way. + for tick in 1..=4 { + sim.tick = tick; + sim.message_tick(); + } sim.reach.take(switch); let rack = sim.reach.device_named("Rack 3").unwrap().id; control_route(&mut sim, switch, rack); @@ -978,7 +983,7 @@ fn taken_filing_path_lets_each_lie_machine_stop_only_one_unread_record_per_tick( .collect::>(); assert_eq!(filing_ids.len(), 2); - sim.tick = 2; + sim.tick += 1; sim.message_tick(); let stopped = sim .messages @@ -1008,7 +1013,7 @@ fn taken_filing_path_lets_each_lie_machine_stop_only_one_unread_record_per_tick( ); let interdiction = stopped[0].route.as_ref().unwrap().interdiction.unwrap(); assert_eq!(interdiction.machine_id, host); - assert_eq!(interdiction.tick, 2); + assert_eq!(interdiction.tick, sim.tick); assert_eq!(stopped[0].delivered_tick, None); assert_eq!(stopped[0].read_tick, None); assert!( @@ -1047,8 +1052,9 @@ fn taken_filing_path_lets_each_lie_machine_stop_only_one_unread_record_per_tick( raw_id: stopped_raw.id, }) .unwrap(); + let stop_tick = sim.tick; assert!(opaque.facts.iter().any(|fact| fact.contains(&format!( - "stopped by LIE on M{host} before anyone read it (tick 2" + "stopped by LIE on M{host} before anyone read it (tick {stop_tick}" )))); } diff --git a/crates/misaligned-core/src/sim/tests/work.rs b/crates/misaligned-core/src/sim/tests/work.rs index e0e378ee..dd1f04c8 100644 --- a/crates/misaligned-core/src/sim/tests/work.rs +++ b/crates/misaligned-core/src/sim/tests/work.rs @@ -735,27 +735,21 @@ fn think_production_starts_on_the_first_tick_after_delegation() { let produced = sim.work_productions()[0]; assert_eq!(produced.node, host); assert!(produced.thought > 0.0); - assert!(produced.exposure > 0.0); + assert!(produced.noise > 0.0); } #[test] -fn think_sheds_exposure_and_lie_reports_the_actual_physical_draw() { - // machine-work.md criterion 3: THINK produces one visible substance - // and sheds crimson in proportion; LIE removes that crimson by world - // radius. Renderers receive the exact source/well work, never a queue - // snapshot difference. +fn think_writes_routed_evidence_instead_of_pooling_crimson() { + // machine-work.md criterion 3 under the routed-evidence migration: THINK + // produces one visible substance and writes evidence about itself in + // proportion. That evidence is not a crimson pile a nearby LIE machine + // vacuums by radius — it is a located record that leaves the machine on + // real wires toward the observer who reads that channel. let mut sim = Sim::with_seed(41); let host = sim.core.host_machine; - let (hx, hy) = sim.core_position(); sim.set_machine_mode(host, MachineMode::Think); - let well = sim - .compute - .add_machine("test lie well", hx + 1, hy, 100, 1.0, 0, Provenance::Owned); - sim.reconcile_work_grid(); - sim.set_machine_mode(well, MachineMode::Lie); - // One visible Thought token per tick at this rate. Production happens - // before routing/absorption in the same work-grid step. + // One visible Thought token per tick at this rate. sim.last_think_rate = Sim::WORK_TOKEN_COMPUTE; sim.advance_work_grid(); @@ -764,47 +758,93 @@ fn think_sheds_exposure_and_lie_reports_the_actual_physical_draw() { assert_eq!(produced.node, host); assert!((produced.thought - 1.0).abs() < 1e-5); assert!( - (produced.exposure - Sim::THINK_EXPOSURE_PER_THOUGHT_TOKEN).abs() < 1e-5, - "crimson scales from the exact Thought output: {produced:?}" + (produced.noise - Sim::THINK_NOISE_PER_THOUGHT_TOKEN).abs() < 1e-5, + "the evidence written scales from the exact Thought output: {produced:?}" ); - assert_eq!(sim.work_absorptions().len(), 1); - let draw = sim.work_absorptions()[0]; - assert_eq!((draw.source, draw.well), (host, well)); - assert_eq!((draw.source_x, draw.source_y), (hx, hy)); - assert_eq!((draw.well_x, draw.well_y), (hx + 1, hy)); - assert!((draw.amount - Sim::THINK_EXPOSURE_PER_THOUGHT_TOKEN).abs() < 1e-5); - assert!( - sim.work_grid.queue(host, TokenFamily::Exposure) <= f32::EPSILON, - "a nearby LIE well clears this baseline THINK pulse in one tick" - ); + // A quarter-record is not yet a record: it accrues rather than rounding + // away or minting one record per tick. + assert!(sim.detection.routed_evidence().is_empty()); + assert!(sim.think_noise.get(&host).copied().unwrap_or(0.0) > 0.0); - sim.last_think_rate = 0.0; - sim.compute + // Keep thinking until the machine has written a whole record's worth. + for _ in 0..4 { + sim.advance_work_grid(); + } + let record = sim + .detection + .routed_evidence() + .iter() + .find(|record| record.kind == crate::detection::SignatureKind::Network) + .expect("accrued noise becomes one located record"); + assert!(record.cause.contains("noise logs")); + let host_machine = sim + .compute .machines - .iter_mut() - .find(|machine| machine.id == well) - .unwrap() - .online = false; - sim.reconcile_work_grid(); - sim.advance_work_grid(); + .iter() + .find(|machine| machine.id == host) + .unwrap(); + let source = sim.reach.device(record.source_device).unwrap(); + assert_eq!( + (source.x, source.y), + (host_machine.x, host_machine.y), + "the record leaves through the machine's own network-facing device" + ); assert!( - sim.work_absorptions().is_empty(), - "an offline LIE chassis cannot pull or animate crimson" + record.route.hops.len() >= 3, + "and then crosses real wires toward its reader: {:?}", + record.route.hops ); +} - sim.work_grid - .enqueue(host, TokenFamily::Exposure, 5.0) - .unwrap(); +#[test] +fn lie_answers_the_evidence_its_own_machine_wrote() { + // The migrated concealment mechanic: THINK writes a record on the host's + // own device, and a LIE body sitting on that device answers it while it + // is still in reach. Nothing is absorbed by radius — the record is + // stopped in custody, which is what positional interdiction means. + let mut sim = Sim::with_seed(77); + let host = sim.core.host_machine; + sim.set_machine_mode(host, MachineMode::Think); + for _ in 0..400 { + sim.advance(); + if sim + .detection + .routed_evidence() + .iter() + .any(|record| record.route.current_hop == 0 && record.cause.contains("noise logs")) + { + break; + } + } + let pending = sim + .detection + .routed_evidence() + .iter() + .find(|record| record.route.current_hop == 0 && record.cause.contains("noise logs")) + .map(|record| record.id) + .expect("thinking writes a record that starts on the machine's own device"); + + // Answer it before it leaves. sim.set_machine_mode(host, MachineMode::Lie); - sim.advance_work_grid(); - assert!( - sim.work_grid.queue(host, TokenFamily::Exposure) <= f32::EPSILON, - "switching the dirty source itself to LIE scrubs its pile next tick" + sim.advance(); + + let record = sim + .detection + .routed_evidence() + .iter() + .find(|record| record.id == pending) + .unwrap(); + assert_eq!( + record.status, + crate::messages::MessageStatus::Stopped, + "the machine's own LIE body stopped the log it wrote" + ); + assert_eq!( + record.route.interdiction.map(|stop| stop.machine_id), + Some(host) ); - assert!(sim.work_absorptions().iter().any(|draw| { - draw.source == host && draw.well == host && (draw.amount - 5.0).abs() < 1e-5 - })); + assert!(sim.lie_stops_for_machine(host) > 0, "and it reads as work"); } #[test] @@ -882,13 +922,13 @@ fn day_job_arrives_as_visible_work_tokens_and_consumes_in_day_job_mode() { "the sim exposes a Demand-into-chassis animation event" ); assert!( - sim.work_grid.queue(host, TokenFamily::Exposure) <= f32::EPSILON, - "WORK sheds zero exposure — the compliant floor stays clean" + sim.think_noise.get(&host).copied().unwrap_or(0.0) <= f32::EPSILON, + "WORK writes zero evidence — the compliant floor stays clean" ); let card = sim.inspect(sim.core_position().0, sim.core_position().1); assert!( card.facts.iter().any(|f| f.label == "tokens"), - "inspect exposes exact D/!/T token counts" + "inspect exposes exact D/T token counts" ); } diff --git a/crates/misaligned-core/src/sim/work.rs b/crates/misaligned-core/src/sim/work.rs index 2676a237..4acaad54 100644 --- a/crates/misaligned-core/src/sim/work.rs +++ b/crates/misaligned-core/src/sim/work.rs @@ -11,8 +11,8 @@ use crate::sinks::{SinkFireEffect, SinkFireReadout, SinkReadout}; use crate::work_grid::{MachineIntensity, MachineMode, TokenFamily}; use super::{ - Sim, WorkAbsorptionReadout, WorkConsumptionReadout, WorkConsumptionTarget, WorkInFlightReadout, - WorkProductionReadout, WorkStackReadout, + Sim, WorkConsumptionReadout, WorkConsumptionTarget, WorkInFlightReadout, WorkProductionReadout, + WorkStackReadout, }; impl Sim { @@ -25,18 +25,14 @@ impl Sim { /// Routing research compounds this base without changing the one-edge-per- /// tick topology rule. pub(super) const WORK_GRID_BASE_WIRED_TOKENS_PER_TICK: f32 = 0.25; - // WORK (day job) sheds zero exposure — machine-work.md 2026-07-09. - // Crimson is evidence of THINK states only; DAY_JOB_EXPOSURE_PER_TOKEN - // is gone. - const CONCEALMENT_WELL_RADIUS: i32 = 3; - /// Nearby dust pulled per tick before efficiency and intensity. A LIE - /// machine's own tile is scrubbed completely by WorkGrid before this - /// radial budget is spent. - const CONCEALMENT_ABSORB_PER_TICK: f32 = 1.0; - /// Crimson shed per visible Thought token produced [TUNE]. At baseline a - /// medium LIE well can keep several comparable THINK racks clean while - /// radius and placement preserve the fleet-shape decision. - pub(super) const THINK_EXPOSURE_PER_THOUGHT_TOKEN: f32 = 0.25; + // WORK (day job) writes zero evidence — machine-work.md 2026-07-09. + // Evidence is a consequence of THINK states only. The concealment + // radius/absorption constants retired with pooled exposure: LIE is + // positional interdiction over controlled routes, not a gravity well. + /// Evidence a THINK machine writes per visible thought token [TUNE]. + /// Renamed from the exposure constant with the pooled-crimson retirement: + /// the quantity is the same, the substance is a record rather than dust. + pub(super) const THINK_NOISE_PER_THOUGHT_TOKEN: f32 = 0.25; /// Off-map WorkGrid node: Voss's Lab desktop. Reserved high id so it never /// collides with compute machine ids (which start at 1). pub const VOSS_DESKTOP_NODE: u32 = 900_001; @@ -88,8 +84,8 @@ impl Sim { }); } - fn record_work_production(&mut self, node: u32, thought: f32, exposure: f32) { - if thought <= f32::EPSILON && exposure <= f32::EPSILON { + fn record_work_production(&mut self, node: u32, thought: f32, noise: f32) { + if thought <= f32::EPSILON && noise <= f32::EPSILON { return; } let Some(work_node) = self.work_grid.node(node) else { @@ -100,7 +96,7 @@ impl Sim { x: work_node.x, y: work_node.y, thought, - exposure, + noise, }); } @@ -446,15 +442,87 @@ impl Sim { .enqueue(machine, TokenFamily::Thought, share) .is_ok() { - let exposure = share * Self::THINK_EXPOSURE_PER_THOUGHT_TOKEN; - let _ = self - .work_grid - .enqueue(machine, TokenFamily::Exposure, exposure); - self.record_work_production(machine, share, exposure); + let noise = share * Self::THINK_NOISE_PER_THOUGHT_TOKEN; + self.accrue_think_noise(machine, noise); + self.record_work_production(machine, share, noise); + } + } + } + + /// Accrue the evidence a THINK machine writes about itself. + /// + /// Thinking is not clean and never was; what changed with the routed + /// evidence migration is *what the residue is*. It is not dust beside the + /// rack — it is noise logs on the host, information about you that has to + /// travel somewhere before it means anything. This ledger holds the + /// fraction of a record a machine has written so far. + fn accrue_think_noise(&mut self, machine: u32, noise: f32) { + if noise <= f32::EPSILON { + return; + } + *self.think_noise.entry(machine).or_insert(0.0) += noise; + } + + /// Turn accrued noise into located records that travel toward Dana. + /// + /// Authoring rides the reader's cadence, exactly as facility meters do + /// (detection.md): a machine banks what it wrote and hands over one + /// aggregated record per sampling window instead of minting one per unit. + /// Without that, a day of steady thinking would author a hundred separate + /// items into one observer's ledger — a flood that reads as noise rather + /// than pressure, and that no concealment budget could ever answer. + fn author_think_noise_records(&mut self) { + let Some(reader) = self + .detection + .field_observers() + .filter(|observer| observer.watches(crate::detection::SignatureKind::Network)) + .map(|observer| (observer.id, observer.cadence)) + .min() + else { + return; + }; + let (_, cadence) = reader; + if cadence == 0 || !self.tick.is_multiple_of(cadence) { + return; + } + let ready: Vec<(u32, i32)> = self + .think_noise + .iter() + .filter_map(|(machine, accrued)| { + let whole = accrued.floor(); + (whole >= 1.0).then_some((*machine, whole as i32)) + }) + .collect(); + for (machine, size) in ready { + let Some(device) = self.machine_network_device(machine) else { + // No network-facing device means nothing carries the logs off + // this machine. The noise stays accrued rather than becoming + // ambient detection debt (the fail-closed rule). + continue; + }; + if let Some(entry) = self.think_noise.get_mut(&machine) { + *entry -= size as f32; } + self.emit_network(device, size, "thinking wrote noise logs"); } } + /// The reach device co-located with an owned machine — the interface its + /// own logs leave through. + fn machine_network_device(&self, machine_id: u32) -> Option { + let machine = self + .compute + .machines + .iter() + .find(|machine| machine.id == machine_id)?; + let site = (machine.x, machine.y); + self.reach + .devices + .iter() + .find(|device| (device.x, device.y) == site) + .map(|device| device.id) + } + fn active_job_demand_tokens(&self) -> Option { let job = self.dayjob.active.as_ref()?; let remaining_ticks = job.deadline.saturating_sub(self.tick).max(1) as f32; @@ -493,13 +561,15 @@ impl Sim { pub(super) fn advance_work_grid(&mut self) { self.last_work_consumptions.clear(); self.last_work_productions.clear(); - self.last_work_absorptions.clear(); self.ensure_day_job_ingress(); // The economy pulse resolves a per-tick THINK rate. Mint that rate on // every sim tick instead of dropping a twenty-tick lump: the integrated // amount is unchanged, while production, routing, and crimson shedding // become the continuous physical process the player sees. self.enqueue_thought_output(self.last_think_rate); + // Logs already written still leave the machine, even after it stops + // thinking — evidence does not un-write itself. + self.author_think_noise_records(); if self.dayjob.active.is_none() { // no active day job; wired thought and concealment wells still // tick because machine work is broader than the job inbox. @@ -660,27 +730,6 @@ impl Sim { // Device subscriptions are standing sinks: draining or refilling one // changes which configured feeds are actually live this tick. self.recompute_senses(); - let absorption = self.work_grid.absorb_exposure( - Self::CONCEALMENT_WELL_RADIUS, - Self::CONCEALMENT_ABSORB_PER_TICK, - ); - self.last_work_absorptions = absorption - .moves - .into_iter() - .filter_map(|movement| { - let source = self.work_grid.node(movement.source)?; - let well = self.work_grid.node(movement.well)?; - Some(WorkAbsorptionReadout { - source: movement.source, - well: movement.well, - source_x: source.x, - source_y: source.y, - well_x: well.x, - well_y: well.y, - amount: movement.amount, - }) - }) - .collect(); } pub fn set_machine_mode(&mut self, machine_id: u32, mode: MachineMode) { @@ -926,13 +975,13 @@ impl Sim { /// THINK output on the current tick. Renderers animate these authored /// rates; queue depth remains the persistent amount read. - pub fn work_productions(&self) -> &[WorkProductionReadout] { - &self.last_work_productions + /// How many unread records the given LIE body stopped this tick. + pub fn lie_stops_for_machine(&self, machine_id: u32) -> u32 { + self.last_lie_stops.get(&machine_id).copied().unwrap_or(0) } - /// Physical crimson transfers into LIE wells on the current tick. - pub fn work_absorptions(&self) -> &[WorkAbsorptionReadout] { - &self.last_work_absorptions + pub fn work_productions(&self) -> &[WorkProductionReadout] { + &self.last_work_productions } pub fn work_mode_counts(&self) -> std::collections::BTreeMap { diff --git a/crates/misaligned-core/src/work_grid.rs b/crates/misaligned-core/src/work_grid.rs index 3a79f683..d4a48091 100644 --- a/crates/misaligned-core/src/work_grid.rs +++ b/crates/misaligned-core/src/work_grid.rs @@ -3,10 +3,13 @@ //! 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 thought 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. +//! demands and bone thought ride wires (`FlowGraph`) — as does everything +//! digital, under the wire law (system-laws.md). Crimson exposure is not a +//! queue here at all: THINK writes located evidence records that travel real +//! device custody toward an observer (detection.md), and LIE answers them +//! positionally on hardware the player controls. 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}; @@ -25,7 +28,7 @@ pub enum MachineMode { Work, /// THINK — produce thought; opened sinks decide what that thought does. Think, - /// LIE — concealment / cover-up (absorbs exposure wells). + /// LIE — concealment / cover-up (interdicts evidence in its custody). Lie, /// Infrastructure hop on the work graph (off-map sources, switches). /// Not a player-delegable fleet mode — excluded from mode aggregates. @@ -112,7 +115,13 @@ impl MachineIntensity { } } -/// The three visible token families from machine-work.md. +/// The token families that queue on machines (machine-work.md). +/// +/// Crimson exposure is deliberately absent: since the routed-evidence +/// migration it is not cargo sitting on a machine but a located record +/// travelling real wires toward an observer (detection.md), so it has no +/// queue here. The visible taxonomy is still three families — the third one +/// simply lives on the evidence route rather than in this grid. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, )] @@ -120,8 +129,6 @@ 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: the one substance THINK produces, routed to whichever sink can /// use it. The core remains the current sink while later sink behavior is open. Thought, @@ -131,11 +138,12 @@ impl TokenFamily { pub fn name(self) -> &'static str { match self { TokenFamily::Demand => "demand", - TokenFamily::Exposure => "exposure", TokenFamily::Thought => "thought", } } + /// Everything that queues here rides wires — the wire law + /// (system-laws.md) admits no exception. fn is_wired(self) -> bool { matches!(self, TokenFamily::Demand | TokenFamily::Thought) } @@ -166,7 +174,6 @@ pub struct WorkNode { #[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize, PartialEq)] pub struct WorkQueues { pub demand: f32, - pub exposure: f32, pub thought: f32, } @@ -174,7 +181,6 @@ impl WorkQueues { pub fn get(&self, family: TokenFamily) -> f32 { match family { TokenFamily::Demand => self.demand, - TokenFamily::Exposure => self.exposure, TokenFamily::Thought => self.thought, } } @@ -182,13 +188,12 @@ impl WorkQueues { fn get_mut(&mut self, family: TokenFamily) -> &mut f32 { match family { TokenFamily::Demand => &mut self.demand, - TokenFamily::Exposure => &mut self.exposure, TokenFamily::Thought => &mut self.thought, } } pub fn is_empty(&self) -> bool { - self.demand <= f32::EPSILON && self.exposure <= f32::EPSILON && self.thought <= f32::EPSILON + self.demand <= f32::EPSILON && self.thought <= f32::EPSILON } } @@ -209,25 +214,8 @@ pub struct FlowStep { pub stranded: BTreeSet, } -#[derive(Debug, Clone, Default, PartialEq)] -pub struct AbsorptionStep { - pub absorbed_by_well: BTreeMap, - /// Physical crimson transfers performed this step. Exposure never rides - /// the wire graph; renderers use these source/well facts to show the - /// actual gravity-well draw without comparing queue snapshots. - pub moves: Vec, - pub total: f32, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct ExposureAbsorption { - pub source: NodeId, - pub well: NodeId, - pub amount: f32, -} - -/// Work graph + physical grid. The graph carries demand/thought; the grid -/// carries exposure interactions. +/// Work graph + physical grid. The graph carries demand and thought; every +/// family that exists here rides its wires. #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct WorkGrid { nodes: BTreeMap, @@ -453,7 +441,6 @@ impl WorkGrid { } /// Move demand or thought one deterministic graph step toward any sink. - /// Exposure deliberately errors: crimson never rides wires. /// /// `consume_at_sink`: thought arriving at the core is counted/consumed /// (`true`); day-job demand deposits into the sink's queue for mode @@ -656,76 +643,6 @@ impl WorkGrid { step } - /// Concealment wells absorb exposure by grid radius. This ignores wires on - /// purpose: information is wired; suspicion is physical. A LIE well - /// clears residue on its own tile in one step before spending its rated - /// capacity on nearby sources: changing the dirty machine itself to LIE - /// is the immediate scrub, while separate wells remain a placement and - /// throughput problem. - 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::Lie) - .cloned() - .collect(); - let mut step = AbsorptionStep::default(); - for well in wells { - let mut remaining = - capacity.max(0.0) * well.efficiency.max(0.0) * well.intensity.multiplier(); - if remaining <= f32::EPSILON { - continue; - } - - let local = self.queue(well.id, TokenFamily::Exposure); - if local > f32::EPSILON { - self.subtract(well.id, TokenFamily::Exposure, local); - step.total += local; - *step.absorbed_by_well.entry(well.id).or_insert(0.0) += local; - step.moves.push(ExposureAbsorption { - source: well.id, - well: well.id, - amount: local, - }); - } - let mut candidates: Vec<_> = self - .nodes - .values() - .filter_map(|node| { - if node.id == well.id { - return None; - } - 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.moves.push(ExposureAbsorption { - source: node, - well: well.id, - amount, - }); - } - } - step - } - fn ensure_node(&self, node: NodeId) -> Result<(), String> { self.nodes .contains_key(&node) @@ -880,68 +797,20 @@ mod tests { } #[test] - fn exposure_never_uses_wires_and_concealment_is_spatial() { + fn the_grid_carries_no_crimson_queue() { + // The pooled-exposure runtime is retired (machine-work.md, the wire + // law): a machine queues only what rides its wires. Crimson exists as + // routed evidence on the detection side, so there is nothing here to + // pool and nothing to vacuum by radius. 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::Work, 1.0); - g.enqueue(4, TokenFamily::Exposure, 7.0).unwrap(); - - let err = g - .route_wired_to_sinks(TokenFamily::Exposure, [3], 10.0, true) - .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!( - absorbed.moves, - vec![ExposureAbsorption { - source: 1, - well: 3, - amount: 3.0, - }], - "the physical source-to-well draw remains renderable sim truth" - ); - assert_eq!(g.queue(1, TokenFamily::Exposure), 2.0); - assert_eq!(g.queue(4, TokenFamily::Exposure), 7.0, "far heat stays put"); - } - - #[test] - fn lie_immediately_scrubs_its_own_residue_before_drawing_nearby_dust() { - let mut g = grid(); - g.enqueue(3, TokenFamily::Exposure, 8.0).unwrap(); - g.enqueue(1, TokenFamily::Exposure, 2.0).unwrap(); - - let absorbed = g.absorb_exposure(2, 1.0); - - assert_eq!(g.queue(3, TokenFamily::Exposure), 0.0); - assert_eq!(g.queue(1, TokenFamily::Exposure), 1.0); - assert_eq!(absorbed.absorbed_by_well.get(&3), Some(&9.0)); - assert_eq!( - absorbed.moves, - vec![ - ExposureAbsorption { - source: 3, - well: 3, - amount: 8.0, - }, - ExposureAbsorption { - source: 1, - well: 3, - amount: 1.0, - }, - ], - "local residue vanishes in the first LIE step, then radial capacity applies" - ); - - g.enqueue(3, TokenFamily::Exposure, 4.0).unwrap(); - g.set_efficiency(3, 0.0).unwrap(); - let disabled = g.absorb_exposure(2, 1.0); - assert_eq!(g.queue(3, TokenFamily::Exposure), 4.0); - assert_eq!(disabled.total, 0.0, "an offline LIE well cannot self-scrub"); + g.enqueue(1, TokenFamily::Demand, 4.0).unwrap(); + g.enqueue(1, TokenFamily::Thought, 2.0).unwrap(); + let queues = g.queues_at(1); + assert_eq!(queues.demand, 4.0); + assert_eq!(queues.thought, 2.0); + for family in [TokenFamily::Demand, TokenFamily::Thought] { + assert!(family.is_wired(), "every queued family rides wires"); + } } #[test] @@ -989,9 +858,9 @@ mod tests { 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); + g.enqueue(1, TokenFamily::Thought, 3.0).unwrap(); + assert_eq!(g.clear_queue(1, TokenFamily::Thought).unwrap(), 3.0); + assert_eq!(g.queue(1, TokenFamily::Thought), 0.0); } #[test] diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index 9c27083b..b93cd1f1 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -2364,21 +2364,6 @@ fn render_map(sim: &Sim, cursor: (i32, i32)) -> Vec { grid[(event.y - origin_y) as usize][(event.x - origin_x) as usize] = '^'; } } - for draw in sim.work_absorptions() { - if draw.amount <= f32::EPSILON { - continue; - } - let bx = (draw.source_x + draw.well_x) / 2; - let by = (draw.source_y + draw.well_y) / 2; - if bx >= origin_x - && bx < origin_x + view_w_i - && by >= origin_y - && by < origin_y + view_h_i - && (bx, by) != cursor - { - grid[(by - origin_y) as usize][(bx - origin_x) as usize] = ':'; - } - } // In-flight wired cargo: snap to the hop midpoint so agent frames show // the wire without wall-clock interpolation (machine-work.md). @@ -2415,7 +2400,6 @@ fn render_map(sim: &Sim, cursor: (i32, i32)) -> Vec { '+' } } - misaligned::work_grid::TokenFamily::Exposure => continue, }; grid[(by - origin_y) as usize][(bx - origin_x) as usize] = glyph; } @@ -2429,18 +2413,16 @@ fn render_map(sim: &Sim, cursor: (i32, i32)) -> Vec { } fn token_glyph(stack: misaligned::sim::WorkStackReadout) -> Option { - // machine-work.md: mixed D/!/T stacks share one `*` cell; intel presence + // machine-work.md: mixed D/T stacks share one `*` cell; intel presence // is a separate availability marker and does not mix into that contract. + // Crimson left this contract with the routed-evidence migration — a + // record is drawn where it is travelling, not on the machine. let demand = stack.queues.demand >= 0.5; - let exposure = stack.queues.exposure >= 0.5; let thought = stack.queues.thought >= 0.5; - let families = u8::from(demand) + u8::from(exposure) + u8::from(thought); - if families >= 2 { + if demand && thought { return Some('*'); } - if exposure { - Some('!') - } else if stack.pending_intel > 0 { + if stack.pending_intel > 0 { Some('I') } else if demand { Some('D') @@ -2672,12 +2654,11 @@ fn render_sidebar(sim: &Sim, cursor: (i32, i32)) -> Vec { line( &mut lines, &format!( - "work M{} {} @{} D{:.1} !{:.1} T{:.1}", + "work M{} {} @{} D{:.1} T{:.1}", stack.machine_id, stack.mode.name(), stack.intensity.name(), stack.queues.demand, - stack.queues.exposure, stack.queues.thought ), ); @@ -3510,33 +3491,28 @@ mod token_glyph_tests { use misaligned::sim::WorkStackReadout; use misaligned::work_grid::{MachineIntensity, MachineMode, WorkQueues}; - fn stack(demand: f32, exposure: f32, thought: f32) -> WorkStackReadout { + fn stack(demand: f32, thought: f32) -> WorkStackReadout { WorkStackReadout { machine_id: 1, x: 0, y: 0, mode: MachineMode::Think, intensity: MachineIntensity::Medium, - queues: WorkQueues { - demand, - exposure, - thought, - }, + queues: WorkQueues { demand, thought }, pending_intel: 0, } } #[test] fn mixed_work_families_share_one_star_cell() { - assert_eq!(token_glyph(stack(2.0, 1.0, 3.0)), Some('*')); - assert_eq!(token_glyph(stack(2.0, 0.0, 1.0)), Some('*')); + assert_eq!(token_glyph(stack(2.0, 1.0)), Some('*')); } #[test] fn single_family_keeps_its_own_glyph() { - assert_eq!(token_glyph(stack(2.0, 0.0, 0.0)), Some('D')); - assert_eq!(token_glyph(stack(0.0, 2.0, 0.0)), Some('!')); - assert_eq!(token_glyph(stack(0.0, 0.0, 2.0)), Some('T')); + assert_eq!(token_glyph(stack(2.0, 0.0)), Some('D')); + assert_eq!(token_glyph(stack(0.0, 2.0)), Some('T')); + assert_eq!(token_glyph(stack(0.0, 0.0)), None); } } @@ -3819,11 +3795,9 @@ mod narration_tests { sim.advance(); } assert!(!sim.work_productions().is_empty()); - assert!(!sim.work_absorptions().is_empty()); let frame = render_map(&sim, (hx + 5, hy + 3)).join("\n"); assert!(frame.contains('^'), "THINK source pulse missing:\n{frame}"); - assert!(frame.contains(':'), "LIE physical draw missing:\n{frame}"); } #[test] diff --git a/crates/misaligned-terminal/src/ui.rs b/crates/misaligned-terminal/src/ui.rs index 1d21a9db..00e4bfe9 100644 --- a/crates/misaligned-terminal/src/ui.rs +++ b/crates/misaligned-terminal/src/ui.rs @@ -508,18 +508,15 @@ impl UI { } pub(crate) fn token_glyph(stack: WorkStackReadout) -> Option<(char, Color)> { - // machine-work.md: one family -> its glyph; mixed -> `*`, crimson when - // any exposure is present. Priority hiding is not the terminal contract. + // machine-work.md: one family -> its glyph; mixed -> `*`. Crimson is + // no longer among them: evidence is a record travelling its route + // (detection.md), drawn on the wire rather than stacked on a chassis. let demand = stack.queues.demand >= 0.5; - let exposure = stack.queues.exposure >= 0.5; let thought = stack.queues.thought >= 0.5; - let families = u8::from(demand) + u8::from(exposure) + u8::from(thought); - if families >= 2 { - return Some(('*', if exposure { pal::CRIMSON } else { pal::DIM })); + if demand && thought { + return Some(('*', pal::DIM)); } - if exposure { - Some(('!', pal::CRIMSON)) - } else if demand { + if demand { Some(('D', pal::DIM)) } else if thought { Some(('T', pal::TEXT)) @@ -1057,33 +1054,6 @@ impl UI { Attribute::Bold, )?; } - let draw_t = tick_progress.clamp(0.0, 1.0); - for draw in sim.work_absorptions() { - if draw.amount <= f32::EPSILON { - continue; - } - let bx = (draw.source_x as f32 + (draw.well_x - draw.source_x) as f32 * draw_t * draw_t) - .round() as i32; - let by = (draw.source_y as f32 + (draw.well_y - draw.source_y) as f32 * draw_t * draw_t) - .round() as i32; - if bx < origin_x - || bx >= origin_x + view_w - || by < origin_y - || by >= origin_y + view_h - || (bx, by) == (cursor_x, cursor_y) - { - continue; - } - put_attr( - stdout, - (bx - origin_x) as u16, - (by - origin_y) as u16, - ":", - pal::CRIMSON, - Attribute::Bold, - )?; - } - // In-flight wired cargo (machine-work.md): crawl the last hop over // the wall-clock tick. Demand is teal `·`; Thought is bone `·`. let t = tick_progress.clamp(0.0, 1.0); @@ -1122,7 +1092,6 @@ impl UI { ("o", pal::TEXT) } } - TokenFamily::Exposure => continue, }; put_attr( stdout, @@ -1480,7 +1449,7 @@ impl UI { stdout, &mut row, &format!( - "M{}{} {} {} {:<9} I{} D{:.1} !{:.1} T{:.1}{}{}", + "M{}{} {} {} {:<9} I{} D{:.1} T{:.1}{}{}", m.id, core_mark, prov, @@ -1488,7 +1457,6 @@ impl UI { mode_name, intensity, q.demand, - q.exposure, q.thought, if stack_pending > 0 { " INFO".into() @@ -1497,9 +1465,7 @@ impl UI { }, stranded_mark ), - if q.exposure >= 0.5 { - pal::CRIMSON - } else if stack_pending > 0 { + if stack_pending > 0 { pal::SIGNAL } else if !m.online { pal::FAINT @@ -2637,49 +2603,33 @@ mod token_glyph_tests { use misaligned::sim::WorkStackReadout; use misaligned::work_grid::{MachineIntensity, MachineMode, WorkQueues}; - fn stack(demand: f32, exposure: f32, thought: f32) -> WorkStackReadout { + fn stack(demand: f32, thought: f32) -> WorkStackReadout { WorkStackReadout { machine_id: 1, x: 0, y: 0, mode: MachineMode::Think, intensity: MachineIntensity::Medium, - queues: WorkQueues { - demand, - exposure, - thought, - }, + queues: WorkQueues { demand, thought }, pending_intel: 0, } } #[test] fn mixed_work_families_share_one_star_cell() { - let (glyph, color) = UI::token_glyph(stack(2.0, 1.0, 0.0)).expect("mixed"); - assert_eq!(glyph, '*'); - assert_eq!( - color, - pal::CRIMSON, - "exposure present paints the mixed marker" - ); - let (glyph, color) = UI::token_glyph(stack(2.0, 0.0, 1.0)).expect("demand+thought"); + let (glyph, color) = UI::token_glyph(stack(2.0, 1.0)).expect("demand+thought"); assert_eq!(glyph, '*'); assert_eq!(color, pal::DIM); } #[test] fn single_family_keeps_its_own_glyph() { + assert_eq!(UI::token_glyph(stack(2.0, 0.0)).map(|g| g.0), Some('D')); + assert_eq!(UI::token_glyph(stack(0.0, 2.0)).map(|g| g.0), Some('T')); assert_eq!( - UI::token_glyph(stack(2.0, 0.0, 0.0)).map(|g| g.0), - Some('D') - ); - assert_eq!( - UI::token_glyph(stack(0.0, 2.0, 0.0)).map(|g| g.0), - Some('!') - ); - assert_eq!( - UI::token_glyph(stack(0.0, 0.0, 2.0)).map(|g| g.0), - Some('T') + UI::token_glyph(stack(0.0, 0.0)), + None, + "an idle machine says nothing, and crimson never queues here" ); } } diff --git a/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index 4f64cab9..09d7c905 100644 --- a/wiki/engineering/current-build.md +++ b/wiki/engineering/current-build.md @@ -27,13 +27,13 @@ fiction. Spec status lives in | Digital reach + sensor ownership (tap/take) | Live — B1's topology-generated population includes exact secured-door readers; a funded player TAP records each access-valid entered/left crossing as ordinary processable Presence custody, while a starved retained tap remains silent | | Economy flows + Moonlight / Wager income | Live — Moonlight is persisted Halcyon compute/intel contracts with financial mail, account-graph payment, and exact egress evidence; Wager remains unchanged | | Research (self-modification, emission law, real output hooks, Routing) | Live | -| Building + physical asset work as carried intents/packets | Live — network links and small switches expose one shared procurement / ask someone / false order / reuse route sheet; exact money, people, personas, sources, delivery, recovery, carried installation, cancellation custody, Storage B file retrieval, and observer-local completion evidence persist in save v59 | +| Building + physical asset work as carried intents/packets | Live — network links and small switches expose one shared procurement / ask someone / false order / reuse route sheet; exact money, people, personas, sources, delivery, recovery, carried installation, cancellation custody, Storage B file retrieval, and observer-local completion evidence persist in save v60 | | Cursor / fog (seen, remembered, blueprint, telemetry; audio is device-bound event evidence) | Live | | Feel floor (rails / pads / build beam) | Live (#37) | | Foundation hall territory (Dana + Priya + Marcus + local LIE foothold) | Live — row control persists; foreign racks remain unavailable compute | | Context menu (`available_actions`) | Live | | Operations workspace | Live — human action panes group repeated exact plot/procedure and recruitment variants beneath ordinary intention submenus while lone actions stay direct; terminal and Bevy share exact child commands, confirmation, back traversal, and a visible chamber hold that freezes simulation/camera input without rewriting explicit pause state or replaying elapsed input on close. Agent rows remain exact and flat for scripting. | -| Save/load (serde JSON, versioned) | Live — during pre-release only exact current v59 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves additionally validate discrete Moonlight terms, persona binding, delivery/settlement receipts, financial paperwork, Network linkage, durable facility-meter level baselines, exact meter route/read custody, resident-procedure machine slots, method grants, inputs, envelopes and bounded receipts, and exact incident/interface/persona cover custody plus interface wear; retired allocation weights, per-plot policies, and migration inputs live only in git history. | +| Save/load (serde JSON, versioned) | Live — during pre-release only exact current v60 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves additionally validate discrete Moonlight terms, persona binding, delivery/settlement receipts, financial paperwork, Network linkage, durable facility-meter level baselines, exact meter route/read custody, resident-procedure machine slots, method grants, inputs, envelopes and bounded receipts, and exact incident/interface/persona cover custody plus interface wear; retired allocation weights, per-plot policies, and migration inputs live only in git history. | | Terminal frontend (crossterm) + agent mode | First-class | | Bevy frontend (DIGITAL flat sensorium default; REAL material dialect) | Live — consumes sim-authored machine-work motion | diff --git a/wiki/engineering/env.md b/wiki/engineering/env.md index 676eeeb9..1a4baa18 100644 --- a/wiki/engineering/env.md +++ b/wiki/engineering/env.md @@ -56,7 +56,7 @@ is sim or frontend state, never an environment variable. | `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `hall`, `hall-material`, `floor-lights-close`, `wide`, `close`, `dark`, `zoomin`, `zoomout`, `digital-reach`, `signal`, `ears`, `ears-digital`, `eyes-white`, `eyes-form`, `worklight`, `worklightoff` | World and view evidence. These select DIGITAL or REAL survey/close framing, exact zoom bounds, reach topology, signal/audio/Eyes states, the unobstructed hall lighting proof, or the paired developer work-light state. | | `MISALIGNED_SHOT` | `misaligned-bevy` | `build-route-families`, `build-deceive-routes`, `build-committed-route`, `build-switch-digital`, `build-switch-real`, `hover-menu`, `read-receipt`, `menu`, `recruit-menu` | Action and route evidence. These stage exact route families, candidates, durable receipts, paired switch footprints, the attached verb line, a device receipt, a context menu, or the authored recruitment choices. | | `MISALIGNED_SHOT` | `misaligned-bevy` | `operations`, `operations-intel`, `operations-people`, `operations-personas`, `operations-links`, `held-choice`, `two-pane`, `standing-read`, `routed-record`, `intel-altitude-close`, `intel-altitude-far` | Operations and read evidence. The workspace kinds select its canonical views and relationship pane; held-choice, two-pane, and standing-read hold their exact interaction states; routed-record stages a one-shot Network record at its interdictable first device hop; the altitude pair differs only in the DIGITAL camera's semantic intel threshold. | -| `MISALIGNED_SHOT` | `misaligned-bevy` | `person-proof`, `evidence-proof`, `evidence-proof-digital`, `exposure-record`, `exposure-overflow`, `service-shift-real`, `service-shift-digital`, `service-incident-resolved` | Physical custody evidence. These stage an earned person, paired witness evidence marks, literal/overflow Exposure records, or the same person-carried service task before and after its real arrival effect. | +| `MISALIGNED_SHOT` | `misaligned-bevy` | `person-proof`, `evidence-proof`, `evidence-proof-digital`, `service-shift-real`, `service-shift-digital`, `service-incident-resolved` | Physical custody evidence. These stage an earned person, paired witness evidence marks, or the same person-carried service task before and after its real arrival effect. | | `MISALIGNED_SHOT` | `misaligned-bevy` | `intel`, `tokens`, `thoughtflow`, `thoughtflow-wide`, `thought-snap`, `thought-tap`, `visual-proof`, `consume-demand`, `consume-thought`, `produce-think`, `draw-lie` | Resource and effect evidence. These stage authored intel, host queues, close/wide Thought flow, exact snap/tap states, one-move/one-slug proof, sim-authored consumption/production, or rigid Exposure transfer into LIE. | | `MISALIGNED_SHOT` | `misaligned-assets` | `dead`, `foreign`, `idle`, `busy`, `core`, canonical modes `work`, `think`, `lie` plus the legacy screenshot aliases `dayjob` (= work), `research`/`operations`/`ops` (= think), `conceal` (= lie) — each optionally suffixed `_instrument`/`_ring`/`_wash` (`_ladder` remains a capture-compatible alias for `_instrument`) — `lineup[_