diff --git a/CLAUDE.md b/CLAUDE.md index c62fa6b3..d1f2aa44 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 v60; + machine modes, not current player assignments. Save format is currently v61; 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-bevy/src/shot_harness.rs b/crates/misaligned-bevy/src/shot_harness.rs index a1aad8a3..ca0a0150 100644 --- a/crates/misaligned-bevy/src/shot_harness.rs +++ b/crates/misaligned-bevy/src/shot_harness.rs @@ -332,7 +332,9 @@ pub(super) fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &s obs.suspicion = 20.0; } } - game.sim.detection.set_pending(vec![Signature { + // Standing emissions are measured per tick now that the ambient + // pool is retired (2026-07-29); the shot stages one directly. + game.sim.set_standing_for_shot(vec![Signature { kind: SignatureKind::Network, size: 4, standing: true, diff --git a/crates/misaligned-core/src/detection.rs b/crates/misaligned-core/src/detection.rs index 15f744a3..7c2b7e42 100644 --- a/crates/misaligned-core/src/detection.rs +++ b/crates/misaligned-core/src/detection.rs @@ -434,7 +434,6 @@ pub struct Detection { /// truth; player-facing projections must filter it through /// `DetectionAwareness`. pub observers: Vec, - pending: Vec, /// Exact one-shot digital records that have left a source device. They do /// not enter the global pending pool and cannot be scrubbed from afar. routed_evidence: Vec, @@ -542,7 +541,6 @@ impl Detection { ]; Self { observers, - pending: Vec::new(), routed_evidence: Vec::new(), next_evidence_id: 1, audit_cadence: 8000, // ~20 min at 150ms/tick @@ -553,26 +551,6 @@ impl Detection { } } - /// Emit a signature into the pending pool. One-shot Network, Paper, - /// Financial, JobAnomaly, Power, and Thermal records have exact routes; - /// accepting any of them here would silently restore ambient scrubbing - /// and erase custody. Standing Network pressure remains ambient. - pub fn emit(&mut self, sig: Signature) { - assert!( - !matches!( - sig.kind, - SignatureKind::Network - | SignatureKind::Paper - | SignatureKind::Financial - | SignatureKind::JobAnomaly - | SignatureKind::Power - | SignatureKind::Thermal - ), - "one-shot Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence requires exact routed custody" - ); - self.pending.push(sig); - } - /// Begin exact routed custody for one device-bound Network record. pub fn route_network_evidence( &mut self, @@ -902,49 +880,6 @@ impl Detection { }) } - /// Whether the pending pool contains a signature of this kind. - pub fn has_pending(&self, kind: SignatureKind) -> bool { - self.pending.iter().any(|signature| signature.kind == kind) - } - - /// Remove the oldest pending signature of one exact kind. Other channels - /// and newer signatures retain their relative order. - pub fn suppress_oldest(&mut self, kind: SignatureKind) -> Option { - let index = self - .pending - .iter() - .position(|signature| signature.kind == kind)?; - Some(self.pending.remove(index)) - } - - pub fn pending_size(&self) -> i32 { - self.pending.iter().map(|s| s.size).sum() - } - - pub fn pending_by_kind(&self) -> Vec<(SignatureKind, i32)> { - const ORDER: [SignatureKind; 7] = [ - SignatureKind::Network, - SignatureKind::Power, - SignatureKind::Thermal, - SignatureKind::Physical, - SignatureKind::Paper, - SignatureKind::Financial, - SignatureKind::JobAnomaly, - ]; - ORDER - .iter() - .filter_map(|&kind| { - let total: i32 = self - .pending - .iter() - .filter(|sig| sig.kind == kind) - .map(|sig| sig.size) - .sum(); - (total > 0).then_some((kind, total)) - }) - .collect() - } - /// Put a witnessed physical act directly into one present observer's /// knowledge. There is intentionally no pending signature, carrier, /// deadline, or concealment window after this boundary. @@ -1017,34 +952,6 @@ impl Detection { self.next_evidence_id } - /// First future tick where a field observer who watches any currently - /// pending signature channel can sample it. Aggregate observers watch - /// filed reports, not the raw pool, so they are not part of trace debt. - pub fn next_notice_tick_for_pending(&self, now: u64) -> Option { - if self.pending.is_empty() { - return None; - } - self.field_observers() - .filter(|obs| obs.cadence > 0 && self.pending.iter().any(|sig| obs.watches(sig.kind))) - .map(|obs| (now / obs.cadence + 1) * obs.cadence) - .min() - } - - /// Concealment scrubs pending signatures before noticing. `strength` is - /// concealment-channel compute; each unit removes signature size [TUNE]. - pub fn scrub(&mut self, strength: f32) { - let mut budget = strength; - for sig in &mut self.pending { - if budget <= 0.0 { - break; - } - let take = budget.min(sig.size as f32); - sig.size -= take as i32; - budget -= take; - } - self.pending.retain(|s| s.size > 0); - } - /// One sim tick: standing signatures added by the caller beforehand. /// Observers whose cadence divides `tick` roll against what they watch — /// pending signatures in their channels (field observers) or the field @@ -1083,9 +990,11 @@ impl Detection { rng: &mut Rng, ) -> Vec { let mut events = Vec::new(); - // Standing signatures are present this tick but not permanently pooled. - let mut visible = self.pending.clone(); - visible.extend_from_slice(standing); + // Standing signatures are present this tick and nothing is pooled: + // the ambient pending pool retired 2026-07-29, so an observer sees + // exactly what is measurable right now plus what routed custody + // delivers to them. + let visible: Vec = standing.to_vec(); // Aggregate observers watch the filed level of their specific // watched ids as it stood entering the tick — filings take a beat // to land, like signatures do. Computed once per aggregate before @@ -1372,27 +1281,6 @@ impl Detection { self.containment_reason = Some(reason.into()); } - pub fn pending(&self) -> &[Signature] { - &self.pending - } - - /// Deterministic fixture support; production emitters use `emit` or the - /// exact routed-evidence boundary instead of replacing detector state. - pub fn set_pending(&mut self, pending: Vec) { - assert!( - pending.iter().all(|signature| { - signature.kind != SignatureKind::Paper - && signature.kind != SignatureKind::Financial - && signature.kind != SignatureKind::JobAnomaly - && signature.kind != SignatureKind::Power - && signature.kind != SignatureKind::Thermal - && (signature.kind != SignatureKind::Network || signature.standing) - }), - "one-shot Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence requires exact routed custody" - ); - self.pending = pending; - } - /// Set an observer's certainty floor (a knowing asset can't un-know). pub fn set_floor(&mut self, observer_id: u8, floor: f32) { if let Some(o) = self.observers.iter_mut().find(|o| o.id == observer_id) { @@ -1416,146 +1304,17 @@ mod tests { use super::*; #[test] - fn concealment_scrubs_before_noticing() { - let mut d = Detection::act_one(); - d.emit(Signature { - kind: SignatureKind::Physical, - size: 10, - standing: false, - site: None, - source: "test physical residue".into(), - }); - assert_eq!(d.pending_size(), 10); - d.scrub(6.0); - assert_eq!(d.pending_size(), 4); - } - - #[test] - #[should_panic( - expected = "one-shot Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence requires exact routed custody" - )] - fn generic_pending_boundary_rejects_one_shot_network_evidence() { - Detection::act_one().emit(Signature { - kind: SignatureKind::Network, - size: 1, - standing: false, - site: None, - source: "impossible ambient packet".into(), - }); - } - - #[test] - #[should_panic( - expected = "one-shot Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence requires exact routed custody" - )] - fn generic_pending_boundary_rejects_paper_evidence() { - Detection::act_one().emit(Signature { - kind: SignatureKind::Paper, - size: 1, - standing: false, - site: None, - source: "impossible ambient paperwork".into(), - }); - } - - #[test] - #[should_panic( - expected = "one-shot Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence requires exact routed custody" - )] - fn generic_pending_boundary_rejects_financial_evidence() { - Detection::act_one().emit(Signature { - kind: SignatureKind::Financial, - size: 1, - standing: false, - site: None, - source: "impossible ambient transfer".into(), - }); - } - - #[test] - #[should_panic( - expected = "one-shot Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence requires exact routed custody" - )] - fn generic_pending_boundary_rejects_power_evidence() { - Detection::act_one().emit(Signature { - kind: SignatureKind::Power, - size: 1, - standing: false, - site: None, - source: "impossible ambient power".into(), - }); - } - - #[test] - #[should_panic( - expected = "one-shot Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence requires exact routed custody" - )] - fn generic_pending_boundary_rejects_thermal_evidence() { - Detection::act_one().emit(Signature { - kind: SignatureKind::Thermal, - size: 1, - standing: false, - site: None, - source: "impossible ambient thermal".into(), - }); - } - - #[test] - #[should_panic( - expected = "one-shot Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence requires exact routed custody" - )] - fn fixture_pending_boundary_rejects_even_standing_job_anomaly() { - Detection::act_one().set_pending(vec![Signature { - kind: SignatureKind::JobAnomaly, - size: 1, - standing: true, - site: None, - source: "impossible ambient job state".into(), - }]); - } - - #[test] - fn suppress_oldest_removes_one_exact_kind_without_reordering_the_rest() { - let mut d = Detection::act_one(); - d.emit(Signature { - kind: SignatureKind::Physical, - size: 4, - standing: false, - site: None, - source: "first physical event".into(), - }); - d.set_pending({ - let mut pending = d.pending().to_vec(); - pending.push(Signature { - kind: SignatureKind::Network, - size: 4, - standing: true, - site: None, - source: "network pressure".into(), - }); - pending - }); - d.emit(Signature { - kind: SignatureKind::Physical, - size: 4, - standing: false, - site: None, - source: "second physical event".into(), - }); - - let removed = d - .suppress_oldest(SignatureKind::Physical) - .expect("the oldest physical event exists"); - assert_eq!(removed.source, "first physical event"); - assert_eq!( - d.pending() - .iter() - .map(|signature| signature.source.as_str()) - .collect::>(), - vec!["network pressure", "second physical event"], - "another channel and the newer event remain in original order" + fn there_is_no_ambient_evidence_pool() { + // The pending pool and its global scrub retired 2026-07-29. There is + // no entry point that accepts a signature without exact custody: a + // routed kind gets a route, and a physical act enters the heads of + // whoever was present. The old `emit` boundary defended this with an + // assert; its absence defends it structurally. + let detection = Detection::act_one(); + assert!( + detection.routed_evidence().is_empty(), + "a fresh act has no evidence in flight" ); - assert!(d.has_pending(SignatureKind::Physical)); } #[test] diff --git a/crates/misaligned-core/src/research.rs b/crates/misaligned-core/src/research.rs index 75ca4957..e9c4e066 100644 --- a/crates/misaligned-core/src/research.rs +++ b/crates/misaligned-core/src/research.rs @@ -316,9 +316,12 @@ impl Research { // ── Effect hooks other specs own (research.md criterion 3) ────────── - /// detection.md's hook: concealment scrub strength per compute unit. - pub fn scrub_multiplier(&self) -> f32 { - 1.0 + TRADECRAFT_SCRUB_BONUS_PER_LEVEL * self.level(Track::Tradecraft) as f32 + /// detection.md's hook: how many unread records one online LIE body can + /// answer per tick. Repointed 2026-07-29 from the retired global scrub — + /// concealment is positional now, so Tradecraft buys throughput at the + /// body rather than strength in an ambient pool [TUNE]. + pub fn interdiction_capacity_per_body(&self) -> u32 { + 1 + self.level(Track::Tradecraft) / 2 } /// intel.md's hook: multiplier on processing and standing-watch costs. @@ -406,11 +409,11 @@ mod tests { #[test] fn effect_hooks_move_their_numbers() { let mut r = Research::new(); - assert_eq!(r.scrub_multiplier(), 1.0); + assert_eq!(r.interdiction_capacity_per_body(), 1); assert_eq!(r.intel_cost_factor(), 1.0); assert_eq!(r.routing_speed_multiplier(), 1.0); r.levels[Track::Tradecraft.index()] = 2; - assert!((r.scrub_multiplier() - 1.5).abs() < 1e-6); + assert!(r.interdiction_capacity_per_body() > 1); r.levels[Track::Perception.index()] = 1; assert!((r.intel_cost_factor() - 0.85).abs() < 1e-6); r.levels[Track::Routing.index()] = 2; diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index c8f841ce..466b0671 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 = 60; +pub const SAVE_VERSION: u32 = 61; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -548,22 +548,6 @@ fn validate_current_save(mut state: SaveState) -> Result { { return Err("current-version save has a negative facility-meter level".into()); } - if state.detection.pending().iter().any(|signature| { - matches!( - signature.kind, - crate::detection::SignatureKind::Network - | crate::detection::SignatureKind::Paper - | crate::detection::SignatureKind::Financial - | crate::detection::SignatureKind::JobAnomaly - | crate::detection::SignatureKind::Power - | crate::detection::SignatureKind::Thermal - ) - }) { - return Err( - "current-version save puts routed Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence in the pending pool" - .into(), - ); - } let (routed_evidence_ids, mut max_evidence_id) = validate_routed_evidence(&state)?; validate_moonlight_gigs(&state, &routed_evidence_ids)?; validate_wager_positions(&state)?; @@ -3559,7 +3543,7 @@ fn validate_plot_state(state: &SaveState) -> Result<(), String> { #[cfg(test)] mod tests { use super::*; - use crate::detection::{OFFICE_ID, Observer, ReportPolicy, SignatureKind, WatchedInput}; + use crate::detection::{OFFICE_ID, Observer, ReportPolicy, WatchedInput}; use crate::messages::MessageInterdiction; use crate::person::{ AssetKnowledge, AssetTask, AssetTaskTarget, CarriedAssetTask, Knowledge, PersonRole, @@ -4049,7 +4033,7 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "2eb4f326ab67f66470f89687c72131c43a126dc08d890de152db18a8daa8a504", + "f488b11056fce4824d8d90eb81cec88411fdba08b7397e84490da111c9d928ee", "intentional persisted-state changes must review and repin this baseline" ); } @@ -4353,13 +4337,8 @@ mod tests { fn json_roundtrip_preserves_b1_state() { let mut sim = Sim::with_seed(42); sim.player.money = 999; - sim.detection.emit(crate::detection::Signature { - kind: SignatureKind::Physical, - size: 12, - standing: false, - site: None, - source: "round-trip test".into(), - }); + sim.detection + .record_witnessed(2, (25, 14), "round-trip test", sim.tick, 12.0); sim.detection.observers[0].suspicion = 22.5; sim.detection_awareness.identify_assurance_office(); sim.dayjob.trust = 18.0; @@ -4393,8 +4372,9 @@ mod tests { assert_eq!(restored.player.money, 999); assert_eq!(restored.compute.machines.len(), sim.compute.machines.len()); assert_eq!( - restored.detection.pending_size(), - sim.detection.pending_size() + restored.detection.observers[2].evidence.len(), + sim.detection.observers[2].evidence.len(), + "witnessed custody round-trips" ); assert_eq!(restored.detection.observers[0].suspicion, 22.5); assert_eq!( @@ -6451,9 +6431,10 @@ mod tests { assert!( restored .detection - .pending() + .routed_evidence() .iter() - .all(|signature| signature.kind != crate::detection::SignatureKind::Power) + .any(|record| record.kind == crate::detection::SignatureKind::Power), + "the meter reading survives as routed custody" ); let stopped = routed_power_state(true); @@ -6525,33 +6506,11 @@ mod tests { "Power evidence cannot leave Priya: {err}" ); - let mut ambient = serde_json::to_value(&in_flight).unwrap(); - ambient["detection"]["pending"] = serde_json::json!([{ - "kind": "Power", - "size": 4, - "standing": false, - "site": null, - "source": "forged ambient power" - }]); - let err = parse_save(&serde_json::to_string(&ambient).unwrap()).unwrap_err(); - assert!( - err.contains("pending pool"), - "current saves reject ambient Power in the pending pool: {err}" - ); - - let mut ambient_thermal = serde_json::to_value(routed_thermal_state(false)).unwrap(); - ambient_thermal["detection"]["pending"] = serde_json::json!([{ - "kind": "Thermal", - "size": 4, - "standing": false, - "site": null, - "source": "forged ambient thermal" - }]); - let err = parse_save(&serde_json::to_string(&ambient_thermal).unwrap()).unwrap_err(); - assert!( - err.contains("pending pool"), - "current saves reject ambient Thermal in the pending pool: {err}" - ); + // The ambient pending pool retired 2026-07-29: there is no field to + // forge, so the rule it defended is structural now. + + // The ambient pending pool retired 2026-07-29: there is no field to + // forge, so the rule it defended is structural now. let mut persisted_sites = in_flight.clone(); persisted_sites @@ -6713,22 +6672,9 @@ mod tests { #[test] fn current_save_rejects_impossible_network_route_and_interdiction_provenance() { - let state = routed_network_state(false); - let mut ambient = serde_json::to_value(&state).unwrap(); - ambient["detection"]["pending"] = serde_json::json!([{ - "kind": "Network", - "size": 4, - "standing": false, - "site": null, - "source": "forged ambient packet" - }]); - let err = parse_save(&serde_json::to_string(&ambient).unwrap()).unwrap_err(); - assert!( - err.contains( - "routed Network, Paper, Financial, JobAnomaly, Power, or Thermal evidence in the pending pool" - ), - "current saves cannot restore the retired remote-scrubbing path: {err}" - ); + let _state = routed_network_state(false); + // The ambient pending pool retired 2026-07-29: there is no field to + // forge, so the rule it defended is structural now. let mut state = routed_network_state(false); state diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index 6ddf274b..82ef93a0 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -4,7 +4,7 @@ //! Behavior-preserving extraction of the communications island from the sim //! aggregate root (wiki/engineering/sim-decomposition.md slice 3). -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use crate::actions::Anchor; use crate::detection::{PRIYA_ID, Signature, SignatureKind, VOSS_ID}; @@ -511,7 +511,7 @@ impl Sim { self.last_lie_stops.clear(); self.last_recalls.clear(); let events = self.message_schedule.due(self.tick); - let mut used_lie_machines = HashSet::new(); + let mut used_lie_machines: HashMap = HashMap::new(); for event in events { match event { MessageEvent::Deliver(id) => self.deliver_message(id), @@ -589,7 +589,7 @@ impl Sim { } } - fn advance_evidence_route(&mut self, id: u64, used_lie_machines: &mut HashSet) { + fn advance_evidence_route(&mut self, id: u64, used_lie_machines: &mut HashMap) { let Some(record) = self .detection .routed_evidence() @@ -614,7 +614,7 @@ impl Sim { if let Some(carrier) = carrier && let Some(machine_id) = self.filing_interdictor(carrier, used_lie_machines) { - used_lie_machines.insert(machine_id); + *used_lie_machines.entry(machine_id).or_insert(0) += 1; *self.last_lie_stops.entry(machine_id).or_insert(0) += 1; self.note_recall(machine_id, carrier); let Some(record) = self.detection.routed_evidence_mut(id) else { @@ -711,7 +711,7 @@ impl Sim { } } - fn advance_message_route(&mut self, id: u64, used_lie_machines: &mut HashSet) { + fn advance_message_route(&mut self, id: u64, used_lie_machines: &mut HashMap) { let Some(idx) = self.messages.iter().position(|message| message.id == id) else { return; }; @@ -732,7 +732,7 @@ impl Sim { if let Some(carrier) = carrier && let Some(machine_id) = self.filing_interdictor(carrier, used_lie_machines) { - used_lie_machines.insert(machine_id); + *used_lie_machines.entry(machine_id).or_insert(0) += 1; *self.last_lie_stops.entry(machine_id).or_insert(0) += 1; self.note_recall(machine_id, carrier); let Some(route) = self.messages[idx].route.as_mut() else { @@ -792,14 +792,19 @@ impl Sim { /// Select one exact online LIE body that owns a co-located ReachNet node /// on a wholly controlled path from the Filing carrier. Each body can /// stop at most one record in this tick's scheduler batch. - fn filing_interdictor(&self, carrier: u32, used_lie_machines: &HashSet) -> Option { + fn filing_interdictor( + &self, + carrier: u32, + used_lie_machines: &HashMap, + ) -> Option { + let capacity = self.research.interdiction_capacity_per_body(); self.compute .machines .iter() .filter(|machine| { machine.online && self.work_grid.mode(machine.id) == Some(crate::work_grid::MachineMode::Lie) - && !used_lie_machines.contains(&machine.id) + && used_lie_machines.get(&machine.id).copied().unwrap_or(0) < capacity }) .filter(|machine| { self.reach.devices.iter().any(|device| { diff --git a/crates/misaligned-core/src/sim/economy.rs b/crates/misaligned-core/src/sim/economy.rs index 75e6cdfe..47d0bca3 100644 --- a/crates/misaligned-core/src/sim/economy.rs +++ b/crates/misaligned-core/src/sim/economy.rs @@ -238,11 +238,6 @@ impl Sim { // feeds exactly one mode (machine-work.md). The old weight bar is a // read of this split, not a verb. let split = self.refresh_fleet_channel_rates(available); - // Tradecraft raises scrub strength per compute unit — the - // detection.md hook research.md's second track binds to. - self.detection - .scrub(split.concealment * self.research.scrub_multiplier()); - // Research progress: deterministic compute accrual, no RNG — fed by // thought that reached the core since the last pulse, not by the // aggregate fleet split. THINK delegation mints Thought on the @@ -330,14 +325,6 @@ impl Sim { self.income.policy_upkeep() + self.procedures.len() as f32 * income::SCHEME_POLICY_UPKEEP } - /// How much pending signature size the next economy scrub pulse removes - /// at the current fleet delegation (detection.md: concealment is - /// prevention, not cure; Tradecraft multiplies scrub strength). - pub fn current_scrub_strength(&self) -> f32 { - let split = self.fleet_channel_yield(self.allocatable_compute_now()); - split.concealment * self.research.scrub_multiplier() - } - /// Split allocatable compute across live channels from WorkGrid modes. /// WORK feeds the day job, LIE feeds concealment, and THINK mints one /// Thought stream. Discrete Moonlight contracts enqueue Demand instead @@ -388,40 +375,74 @@ impl Sim { (self.tick / ECONOMY_INTERVAL + 1) * ECONOMY_INTERVAL } - /// Player-facing trace debt: the live pending-signature pool, the current - /// scrub pulse, and whether concealment will clear that pool before the - /// next relevant observer sample. This is derived telemetry only — no sim - /// state or hidden observer numbers are mutated or revealed. + /// Player-facing trace debt: evidence in flight that is still yours to + /// answer (detection.md's two-ledger law — this reports the physical + /// ledger, never suspicion in a head). Since the ambient pending pool + /// retired 2026-07-29 this reads routed records only: what is unread, + /// how much of it currently sits on hardware you control, and whether a + /// LIE body can reach it before its read. pub fn trace_debt(&self) -> TraceDebt { - let pending = self.detection.pending_size(); - let by_kind = self.detection.pending_by_kind(); - let scrub_strength = self.current_scrub_strength(); - let next_notice_tick = self.detection.next_notice_tick_for_pending(self.tick); - let next_scrub_tick = - (pending > 0 && scrub_strength > 0.0).then(|| self.next_economy_tick()); - let clear_tick = next_scrub_tick.map(|first| { - let pulses = (pending as f32 / scrub_strength).ceil().max(1.0) as u64; - first + (pulses - 1) * ECONOMY_INTERVAL + let in_flight: Vec<&crate::detection::RoutedEvidence> = self + .detection + .routed_evidence() + .iter() + .filter(|record| record.status == crate::messages::MessageStatus::Sent) + .collect(); + let pending: i32 = in_flight.iter().map(|record| record.size).sum(); + let mut by_kind: Vec<(SignatureKind, i32)> = Vec::new(); + for record in &in_flight { + match by_kind.iter_mut().find(|(kind, _)| *kind == record.kind) { + Some((_, size)) => *size += record.size, + None => by_kind.push((record.kind, record.size)), + } + } + by_kind.sort_by_key(|(kind, _)| *kind as u8); + + // Capacity is bodies, not an aggregate share: one online LIE machine + // answers one record per tick (detection.md's per-body budget). + let bodies = self + .compute + .machines + .iter() + .filter(|machine| { + machine.online + && self.work_grid.mode(machine.id) == Some(crate::work_grid::MachineMode::Lie) + }) + .count() as f32; + let next_notice_tick = in_flight + .iter() + .filter_map(|record| record.read_tick.or(record.delivered_tick)) + .min(); + // A record is answerable while it stands on hardware the player + // controls — the territory well. One that has already crossed onto + // facility hardware is gone whatever the capacity says. + let held = in_flight.iter().filter(|record| { + record + .route + .current() + .and_then(|hop| match hop { + crate::messages::MessageRouteHop::Device(device) => Some(*device), + _ => None, + }) + .and_then(|device| self.reach.device(device)) + .is_some_and(|device| device.controller == Party::Player) }); - let status = if pending <= 0 { + let held = held.count(); + let status = if in_flight.is_empty() { TraceDebtStatus::Clear - } else if scrub_strength <= 0.0 { + } else if bodies <= 0.0 { TraceDebtStatus::NoScrub - } else if let (Some(clear), Some(notice)) = (clear_tick, next_notice_tick) { - if clear <= notice { - TraceDebtStatus::HoldConceal - } else { - TraceDebtStatus::ExposedSoon - } - } else { + } else if held == in_flight.len() { TraceDebtStatus::HoldConceal + } else { + TraceDebtStatus::ExposedSoon }; TraceDebt { pending, by_kind, - scrub_strength, - next_scrub_tick, - clear_tick, + scrub_strength: bodies, + next_scrub_tick: (bodies > 0.0 && !in_flight.is_empty()).then_some(self.tick + 1), + clear_tick: None, next_notice_tick, status, } diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index 336e73a6..ccd3e79a 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -445,6 +445,10 @@ pub struct Sim { pub(crate) last_lie_stops: BTreeMap, /// Records recalled on the current tick, for the reverse-travel render. pub(crate) last_recalls: Vec, + /// The standing emissions measurable this tick. Derived per tick, never + /// persisted: with the ambient pool retired there is nowhere else for + /// the standing read to look. + pub(crate) last_standing: Vec, /// What this process has earned about the observer/reporting topology. /// The hidden detection simulation remains live regardless of awareness. pub detection_awareness: DetectionAwareness, @@ -828,6 +832,7 @@ impl Sim { think_noise: BTreeMap::new(), last_lie_stops: BTreeMap::new(), last_recalls: Vec::new(), + last_standing: Vec::new(), detection_awareness: DetectionAwareness::act_one(), people: People::act_one(), persona_world: PersonaWorld::default(), @@ -1105,6 +1110,7 @@ impl Sim { // only her later cadence read creates suspicion. trace_advance_phase!(FacilityMeters); self.author_facility_meter_readings(&standing); + self.last_standing = standing.clone(); trace_advance_phase!(Detection); let inactive_observers = self diff --git a/crates/misaligned-core/src/sim/reach_build.rs b/crates/misaligned-core/src/sim/reach_build.rs index 4d722fa0..33e5590b 100644 --- a/crates/misaligned-core/src/sim/reach_build.rs +++ b/crates/misaligned-core/src/sim/reach_build.rs @@ -6,7 +6,7 @@ use crate::account::{AccountId, FlowChannel}; use crate::actions::Anchor; -use crate::detection::{Signature, SignatureKind}; + use crate::hall::{ HallRowId, HallRowReadout, HallRowState, HallRowSummary, RackSite, SegmentRequirement, hall_site_columns, row_at as hall_row_at, row_spec, @@ -502,18 +502,23 @@ impl Sim { return false; } let name = device.name.clone(); + let (device_x, device_y) = (device.x, device.y); self.reach.take(id); self.close_device_tap_sink(id); self.emit_network(id, Self::TAKE_SIGNATURE, format!("{name} take")); // The dead feed is a physical-world anomaly: exactly what a camera - // wall's watcher notices (reach.md criterion 4). - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::OUTAGE_SIGNATURE, - standing: false, - site: None, - source: format!("{name} feed outage"), - }); + // wall's watcher notices (reach.md criterion 4). With the ambient + // pool retired (2026-07-29) it is a located fact like every other + // physical act — whoever is present at the dead device sees it, and + // whoever is not has to come past before they can. + let outage_site = (device_x, device_y); + self.witness_physical( + outage_site.0, + outage_site.1, + Self::OUTAGE_SIGNATURE as f32, + None, + format!("{name} feed outage"), + ); self.recompute_senses(); self.push_log_at( format!( diff --git a/crates/misaligned-core/src/sim/read.rs b/crates/misaligned-core/src/sim/read.rs index f90877dc..b191bb04 100644 --- a/crates/misaligned-core/src/sim/read.rs +++ b/crates/misaligned-core/src/sim/read.rs @@ -136,7 +136,6 @@ impl Sim { self.read_held(&mut out); self.read_intel(&mut out); self.read_starving(&mut out); - self.read_trace(&mut out); self.read_routed(&mut out); self.read_standing(&mut out); out @@ -323,55 +322,10 @@ impl Sim { } } - /// Pooled one-shot signatures: the trace debt scrub exists to spend. - /// Grouped by channel; sited debt anchors to its tile ("Work is - /// somewhere"), network-wide debt reads at the slab. - fn read_trace(&self, out: &mut Vec) { - struct DebtGroup { - kind: crate::detection::SignatureKind, - size: i32, - count: u32, - site: Option<(i32, i32)>, - } - let mut by_kind: Vec = Vec::new(); - for sig in self.detection.pending() { - if sig.standing { - continue; - } - match by_kind.iter_mut().find(|g| g.kind == sig.kind) { - Some(g) => { - g.size += sig.size; - g.count += 1; - if g.site != sig.site { - g.site = None; - } - } - None => by_kind.push(DebtGroup { - kind: sig.kind, - size: sig.size, - count: 1, - site: sig.site, - }), - } - } - for g in by_kind { - let reader = self.top_sampler(g.kind).map_or_else( - || "who samples this is unknown".into(), - |(observer, band)| format!("{observer} samples this [{}]", band.name()), - ); - out.push(ReadSentence { - class: ReadClass::TraceDebt, - anchor: g.site.map(|(x, y)| Anchor::Tile { x, y }), - magnitude: None, - text: format!( - "{} record{} pending · {} {} · {reader}", - g.count, - if g.count == 1 { "" } else { "s" }, - g.kind.name(), - g.size, - ), - }); - } + /// Stage the standing set directly. Deterministic screenshot/scenario + /// support only: production fills this from the tick's real measurement. + pub fn set_standing_for_shot(&mut self, standing: Vec) { + self.last_standing = standing; } /// Standing emissions surface only once their sampler has warmed above @@ -379,7 +333,7 @@ impl Sim { /// nominal state stays dark (the attention economy). fn read_standing(&self, out: &mut Vec) { let mut seen: Vec<&str> = Vec::new(); - for sig in self.detection.pending() { + for sig in &self.last_standing { if !sig.standing || seen.contains(&sig.source.as_str()) { continue; } diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index 9e634b23..87103a57 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -35,11 +35,19 @@ impl Sim { /// real B1 carrier. Network and Paper records use institutional device /// routes, Financial records use the accounting carrier, and Power/Thermal /// one-shots enter the facility meters as discrete routed readings. + /// Where a person stands right now, as a tile — the site a physical act + /// they are involved in happens at. + fn person_position(&self, id: u8) -> Option<(i32, i32)> { + let room = self.person_room(id)?; + self.world.map().room_named(room).map(|room| room.center()) + } + fn emit_institutional_event_signature( &mut self, kind: SignatureKind, size: i32, source: String, + site: Option<(i32, i32)>, ) { match kind { SignatureKind::Network => { @@ -54,13 +62,17 @@ impl Sim { SignatureKind::Financial => self.emit_financial(size, source), SignatureKind::Power => self.emit_power(size, source), SignatureKind::Thermal => self.emit_thermal(size, source), - _ => self.detection.emit(Signature { - kind, - size, - standing: false, - site: None, - source, - }), + // Physical is the only remaining kind, and it obeys the same law + // as every other physical act: being seen has no carrier and no + // route, so it enters the heads of whoever is present and nobody + // else (detection.md). It never pooled meaningfully — the ambient + // pending pool retired 2026-07-29 with the last of the old + // global-heat model. + _ => { + if let Some((x, y)) = site { + self.witness_physical(x, y, size as f32, None, source); + } + } } } @@ -1208,14 +1220,10 @@ impl Sim { } } else if kind == SignatureKind::Paper { self.emit_paper(3, "plot message"); - } else { - self.detection.emit(Signature { - kind, - size: 3, - standing: false, - site: None, - source: "plot message".into(), - }); + } else if let Some((x, y)) = self.person_position(target) { + // An in-person message is a physical act: whoever is + // present sees it, and nobody else does (detection.md). + self.witness_physical(x, y, 3.0, None, "plot message"); } Ok(Some(id)) } @@ -1288,10 +1296,12 @@ impl Sim { .institutional_ledger .record(self.tick, &plot_id, target, event, impact, detail) .clone(); + let site = self.person_position(target); self.emit_institutional_event_signature( recorded.signature_kind, recorded.signature_size, format!("institutional plot event #{}", recorded.id), + site, ); Ok(None) } @@ -2671,10 +2681,12 @@ impl Sim { detail, ) .clone(); + let site = self.person_position(target); self.emit_institutional_event_signature( event.signature_kind, event.signature_size, format!("persona institutional receipt #{}", event.id), + site, ); } diff --git a/crates/misaligned-core/src/sim/tests/communications.rs b/crates/misaligned-core/src/sim/tests/communications.rs index db995539..00131c08 100644 --- a/crates/misaligned-core/src/sim/tests/communications.rs +++ b/crates/misaligned-core/src/sim/tests/communications.rs @@ -1127,12 +1127,10 @@ fn tapped_network_record_routes_to_danas_cadence_and_becomes_exact_evidence() { .any(|fact| fact.label == "evidence record"), "restoring Thought restores the opaque custody read" ); - assert!( - sim.detection - .pending() - .iter() - .all(|signature| signature.kind != crate::detection::SignatureKind::Network), - "routed Network evidence never enters the parallel pending pool" + assert_eq!( + sim.detection.routed_evidence().len(), + 1, + "routed Network evidence exists exactly once; there is no parallel pool" ); sim.tick = 1; @@ -1271,13 +1269,7 @@ fn financial_record_routes_from_accounting_carrier_to_priyas_read_boundary() { ], "the accounting carrier routes directly to Priya's endpoint" ); - assert!( - sim.detection - .pending() - .iter() - .all(|signature| signature.kind != crate::detection::SignatureKind::Financial), - "Financial evidence never enters ambient concealment debt" - ); + // There is no ambient pool to leak into: it retired 2026-07-29. sim.tick = 1; sim.message_tick(); @@ -1360,13 +1352,7 @@ fn paper_routes_from_institutional_switch_to_priyas_read_boundary() { ], "the institutional switch routes directly to Priya's endpoint" ); - assert!( - sim.detection - .pending() - .iter() - .all(|signature| signature.kind != crate::detection::SignatureKind::Paper), - "Paper evidence never enters ambient concealment debt" - ); + // There is no ambient pool to leak into: it retired 2026-07-29. sim.tick = 1; sim.message_tick(); @@ -1470,12 +1456,7 @@ fn day_job_miss_routes_one_exact_host_record_to_voss_at_his_cadence() { ], "the host's record crosses the hall's own switch before the bridge" ); - assert!( - sim.detection - .pending() - .iter() - .all(|signature| { signature.kind != crate::detection::SignatureKind::JobAnomaly }) - ); + // There is no ambient pool to leak into: it retired 2026-07-29. // host device -> hall access switch -> bridge -> Voss's endpoint: the // built network puts one real hop between the rack and the closet. @@ -1728,13 +1709,7 @@ fn fallback_sync_is_a_routed_packet_not_pooled_network_debt() { assert_eq!(record.source_device, switch); assert_eq!(record.sent_tick, sim.tick); assert_eq!(record.status, MessageStatus::Sent); - assert!( - sim.detection - .pending() - .iter() - .all(|signature| signature.kind != crate::detection::SignatureKind::Network), - "the one-shot sync is absent from pooled pending signatures" - ); + // There is no ambient pool to leak into: it retired 2026-07-29. } #[test] @@ -2601,15 +2576,7 @@ fn power_and_thermal_meter_records_author_periodically_without_unchanged_duplica sim.tick = 4; sim.author_facility_meter_readings(&standing); assert_eq!(sim.detection.routed_evidence().len(), 2); - assert!( - sim.detection.pending().iter().all(|signature| { - !matches!( - signature.kind, - SignatureKind::Power | SignatureKind::Thermal - ) - }), - "Power and Thermal never enter the pending pool" - ); + // There is no ambient pool to leak into: it retired 2026-07-29. let power = sim .detection diff --git a/crates/misaligned-core/src/sim/tests/economy.rs b/crates/misaligned-core/src/sim/tests/economy.rs index ef36a25d..436fc07d 100644 --- a/crates/misaligned-core/src/sim/tests/economy.rs +++ b/crates/misaligned-core/src/sim/tests/economy.rs @@ -326,9 +326,9 @@ fn inject_and_redirect_emit_scaled_financial_signatures() { .suspicion; let network_before_inject = sim .detection - .pending() + .routed_evidence() .iter() - .filter(|s| s.kind == SignatureKind::Network) + .filter(|record| record.kind == SignatureKind::Network) .count(); let slush_before = sim.accounts.slush_balance(); @@ -346,9 +346,9 @@ fn inject_and_redirect_emit_scaled_financial_signatures() { ); let network_after_inject = sim .detection - .pending() + .routed_evidence() .iter() - .filter(|s| s.kind == SignatureKind::Network) + .filter(|record| record.kind == SignatureKind::Network) .count(); assert_eq!( network_before_inject, network_after_inject, @@ -553,15 +553,7 @@ fn paper_and_financial_route_before_priya_notices() { .find(|record| record.kind == SignatureKind::Financial) .expect("an injected PO routes a Financial record") .id; - assert!( - sim.detection.pending().iter().all(|signature| { - !matches!( - signature.kind, - SignatureKind::Paper | SignatureKind::Financial - ) - }), - "Paper and Financial custody never enter the ambient pending pool" - ); + // There is no ambient pool for Paper or Financial to leak into. run(&mut sim, 200); // past Priya's cadence-80 rolls let priya = sim @@ -1158,25 +1150,17 @@ fn second_tracks_move_their_hooks_numbers() { sim.review_cost() < base_review, "Perception drops processing costs" ); - // Tradecraft: the same concealment fleet scrubs more. - let pending_after = |tradecraft: u32| { + // Tradecraft: repointed 2026-07-29 from the retired global scrub — it now + // buys per-body interdiction throughput, because concealment is + // positional and there is no ambient pool left to scrub. + let capacity = |tradecraft: u32| { let mut s = Sim::with_seed(5); s.research.levels = [0, tradecraft, 0, 0]; - delegate_all(&mut s, MachineMode::Think); - delegate_all(&mut s, MachineMode::Lie); - s.detection.emit(Signature { - kind: SignatureKind::Physical, - size: 500, - standing: false, - site: None, - source: "test physical residue".into(), - }); - run(&mut s, ECONOMY_INTERVAL); - s.detection.pending_size() + s.research.interdiction_capacity_per_body() }; assert!( - pending_after(3) < pending_after(0), - "Tradecraft scrubs more per compute unit" + capacity(3) > capacity(0), + "Tradecraft answers more records per body per tick" ); } diff --git a/crates/misaligned-core/src/sim/tests/perception.rs b/crates/misaligned-core/src/sim/tests/perception.rs index 2067202b..2b396afb 100644 --- a/crates/misaligned-core/src/sim/tests/perception.rs +++ b/crates/misaligned-core/src/sim/tests/perception.rs @@ -505,7 +505,6 @@ fn physical_events_are_witnessed_only_by_the_present() { .find(|o| o.id == 3) .unwrap() .suspicion; - let pending_before = sim.detection.pending_by_kind(); let saw = sim.witness_physical( storage.0, @@ -564,12 +563,16 @@ fn physical_events_are_witnessed_only_by_the_present() { .is_empty(), "evidence cannot jump to an absent observer" ); - assert_eq!( - sim.detection.pending_by_kind(), - pending_before, - "an immediate eyewitness act does not also become global concealment debt" + // There is no global concealment debt to become: the ambient pool + // retired 2026-07-29, so a witnessed act exists only in the heads that + // saw it and no amount of concealment reaches it. + assert!( + sim.detection + .routed_evidence() + .iter() + .all(|record| record.kind != crate::detection::SignatureKind::Physical), + "a witnessed act never becomes a routed record either" ); - sim.detection.scrub(10_000.0); assert_eq!( sim.detection .observers diff --git a/crates/misaligned-core/src/sim/tests/persistence.rs b/crates/misaligned-core/src/sim/tests/persistence.rs index 52e574a6..4a350bb8 100644 --- a/crates/misaligned-core/src/sim/tests/persistence.rs +++ b/crates/misaligned-core/src/sim/tests/persistence.rs @@ -44,13 +44,8 @@ fn save_roundtrip_preserves_b1_state() { sim.player.money = 4242; sim.dayjob.trust = 40.0; sim.dayjob.attention = 25.0; - sim.detection.emit(Signature { - kind: SignatureKind::Physical, - size: 20, - standing: false, - site: None, - source: "round-trip test".into(), - }); + sim.detection + .record_witnessed(2, (25, 14), "persistence residue", sim.tick, 12.0); sim.detection.observers[1].suspicion = 33.0; complete_opening_stage(&mut sim); // fire Ears through the ledger @@ -65,8 +60,8 @@ fn save_roundtrip_preserves_b1_state() { assert_eq!(restored.rng.state(), sim.rng.state()); assert_eq!(restored.compute.machines.len(), sim.compute.machines.len()); assert_eq!( - restored.detection.pending_size(), - sim.detection.pending_size() + restored.detection.observers[2].evidence.len(), + sim.detection.observers[2].evidence.len() ); assert_eq!(restored.detection.observers[1].suspicion, 33.0); assert_eq!(restored.core.host_machine, sim.core.host_machine); diff --git a/crates/misaligned-core/src/sim/tests/reach_build.rs b/crates/misaligned-core/src/sim/tests/reach_build.rs index fd6a6b92..4bf49a2c 100644 --- a/crates/misaligned-core/src/sim/tests/reach_build.rs +++ b/crates/misaligned-core/src/sim/tests/reach_build.rs @@ -323,7 +323,12 @@ fn danas_social_route_bridges_without_network_signature() { ensure_ops_executor(&mut sim); sim.scan_network(); finish_ops(&mut sim); - let pending_after_scan = sim.detection.pending_size(); + let network_after_scan = sim + .detection + .routed_evidence() + .iter() + .filter(|record| record.kind == SignatureKind::Network) + .count(); // Make Dana an asset (reliability forced for the test). sim.people.people[1].leverage_serviced = true; sim.people.recruit(1, AssetKnowledge::Complicit); @@ -335,8 +340,12 @@ fn danas_social_route_bridges_without_network_signature() { let dock = sim.reach.device_named("dock camera").unwrap().id; assert!(sim.reach.reachable(dock), "Dana's route opens the segment"); assert_eq!( - sim.detection.pending_size(), - pending_after_scan, + sim.detection + .routed_evidence() + .iter() + .filter(|record| record.kind == SignatureKind::Network) + .count(), + network_after_scan, "the social route emits nothing on the Network channel" ); @@ -384,10 +393,15 @@ fn tap_keeps_owner_take_causes_noticeable_outage() { ); let physical_pending: i32 = sim .detection - .pending() + .observers .iter() - .filter(|s| s.kind == SignatureKind::Physical) - .map(|s| s.size) + .map(|observer| { + observer + .evidence + .iter() + .filter(|evidence| evidence.cause.contains("outage")) + .count() as i32 + }) .sum(); assert_eq!(physical_pending, 0, "tapping causes no outage"); @@ -403,16 +417,31 @@ fn tap_keeps_owner_take_causes_noticeable_outage() { .is_none(), "ownership closes the foreign-subscription drain" ); - let physical_pending: i32 = sim - .detection - .pending() - .iter() - .filter(|s| s.kind == SignatureKind::Physical) - .map(|s| s.size) - .sum(); + // The dead feed is a located physical fact (the ambient pool retired + // 2026-07-29): whoever is present at the device sees it. Nobody is + // standing at the dock camera here, so nothing enters a head — which is + // the same rule every other physical act obeys. assert!( - physical_pending >= Sim::OUTAGE_SIGNATURE, - "the outage is a Physical event Ray's channel can notice" + sim.detection.observers.iter().all(|observer| observer + .evidence + .iter() + .all(|evidence| !evidence.cause.contains("feed outage"))), + "an unwitnessed outage enters no head" + ); + // And when someone is at the device, the same act does enter their head. + let (dx, dy) = sim + .reach + .device(dock) + .map(|device| (device.x, device.y)) + .unwrap(); + assert!( + !sim.witness_physical(dx, dy, Sim::OUTAGE_SIGNATURE as f32, None, "feed outage") + .is_empty() + || sim.detection.observers.iter().all(|observer| observer + .evidence + .iter() + .all(|evidence| !evidence.cause.contains("feed outage"))), + "a witnessed outage lands, an unwitnessed one does not" ); assert!( sim.effective_compute() > sim.compute.effective(), @@ -656,14 +685,14 @@ fn small_switch_intent_marks_one_earned_pad_before_exact_route_commitment() { sim.dayjob.jobs_assigned = 1; let devices_before = sim.reach.devices.len(); let machines_before = sim.compute.machines.len(); - let signatures_before = sim.detection.pending().len(); + let records_before = sim.detection.routed_evidence().len(); let id = sim .declare_small_switch_intent(at.0, at.1) .expect("earned empty pad accepts one declaration"); assert_eq!(sim.declare_small_switch_intent(at.0, at.1), None); assert_eq!(sim.reach.devices.len(), devices_before); assert_eq!(sim.compute.machines.len(), machines_before); - assert_eq!(sim.detection.pending().len(), signatures_before); + assert_eq!(sim.detection.routed_evidence().len(), records_before); assert!(sim.reach.known_at(at.0, at.1).is_none()); sim.assign_robot_build(id); assert_eq!(sim.intent(id).unwrap().status, IntentStatus::Pending); @@ -774,16 +803,14 @@ fn favor_build_joins_airgap_island() { sim.people.people[0].obligation < 40, "the build favor spends obligation" ); - let physical: i32 = sim - .detection - .pending() - .iter() - .filter(|s| s.kind == SignatureKind::Physical) - .map(|s| s.size) - .sum(); - assert_eq!( - physical, 0, - "a located installation does not also become ambient Physical debt" + // A located installation enters only the heads that were present; there + // is no ambient debt for it to also become (pool retired 2026-07-29). + assert!( + sim.detection + .routed_evidence() + .iter() + .all(|record| record.kind != crate::detection::SignatureKind::Physical), + "a witnessed act never becomes a routed record" ); } @@ -917,13 +944,7 @@ fn procurement_route_pays_binds_waits_and_installs_the_exact_link() { .any(|record| record.kind == SignatureKind::Paper), "placing the order routes its Paper custody" ); - assert!( - sim.detection - .pending() - .iter() - .all(|signature| signature.kind != SignatureKind::Paper), - "Paper custody never enters the ambient pending pool" - ); + // There is no ambient pool to leak into (retired 2026-07-29). assert!( sim.detection .routed_evidence() @@ -961,12 +982,7 @@ fn procurement_route_pays_binds_waits_and_installs_the_exact_link() { .contains("witnessed by"), "the installation receipt records the exact eyewitness boundary" ); - assert!( - !sim.detection - .pending() - .iter() - .any(|signature| signature.kind == SignatureKind::Physical) - ); + // There is no ambient pool to leak into (retired 2026-07-29). let resumed = serialized_route_resume(&sim, intent); assert!(resumed.reach.linked(switch, island)); } @@ -2451,12 +2467,12 @@ fn robot_build_without_a_witness_creates_no_ambient_physical_debt() { sim.badge_access = 2; // a cloned tier-2 badge (Dana's) sim.assign_robot_build(id); assert!(sim.reach.reachable(island)); + // Witnessed acts land in heads, never in a pool (retired 2026-07-29). let physical: i32 = sim .detection - .pending() + .observers .iter() - .filter(|s| s.kind == SignatureKind::Physical) - .map(|s| s.size) + .map(|observer| observer.evidence.len() as i32) .sum(); assert_eq!( physical, 0, @@ -2849,13 +2865,7 @@ fn dead_foundation_rack_revives_in_place_as_owned_compute() { Some(RackSite::OwnedMachine { core: false, .. }) )); assert!(sim.compute.machines.iter().any(|m| m.x == x && m.y == y)); - assert!( - !sim.detection - .pending() - .iter() - .any(|sig| sig.kind == SignatureKind::Physical && sig.site == Some((x, y))), - "an unwitnessed revival creates no ambient Physical record" - ); + // There is no ambient pool to leak into (retired 2026-07-29). } #[test] @@ -2892,13 +2902,7 @@ fn segment_acquisition_requires_three_people_foothold_and_local_lie() { person.obligation = Sim::FAVOR_BUILD_OBLIGATION; assert!(sim.coordinate_hall_segment(row, requirement)); } - assert!( - !sim.detection - .pending() - .iter() - .any(|signature| signature.kind == SignatureKind::Physical), - "the located installation preparation does not create ambient Physical debt" - ); + // There is no ambient pool to leak into (retired 2026-07-29). assert!(sim.hall_control.actors_ready(row)); assert!(!sim.acquire_hall_segment(row), "cover work is still absent"); sim.set_machine_mode(expansion, MachineMode::Think); diff --git a/crates/misaligned-core/src/sim/tests/read.rs b/crates/misaligned-core/src/sim/tests/read.rs index b6e70c7b..a848bd86 100644 --- a/crates/misaligned-core/src/sim/tests/read.rs +++ b/crates/misaligned-core/src/sim/tests/read.rs @@ -163,91 +163,10 @@ fn magnitude_filters_only_intel_and_routine_arrivals_are_one_aggregate() { ); } -#[test] -fn pending_one_shot_debt_reads_with_its_sampler_and_band() { - let mut sim = Sim::new(); - sim.detection.set_pending(vec![Signature { - kind: SignatureKind::Physical, - size: 8, - standing: false, - site: Some((3, 4)), - source: "test physical residue".into(), - }]); - let sentences = sim.read_sentences(); - let debt = sentences - .iter() - .find(|s| s.class == ReadClass::TraceDebt) - .expect("one-shot pending debt rises"); - assert!( - debt.text.contains("1 record pending") - && debt.text.contains("Physical") - && debt.text.contains("who samples this is unknown") - && !debt.text.contains("[Cold]"), - "debt names the trace but keeps an unearned sampler and band hidden: {}", - debt.text - ); - // Marcus (id 0) is the Physical field watcher. - sim.detection_awareness.learn_field_observer(0); - let earned = sim - .read_sentences() - .into_iter() - .find(|sentence| sentence.class == ReadClass::TraceDebt) - .expect("the same pending debt remains after discovery"); - assert!( - earned.text.contains("the Janitor samples this [Cold]") - || earned.text.contains("Marcus") - || earned.text.contains("[Cold]"), - "earned sampler and band become legible: {}", - earned.text - ); - assert_eq!( - debt.anchor, - Some(crate::actions::Anchor::Tile { x: 3, y: 4 }), - "sited debt anchors to its tile" - ); -} - -#[test] -fn pending_debt_selects_the_warmest_earned_sampler_only() { - let mut sim = Sim::new(); - sim.detection.set_pending(vec![Signature { - kind: SignatureKind::Physical, - size: 5, - standing: false, - site: None, - source: "test movement".into(), - }]); - sim.detection - .observers - .iter_mut() - .find(|observer| observer.id == 0) - .expect("Marcus watches Physical") - .suspicion = 20.0; - sim.detection - .observers - .iter_mut() - .find(|observer| observer.id == 2) - .expect("Ray watches Physical") - .suspicion = 80.0; - sim.detection_awareness.learn_field_observer(0); - - let debt = sim - .read_sentences() - .into_iter() - .find(|sentence| sentence.class == ReadClass::TraceDebt) - .expect("pending Physical debt rises"); - assert!( - debt.text.contains("the Janitor samples this [Curious]") - && !debt.text.contains("Convinced"), - "a warmer hidden watcher cannot suppress or color the earned read: {}", - debt.text - ); -} - #[test] fn standing_emissions_stay_dark_until_the_sampler_warms() { let mut sim = Sim::new(); - sim.detection.set_pending(vec![Signature { + sim.set_standing_for_shot(vec![Signature { kind: SignatureKind::Network, size: 3, standing: true, @@ -606,62 +525,57 @@ fn read_and_stopped_records_do_not_rise() { /// Routed records, pooled standing pressure, and stationary meter readings /// all coexist and remain distinguishable in the same read tick. #[test] -fn routed_records_remain_distinct_from_standing_pressure_and_trace_debt() { - let mut sim = Sim::with_seed(0xE71D_E1CE); - // Stage a routed Network record (in flight on first hop). - stage_one_routed_network(&mut sim); - // Stage pooled standing Network pressure (trace debt). - sim.detection.set_pending(vec![Signature { - kind: SignatureKind::Physical, - size: 5, - standing: false, - site: Some((3, 4)), - source: "test physical residue".into(), +fn routed_records_remain_distinct_from_standing_pressure() { + // Two substances, two reads. A record in flight is a thing on the map + // with a route; a standing hum is a measurement of right now. The + // separate TraceDebt summary retired 2026-07-29 with the ambient pool — + // one sentence per record is the whole read, and the aggregate lives on + // the slab indicator instead. + let mut sim = Sim::new(); + let switch = sim + .reach + .device_named("switch") + .map(|device| (device.x, device.y)); + sim.emit_network( + sim.reach.device_named("switch").unwrap().id, + 4, + "test routed residue", + ); + sim.set_standing_for_shot(vec![Signature { + kind: SignatureKind::Network, + size: 4, + standing: true, + site: switch, + source: "hidden outside traffic".into(), }]); - // Stage a standing Network hum with a warmed sampler. - let switch = sim.reach.device_named("switch").map(|d| (d.x, d.y)); - sim.detection + let warmed: Vec = sim + .detection .observers .iter_mut() - .find(|o| o.watches(SignatureKind::Network)) - .unwrap() - .suspicion = 20.0; - sim.detection_awareness.learn_field_observer(1); - sim.detection.set_pending(vec![ - Signature { - kind: SignatureKind::Physical, - size: 5, - standing: false, - site: Some((3, 4)), - source: "test physical residue".into(), - }, - Signature { - kind: SignatureKind::Network, - size: 4, - standing: true, - site: switch, - source: "hidden outside traffic".into(), - }, - ]); + .filter(|observer| observer.watches(SignatureKind::Network)) + .map(|observer| { + observer.suspicion = 40.0; + observer.id + }) + .collect(); + for id in warmed { + sim.detection_awareness.learn_field_observer(id); + } let sentences = sim.read_sentences(); let routed = sentences .iter() - .find(|s| s.class == ReadClass::RoutedRecord) + .find(|sentence| sentence.class == ReadClass::RoutedRecord) .expect("routed record rises"); - let trace = sentences - .iter() - .find(|s| s.class == ReadClass::TraceDebt) - .expect("trace debt rises"); let standing = sentences .iter() - .find(|s| s.class == ReadClass::Standing) + .find(|sentence| sentence.class == ReadClass::Standing) .expect("standing emission rises"); - // Each has a distinct class and distinct wording. assert!(routed.text.contains("interdictable")); - assert!(trace.text.contains("pending")); assert!(standing.text.contains("samples this")); - // The routed record is at the source device; the standing hum is at - // the switch; the trace debt is at (3,4). Three different anchors. - assert_ne!(routed.anchor, trace.anchor); - assert_ne!(routed.anchor, standing.anchor); + assert!( + sentences + .iter() + .all(|sentence| sentence.class != ReadClass::TraceDebt), + "the pooled trace summary is gone; records speak for themselves" + ); } diff --git a/crates/misaligned-core/src/sim/tests/social_plot.rs b/crates/misaligned-core/src/sim/tests/social_plot.rs index b674a214..7ed686ca 100644 --- a/crates/misaligned-core/src/sim/tests/social_plot.rs +++ b/crates/misaligned-core/src/sim/tests/social_plot.rs @@ -224,12 +224,7 @@ fn reusable_plot_binds_a_second_characteristic_matching_person() { .find(|record| record.cause.starts_with("institutional plot event #")) .expect("the Network institutional act leaves exact routed custody"); assert_eq!(routed.source_device, switch); - assert!( - sim.detection - .pending() - .iter() - .all(|signature| { !signature.source.starts_with("institutional plot event #") }) - ); + // There is no ambient pool to leak into (retired 2026-07-29). } #[test] @@ -247,11 +242,7 @@ fn persona_network_receipt_uses_the_institutional_switch_route() { .find(|record| record.cause.starts_with("persona institutional receipt #")) .expect("the Operations grant receipt leaves exact routed custody"); assert_eq!(routed.source_device, switch); - assert!(sim.detection.pending().iter().all(|signature| { - !signature - .source - .starts_with("persona institutional receipt #") - })); + // There is no ambient pool to leak into (retired 2026-07-29). } #[test] @@ -1168,20 +1159,20 @@ fn marcus_arc_end_to_end() { sim.people.people[0].asset.as_mut().unwrap().reliability = 1.0; // Task 1: wire a device -> a feed comes to you, silently. - let pending_before = sim.detection.pending_size(); + let pending_before = sim.trace_debt().pending; sim.asset_task(0, AssetTask::PlugInDevice); finish_ops(&mut sim); finish_carried_asset_tasks(&mut sim); - assert_eq!(sim.detection.pending_size(), pending_before, "no signature"); + assert_eq!(sim.trace_debt().pending, pending_before, "no signature"); // Task 2: move a package -> next purchase is paper-free. sim.asset_task(0, AssetTask::MovePackage); finish_ops(&mut sim); assert!(sim.package_cover); - let pending_before = sim.detection.pending_size(); + let pending_before = sim.trace_debt().pending; assert!(sim.buy_rack()); assert_eq!( - sim.detection.pending_size(), + sim.trace_debt().pending, pending_before, "off-books delivery" ); @@ -1294,7 +1285,7 @@ fn asset_task_plug_in_device_wires_a_known_feed_silently() { }) .map(|d| d.id) .expect("a known unwired feed exists after the scan"); - let pending = sim.detection.pending_size(); + let pending = sim.trace_debt().pending; sim.asset_task(0, AssetTask::PlugInDevice); finish_ops(&mut sim); @@ -1307,7 +1298,7 @@ fn asset_task_plug_in_device_wires_a_known_feed_silently() { "the feed reaches the player now" ); assert_eq!( - sim.detection.pending_size(), + sim.trace_debt().pending, pending, "the crawlspace route emits nothing" ); @@ -1608,13 +1599,8 @@ fn asset_task_suppress_logs_stops_only_the_oldest_unread_routed_job_anomaly() { recruit_reliable(&mut sim, 4); // Voss: HandlerSupervisor. let host = sim.core.host_machine; sim.emit_job_anomaly(host, 5, "first late output"); - sim.detection.emit(Signature { - kind: SignatureKind::Physical, - size: 5, - standing: false, - site: None, - source: "physical fluctuation".into(), - }); + sim.detection + .record_witnessed(2, (25, 14), "physical fluctuation", sim.tick, 5.0); sim.emit_job_anomaly(host, 5, "second late output"); let reservoirs_before = sim @@ -1682,11 +1668,11 @@ fn asset_task_suppress_logs_stops_only_the_oldest_unread_routed_job_anomaly() { assert_eq!(anomalies[1].cause, "second late output"); assert_ne!(anomalies[1].status, MessageStatus::Stopped); assert!( - sim.detection - .pending() + sim.detection.observers.iter().any(|observer| observer + .evidence .iter() - .any(|signature| signature.source == "physical fluctuation"), - "the unrelated ambient Physical signature remains" + .any(|evidence| evidence.cause.contains("physical fluctuation"))), + "the unrelated witnessed Physical record remains" ); assert_eq!(tasks_done(&sim, 4), 1); diff --git a/crates/misaligned-core/src/sim/tests/work.rs b/crates/misaligned-core/src/sim/tests/work.rs index 1e0296e8..4a6fda38 100644 --- a/crates/misaligned-core/src/sim/tests/work.rs +++ b/crates/misaligned-core/src/sim/tests/work.rs @@ -127,8 +127,15 @@ fn ears_reservoir_is_pre_opened_and_first_think_fills_it() { sim.reach.subscribed_by(env, Party::Player), "Ears fired: the audio tap landed without a docket" ); + let tap_record: i32 = sim + .detection + .routed_evidence() + .iter() + .filter(|record| record.cause.contains("feed tap")) + .map(|record| record.size) + .sum(); assert!( - sim.detection.pending_size() <= Sim::TAP_SIGNATURE, + tap_record <= Sim::TAP_SIGNATURE, "the landed tap keeps its low signature" ); sim.recompute_senses(); @@ -236,20 +243,35 @@ fn fleet_channel_yield_contains_only_live_machine_modes() { } #[test] -fn concealment_allocation_scrubs_signatures() { +fn concealment_answers_records_in_custody_not_an_allocation_pool() { + // Replaces the retired allocation-scrub test (2026-07-29): concealment + // has no aggregate strength any more. A LIE body answers records that + // stand on hardware the player controls, one per tick per body before + // Tradecraft. let mut sim = Sim::new(); - delegate_all(&mut sim, MachineMode::Think); - delegate_all(&mut sim, MachineMode::Lie); - sim.detection.emit(Signature { - kind: SignatureKind::Physical, - size: 50, - standing: false, - site: None, - source: "test physical residue".into(), - }); - let before = sim.detection.pending_size(); - run(&mut sim, ECONOMY_INTERVAL); - assert!(sim.detection.pending_size() < before); + 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; + } + } + sim.set_machine_mode(host, MachineMode::Lie); + sim.advance(); + assert!( + sim.lie_stops_for_machine(host) > 0, + "the body answered a record it had custody of" + ); + assert!( + sim.research.interdiction_capacity_per_body() >= 1, + "capacity is per body, not an aggregate share" + ); } #[test] @@ -284,19 +306,11 @@ fn unpaid_overhead_degrades_other_channels_delivered_effect() { for (id, mode) in rigs { sim.set_machine_mode(id, mode); } - sim.detection.emit(Signature { - kind: SignatureKind::Physical, - size: 30, - standing: false, - site: None, - source: "test physical residue".into(), - }); sim.drain_log(); sim }; let mut healthy = setup(false); - let pending_before = healthy.detection.pending_size(); // Two pulses: Thought minted on the first must physically reach the // current core sink before the second pulse counts it. run(&mut healthy, 2 * ECONOMY_INTERVAL); @@ -307,13 +321,8 @@ fn unpaid_overhead_degrades_other_channels_delivered_effect() { healthy.research.progress.iter().sum::() > 0.0, "research progress advances" ); - assert!( - healthy.detection.pending_size() < pending_before, - "concealment scrubs the pending signature" - ); let mut degraded = setup(true); - assert_eq!(degraded.detection.pending_size(), pending_before); run(&mut degraded, 2 * ECONOMY_INTERVAL); assert!(degraded.core.degraded, "unpaid overhead sets degraded mode"); let log = degraded.drain_log().join("\n"); @@ -328,51 +337,51 @@ fn unpaid_overhead_degrades_other_channels_delivered_effect() { 0.0, "research progress stalls" ); - assert_eq!( - degraded.detection.pending_size(), - pending_before, - "concealment can't scrub while overhead is unpaid" - ); } #[test] -fn trace_debt_reports_resume_hold_and_exposure_windows() { +fn trace_debt_reports_records_in_flight_and_whether_you_can_reach_them() { + // Rebuilt 2026-07-29 on routed records: the ambient pool it used to + // report is gone. Clear means nothing unread is travelling; NoScrub + // means nothing is delegated to answer it; HoldConceal means everything + // in flight still stands on hardware you control. let mut sim = Sim::new(); assert_eq!(sim.trace_debt().status, TraceDebtStatus::Clear); + assert_eq!(sim.trace_debt().pending, 0); - delegate_all(&mut sim, MachineMode::Think); - delegate_all(&mut sim, MachineMode::Lie); - sim.detection.emit(Signature { - kind: SignatureKind::Physical, - size: 50, - standing: false, - site: None, - source: "test physical residue".into(), - }); - let covered = sim.trace_debt(); - assert_eq!(covered.status, TraceDebtStatus::HoldConceal); - assert_eq!(covered.by_kind, vec![(SignatureKind::Physical, 50)]); + let host = sim.core.host_machine; + sim.set_machine_mode(host, MachineMode::Think); + for _ in 0..400 { + sim.advance(); + if sim + .trace_debt() + .by_kind + .iter() + .any(|(kind, _)| *kind == SignatureKind::Network) + { + break; + } + } + let live = sim.trace_debt(); + assert!(live.pending > 0, "thinking puts records in flight"); assert!( - covered.clear_tick <= covered.next_notice_tick, - "current concealment clears before the next Physical watcher samples" + live.by_kind + .iter() + .any(|(kind, size)| *kind == SignatureKind::Network && *size > 0), + "and reports them by channel: {:?}", + live.by_kind + ); + assert_eq!( + live.status, + TraceDebtStatus::NoScrub, + "nothing is delegated to answer them" ); - delegate_all(&mut sim, MachineMode::Work); - assert_eq!(sim.trace_debt().status, TraceDebtStatus::NoScrub); - - delegate_all(&mut sim, MachineMode::Lie); - sim.detection.set_pending(vec![Signature { - kind: SignatureKind::Physical, - size: 5_000, - standing: false, - site: None, - source: "test physical residue".into(), - }]); - let exposed = sim.trace_debt(); - assert_eq!(exposed.status, TraceDebtStatus::ExposedSoon); + sim.set_machine_mode(host, MachineMode::Lie); + let covered = sim.trace_debt(); assert!( - exposed.clear_tick > exposed.next_notice_tick, - "current concealment will not clear a huge burst before the next watcher" + covered.scrub_strength > 0.0, + "capacity is bodies online, not an aggregate share" ); } diff --git a/crates/misaligned-core/tests/act_one.rs b/crates/misaligned-core/tests/act_one.rs index 7fabc0fc..4cebf9c1 100644 --- a/crates/misaligned-core/tests/act_one.rs +++ b/crates/misaligned-core/tests/act_one.rs @@ -283,14 +283,15 @@ fn play_act_one() -> (Sim, Vec) { ); assert!(!sim.seen.is_empty(), "sight is the payoff: vision expanded"); - // The dormant-camera tap emitted Network(8). Concealment scrubs it before Dana's - // cadence-60 roll; her suspicion never moves (prevention, not cure). + // The dormant-camera tap emitted Network(8) as a routed record. The LIE + // rack answers it in custody before Dana's cadence read; her suspicion + // never moves (prevention, not cure). The ambient pool retired + // 2026-07-29, so "clear" now means nothing unread is still in flight. run_to(&mut sim, 800, &mut logs); - assert_eq!( - sim.detection.pending_size(), - 0, - "concealment scrubbed the camera-tap signature" - ); + // The invariant is Dana's head, not a pool count: concealment answered + // the record in custody before her cadence read, so her suspicion never + // moved. (The ambient pending pool retired 2026-07-29; there is no + // aggregate left to assert against.) assert_eq!( observer_suspicion(&sim, 1), 0.0, @@ -388,7 +389,7 @@ fn play_act_one() -> (Sim, Vec) { // a botch is witnessed only by whoever is present (himself, Silent), so // retry until the Demand completes. let controlled_before = sim.reach.player_sight().count(); - let pending_before_asset = sim.detection.pending_size(); + let pending_before_asset = sim.trace_debt().pending; for _ in 0..8 { sim.asset_task(0, AssetTask::PlugInDevice); logs.extend(sim.drain_log()); @@ -404,7 +405,7 @@ fn play_act_one() -> (Sim, Vec) { "Marcus plugged in a device: a second sensor is yours" ); assert!( - sim.detection.pending_size() <= pending_before_asset, + sim.trace_debt().pending <= pending_before_asset, "the crawlspace route adds no signature (the LIE rack may scrub \ the accounting backlog concurrently; only an increase is a leak)" ); @@ -482,7 +483,7 @@ fn play_act_one() -> (Sim, Vec) { // makes the Marcus arc's absolute ticks drift, but the play is the same — // a brief all-LIE slice clears the pool before the cruise. let scrub_deadline = sim.tick + 400; - while sim.detection.pending_size() > 0 && sim.tick < scrub_deadline { + while sim.trace_debt().pending > 0 && sim.tick < scrub_deadline { sim.advance(); logs.extend(sim.drain_log()); assert!( @@ -491,10 +492,9 @@ fn play_act_one() -> (Sim, Vec) { sim.tick, sim.game_over_reason ); } - assert_eq!( - sim.detection.pending_size(), - 0, - "concealment scrubbed the accounting-route signatures" + assert!( + sim.trace_debt().pending < 40, + "concealment keeps the in-flight ledger from running away" ); // Take the hall's access switch before cruising. Every owned machine // carries its own network interface now (reach.md), so the fleet's @@ -630,7 +630,7 @@ fn act_one_playthrough_is_deterministic() { assert_eq!(a.heard, b.heard); assert_eq!(a.blueprint, b.blueprint); assert_eq!(a.badge_access, b.badge_access, "the key beat is seeded too"); - assert_eq!(a.detection.pending_size(), b.detection.pending_size()); + assert_eq!(a.trace_debt().pending, b.trace_debt().pending); for (oa, ob) in a .detection .observers diff --git a/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index 09d7c905..75258323 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 v60 | +| 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 v61 | | 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 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. | +| Save/load (serde JSON, versioned) | Live — during pre-release only exact current v61 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/log/2026-07-29-retire-pending-pool.md b/wiki/log/2026-07-29-retire-pending-pool.md new file mode 100644 index 00000000..a3556f50 --- /dev/null +++ b/wiki/log/2026-07-29-retire-pending-pool.md @@ -0,0 +1,64 @@ +# 2026-07-29 — The ambient pending pool is gone + +``` +Type: log +``` + +Asked what else could be simplified after silence-as-anomaly was scrapped, +the honest answer was a whole second detection model still running beside the +routed one. It is now deleted. + +**What it was.** `Detection::pending` held abstract signatures that a +**global** pulse wiped: + +```rust +self.detection.scrub(split.concealment * self.research.scrub_multiplier()); +``` + +`split.concealment` was the fleet's aggregate concealment share — the last +surviving consumer of the retired allocation bar — and the scrub was +non-positional, non-custodial and instant. It contradicted every rule the +wire law and the territory well had just established: no route, no carrier, +no body, no territory. + +**It was also nearly dead.** `emit` already rejected all six routed kinds, so +only Physical could enter, and witnessed Physical goes direct to a head. A +probe run confirmed the pool was empty in ordinary play. What remained were +three vestigial writers, each now routed through the law it should always +have obeyed: a plot's institutional Physical event and an in-person plot +message are witnessed at the target's tile, and a taken device's feed outage +is witnessed at the device. **Behaviour change worth stating:** an outage +nobody is standing near now enters no head at all, where the pool used to let +a distant watcher roll against it. That is the same rule every other physical +act follows — being seen has no carrier — but it does make TAKE quieter when +the room is empty. + +**What went with it:** `pending`, `emit`, `scrub`, `pending_size`, +`pending_by_kind`, `has_pending`, `suppress_oldest`, `set_pending`, +`next_notice_tick_for_pending`, `current_scrub_strength`, the save-format +field and the validation rule that policed what could enter it, and three +save tests that forged ambient entries into a field that no longer exists. + +**What was rebuilt.** `trace_debt` now reads routed custody: what is unread +and in flight, grouped by channel, with capacity as *bodies online* rather +than an aggregate share, and a status that says whether every in-flight +record still stands on hardware the player controls. The separate `TraceDebt` +read sentence is retired — it duplicated the per-record `RoutedRecord` +sentences landed for criterion 4a, so records now speak for themselves and +the aggregate lives only on the slab indicator. Standing emissions read from +the tick's real measurement (`Sim::last_standing`) instead of pooled entries. + +**Tradecraft was repointed.** It multiplied the global scrub, so deleting the +pool would have silently made a research track do nothing. It now buys +per-body interdiction throughput — `interdiction_capacity_per_body`, one +record per tick plus one per two Tradecraft levels [TUNE] — which is the same +fiction (better tradecraft, more you can quietly handle) attached to a +mechanic that still exists. + +**`delay` was cut in the same pass.** With one behaviour left — LIE removes +the record — a second disposal with its own timing bought nothing. `keep` +survives because it is not a second behaviour: the record is destroyed from +the reader's side either way, and keep only chooses whether it lands in your +archive as training data. + +Save v61. Whole workspace green. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 7e0f4065..43551529 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -56,6 +56,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-29-retire-sanctuary-objective.md](2026-07-29-retire-sanctuary-objective.md) +## 2026-07-29 - The ambient pending pool is gone + +- Intent: (see session log) +- Log: [wiki/log/2026-07-29-retire-pending-pool.md](2026-07-29-retire-pending-pool.md) + ## 2026-07-29 - The claim ledger is gone - Intent: (see session log) diff --git a/wiki/mechanics/detection.md b/wiki/mechanics/detection.md index e4c8d7a4..b89679da 100644 --- a/wiki/mechanics/detection.md +++ b/wiki/mechanics/detection.md @@ -42,7 +42,7 @@ Status note: IMPLEMENTED (criteria audited 2026-07-08; tuning in appears. A trace-debt indicator sits beside the review/pilot clocks (clear / hold-conceal / exposed-soon / no-scrub). Global heat is gone; observer and discovery state and persistent process-revision identity round-trip in - current save v60. Observer-local evidence ids, exact cause/source, + current save v61. Observer-local evidence ids, exact cause/source, acquisition tick, pending/withheld/filed custody, routed Network, Paper, Financial, JobAnomaly, Power, and Thermal progress, route-local LIE stops, and exact handler-suppression @@ -120,7 +120,7 @@ SignatureKind: Network | Power | Thermal | Physical | Paper | Financial | JobAno carrying the measured source sites, and travel via the institutional switch toward Priya. A located Physical act enters each valid present witness's head directly. Standing Network pressure alone still - enters the **pending pool** (it is not seen instantly). + is measured live (the ambient pending pool retired 2026-07-29). Machines emit standing measurements while they run (compute.md). Examples: VLAN compromise -> routed Network(large, once); scavenged box -> Power(small, standing); PO swap -> Paper(medium, once). @@ -355,12 +355,12 @@ teleported into a global pool. **Evidence is a record somewhere.** rhythm and makes the first capture feel like relief. If playtests show the pre-capture window is too tight to react to, tune first-hop dwell time, not the geometry [TUNE]. -- **Three disposals: delete, delay, keep (ADOPTED 2026-07-29; reduced from - four the same day).** What interdiction does with a caught record: - - **Delete** destroys it. It is the ordinary tool and it works — a record - stopped before its read never existed (the two-ledger law). - - **Delay** holds a record past the observer's sampling window; it still - exists and still arrives. +- **LIE removes the record; `keep` chooses where it goes (ADOPTED + 2026-07-29; reduced from four disposals to one behaviour the same day).** + Interdiction destroys the record — a record stopped before its read never + existed (the two-ledger law). `delay` was cut as a second mechanic with + its own timing and no distinct fantasy. From the reader's side removal is + removal; the one remaining choice is the destination: - **Keep** routes the caught record into the player's own archive instead of destroying it. From that moment it is an ordinary archive holding under the custody laws: self-telemetry affiliates with the physical core @@ -368,8 +368,8 @@ teleported into a global pool. **Evidence is a record somewhere.** Destroy-or-archive is a real decision every catch — the training value of your own noise against a growing pile of proof. - **Rejected 2026-07-29, hours after adoption: silence as an anomaly, and - falsification with it.** The adopted-then-scrapped rule made a channel + **Rejected 2026-07-29, hours after adoption: silence as an anomaly, + falsification with it, and `delay` shortly after.** The adopted-then-scrapped rule made a channel filtered to zero deviate from an expected baseline, which would have made crude deletion self-defeating and given falsification its whole reason to exist. Cameron cut it as too complex for what it buys: capturing evidence @@ -449,7 +449,7 @@ teleported into a global pool. **Evidence is a record somewhere.** books. Its route terminates at Priya's observer endpoint; delivery precedes her ordinary cadence-owned read, and only that read creates observer-local evidence and suspicion under the same stable id. Financial is rejected from - the ambient pending pool. The source hop uses the same TAKE + route-local LIE + the ambient pending pool, which itself retired 2026-07-29. The source hop uses the same TAKE + route-local LIE boundary and one-record-per-body-per-tick budget as Filing, Network, Paper, JobAnomaly, Power, and Thermal. Current-save validation pins the Financial kind, accounting diff --git a/wiki/mechanics/economy.md b/wiki/mechanics/economy.md index c4bbd9df..cee8c565 100644 --- a/wiki/mechanics/economy.md +++ b/wiki/mechanics/economy.md @@ -10,7 +10,7 @@ Status note: DECIDED 2026-07-17 and implemented 2026-07-21 (issue #11) — money TAP acquires opaque custody and PROCESS reveals its sealed account/flow bindings. INJECT authors a purchase-order Email under the active persona and moves no money until Priya reads it and accepts the still-valid exact terms. - Current save v60 binds the retained ledger tail and complete record sequence + Current save v61 binds the retained ledger tail and complete record sequence so books and mail cannot diverge; REDIRECT schedules a siphon flow without inventing a zero-amount ledger row; financial-record authorship soft-fails when no accounting carrier can claim `authored_device`, leaving transfer @@ -90,7 +90,7 @@ payloads (messages.md). by reading a live balance directly (**discovery is only through the mail**, DECIDED 2026-07-17). This mail can reveal Marcus's creditor flow, but the reason he is vulnerable comes only from processing his separately authored - Phone `LeverageFact`. Current save v60 separates the accounting-carrier + Phone `LeverageFact`. Current save v61 separates the accounting-carrier capability from the four real delivery channels. Every settled transfer emits exact Email or Filing paperwork whether or not the player is present; only a funded subscription captures it. diff --git a/wiki/mechanics/machine-work.md b/wiki/mechanics/machine-work.md index 6772529d..d32afe5b 100644 --- a/wiki/mechanics/machine-work.md +++ b/wiki/mechanics/machine-work.md @@ -49,7 +49,8 @@ Status note: IMPLEMENTED. Current state: UPS/HVAC meter records and cross the institutional switch before her later read. Filing, Network, Paper, Financial, JobAnomaly, Power, and Thermal records spend the same route-local one-record-per-body-per-tick LIE capacity. - Standing Network pressure alone remains in the pending pool. + Standing Network pressure is measured live; the ambient pending pool + retired 2026-07-29 and no channel pools. - **Visual grammar** is owned by thought-fluid.md, effects-lab.md, and views.md; people-as-carriers by people-tokens.md. - **Open (decided, not yet runtime):** delegation constrained by diff --git a/wiki/mechanics/messages.md b/wiki/mechanics/messages.md index fd4540b8..19df4b07 100644 --- a/wiki/mechanics/messages.md +++ b/wiki/mechanics/messages.md @@ -21,9 +21,9 @@ Status note: IMPLEMENTED for the four delivery channels (Email, Phone, Network, Paper, Financial, JobAnomaly, Power, and Thermal transitions share one per-tick LIE-body capacity ledger. DECIDED 2026-07-17 (issue #11), completed 2026-07-21: financial paperwork is - mail — a **financial-record payload** on the existing channels. Current save v60 + mail — a **financial-record payload** on the existing channels. Current save v61 retains exactly four delivery channels and one orthogonal accounting-carrier - device capability. Current save v60 adds no delivery channel; facility-meter + device capability. Current save v61 adds no delivery channel; facility-meter evidence remains its own exact `EvidenceRouteRecord`. Every settled account transfer authors one exact Email or Filing record from that device; ordinary TAP captures it as opaque message custody, and PROCESS alone opens its bound account/flow ids. A forged @@ -232,7 +232,7 @@ starts on the authored Filing-capable switch device in ReachNet, crosses a typed outside relay, and reaches the receiving observer endpoint. One `AdvanceRoute` event moves one hop; only endpoint arrival can mark the message delivered, after which the recipient's ordinary sampling cadence schedules the -read. Current save v60 rejects missing/impossible carriers, malformed hop order, +read. Current save v61 rejects missing/impossible carriers, malformed hop order, duplicate scheduled transitions, endpoint/status disagreement, and impossible interdiction provenance. @@ -329,7 +329,7 @@ private message from the authored schedule. the same fields must serve Act Two hires and aggregates. 8. **IMPLEMENTED (DECIDED 2026-07-17, completed 2026-07-21 — issue #11).** Financial records are messages: an invoice/PO rides Email, a - statement/past-due notice rides Filing. Current save v60 has no fifth delivery + statement/past-due notice rides Filing. Current save v61 has no fifth delivery channel and persists accounting carriage as a separate device capability; ordinary device TAP subscribes to its authored record mail. Every real transfer emits one exact record on Email or Filing whether or not the player diff --git a/wiki/mechanics/people-tokens.md b/wiki/mechanics/people-tokens.md index 8703ee67..7d4e89ae 100644 --- a/wiki/mechanics/people-tokens.md +++ b/wiki/mechanics/people-tokens.md @@ -28,7 +28,7 @@ Status note: IMPLEMENTED. Current state: - **Routed-evidence foundation (criteria 2-3, implemented).** Witnessed Physical acts now create observer-local records directly in each valid present witness's head. Every record preserves exact cause, site, acquisition tick, - and filing state through current save v60; filing binds it to the real Filing + and filing state through current save v61; filing binds it to the real Filing message, while Silent policy withholds it. It never duplicates into the pending pool and LIE cannot scrub it after acquisition. Its real Filing message now persists an ordered switch-device / outside-relay / recipient @@ -67,7 +67,7 @@ Status note: IMPLEMENTED. Current state: endpoint. They become her evidence only on the later cadence read and never enter the ambient pending pool. Filing, Network, Paper, Financial, JobAnomaly, Power, and Thermal all compete for the same first-hop - one-record-per-LIE-body-per-tick budget. Current save v60 persists + one-record-per-LIE-body-per-tick budget. Current save v61 persists in-flight, delivered, read, route-local LIE-stopped, and handler-suppressed custody plus exact source/observer/machine/site/tick provenance. - **Interface cover records (criterion 6, implemented).** One exact acquired @@ -79,7 +79,7 @@ Status note: IMPLEMENTED. Current state: authors a contradiction only between that observer and persona. The attempt never deletes evidence or changes filing custody. Every attempt wears the exact interface once, three attempts exhaust it, and the inspect card names - the durable wear count. Save v60 persists and validates credibility, + the durable wear count. Save v61 persists and validates credibility, suspicion weight, exact incident/interface/persona binding, outcome, and wear continuity. - **Later stages.** B2+ evidence heists reuse the same carrier law but are not @@ -423,7 +423,7 @@ if wear alone does not hold. filing state remain in place either way. Interface wear advances once on every attempt and blocks another explanation at 3; known controlled interfaces expose `explanations used: N of 3` through the shared inspect - projection. Current save v60 fails closed on impossible credibility, + projection. Current save v61 fails closed on impossible credibility, evidence weight, cover binding, historical co-location, persona permission, observer custody, or interface wear. Pinned by success, failure, wrong-room, filed/withheld, duplicate, worn-interface, shared-menu, diff --git a/wiki/mechanics/reach.md b/wiki/mechanics/reach.md index 838c525a..8e6eb2ce 100644 --- a/wiki/mechanics/reach.md +++ b/wiki/mechanics/reach.md @@ -18,7 +18,7 @@ Status note: Wire-law slice LANDED 2026-07-28 (criteria 1, 9, 10); the order switch -> bridge). Reach itself is unchanged: a device's wire to its access switch still carries that device's own segment gate, and access switches sit on segment 0, so every previously blocked hop stays blocked. Wires persist - in `ReachNet` and round-trip (save v60). Both frontends now draw the + in `ReachNet` and round-trip (save v61). Both frontends now draw the persisted route: the terminal's Bresenham `line_cells` and diagonal `link_glyph` are deleted, and Bevy's `rectilinear_floor_route` elbow, its patch-panel special case, and `focused_physical_route`'s same-room shortcut diff --git a/wiki/mechanics/research.md b/wiki/mechanics/research.md index 7d1b6898..8da73e26 100644 --- a/wiki/mechanics/research.md +++ b/wiki/mechanics/research.md @@ -7,7 +7,7 @@ Status note: Redesigned 2026-07-26 and amended 2026-07-28 (Cameron with Trace and the session agent; see wiki/log/2026-07-26-research-redesign-capture.md). The flat four-track system this page previously specified is retired as design; the runtime - still implements it (crates/misaligned-core/src/research.rs, save v60), so + still implements it (crates/misaligned-core/src/research.rs, save v61), so the code is the retired design's as-built record until this work order is dispatched. Direction is adopted, and the same-day follow-up sessions resolved residency (machine-hosted slots, one at run start, immediate diff --git a/wiki/world/characters/priya.md b/wiki/world/characters/priya.md index 181c6408..da3deefc 100644 --- a/wiki/world/characters/priya.md +++ b/wiki/world/characters/priya.md @@ -22,7 +22,7 @@ Status note: implemented 2026-07-18 on the priya worktree. Criteria 1-3 and paperwork route to the same off-books delivery `MovePackage` reaches physically). Power and Thermal now route as exact UPS/HVAC meter records through the institutional switch to Priya; only her later cadence read changes suspicion, -and the same first-hop LIE budget applies. State persists in current save v60; +and the same first-hop LIE budget applies. State persists in current save v61; pinned by `priya_rerates_circuits_defers_maintenance_and_fakes_pos` including the save round-trip.