From 1d04e78b3324e1f0c37ee721e7919ad83e5a40bb Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 19 Jul 2026 00:56:24 -0700 Subject: [PATCH] Make Filing custody interceptable before recipient read. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Persist each institutional Filing across its real carrier, relay, and recipient hops so TAP can observe opaque custody while route-local LIE may stop one unread record without erasing acquired evidence. πŸ‘Ύ Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- CLAUDE.md | 2 +- crates/misaligned-core/src/messages.rs | 88 +++- .../src/operations_projection.rs | 12 +- crates/misaligned-core/src/reach.rs | 34 ++ crates/misaligned-core/src/save.rs | 391 +++++++++++++++++- crates/misaligned-core/src/schedule.rs | 10 + .../misaligned-core/src/sim/communications.rs | 195 ++++++++- .../src/sim/tests/communications.rs | 309 +++++++++++++- wiki/engineering/current-build.md | 6 +- wiki/engineering/flow-substrate.md | 10 +- ...026-07-19-filing-route-lie-interdiction.md | 51 +++ wiki/log/DEVLOG.md | 5 + wiki/log/decisions.md | 2 + wiki/log/decisions/2026-07-19.md | 21 + wiki/mechanics/detection.md | 15 +- wiki/mechanics/messages.md | 67 ++- wiki/mechanics/people-tokens.md | 37 +- wiki/mechanics/reach.md | 8 +- wiki/mechanics/sim-mechanics.md | 5 + wiki/world/story/opening.md | 2 +- 20 files changed, 1202 insertions(+), 68 deletions(-) create mode 100644 wiki/log/2026-07-19-filing-route-lie-interdiction.md create mode 100644 wiki/log/decisions/2026-07-19.md diff --git a/CLAUDE.md b/CLAUDE.md index e03fc613..a66cf066 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 v42; + machine modes, not current player assignments. Save format is currently v43; 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-core/src/messages.rs b/crates/misaligned-core/src/messages.rs index 228ed62c..7167293c 100644 --- a/crates/misaligned-core/src/messages.rs +++ b/crates/misaligned-core/src/messages.rs @@ -162,6 +162,8 @@ pub enum MessageStatus { Sent, /// Arrived on the channel; waiting for the recipient's read condition. Delivered, + /// A controlled LIE machine stopped the unread route before delivery. + Stopped, /// The recipient read it and the payload's effects have landed. Read, } @@ -171,6 +173,7 @@ impl MessageStatus { match self { MessageStatus::Sent => "sent", MessageStatus::Delivered => "delivered", + MessageStatus::Stopped => "stopped", MessageStatus::Read => "read", } } @@ -184,8 +187,71 @@ pub enum MessageOrigin { Reply, } +/// One exact custody point on an institutional Filing route. The first hop +/// is a real ReachNet device; the relay and recipient are typed message-graph +/// nodes rather than invented ReachNet hardware. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum MessageRouteHop { + Device(u32), + InstitutionalRelay, + ObserverEndpoint(u8), +} + +/// Provenance for one pre-read concealment act. This is not another evidence +/// ledger: it records why the ordinary message route terminated. +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct MessageInterdiction { + pub machine_id: u32, + pub tick: u64, +} + +/// Persisted Filing custody. `current_hop` advances only through scheduled +/// `MessageEvent::AdvanceRoute` events; delivery begins only at the terminal +/// endpoint. One optional interdiction preserves the exact machine and tick +/// that stopped the still-unread record. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct MessageRoute { + pub hops: Vec, + pub current_hop: usize, + pub interdiction: Option, +} + +impl MessageRoute { + pub fn current(&self) -> Option<&MessageRouteHop> { + self.hops.get(self.current_hop) + } + + pub fn at_endpoint(&self) -> bool { + self.current_hop + 1 == self.hops.len() + } + + /// Opaque route state for captured-but-unprocessed information. The + /// carrier topology and player's own LIE result are knowable; observer + /// identities remain hidden until processing. + pub fn opaque_status(&self, status: MessageStatus) -> String { + if let Some(stopped) = self.interdiction { + return format!( + "stopped by LIE on M{} before recipient read at tick {}", + stopped.machine_id, stopped.tick + ); + } + match status { + MessageStatus::Sent => match self.current() { + Some(MessageRouteHop::Device(_)) => "on intercepted device".into(), + Some(MessageRouteHop::InstitutionalRelay) => "in outside relay".into(), + Some(MessageRouteHop::ObserverEndpoint(_)) => "with recipient".into(), + None => "route unavailable".into(), + }, + MessageStatus::Delivered => "waiting unread with recipient".into(), + MessageStatus::Stopped => "stopped before recipient read".into(), + MessageStatus::Read => "read by recipient".into(), + } + } +} + /// One persisted message. In-flight messages are those whose status is not -/// `Read`; read messages remain as thread/history/provenance. +/// terminal (`Read` or `Stopped`); terminal messages remain as +/// thread/history/provenance. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Message { pub id: u64, @@ -206,6 +272,9 @@ pub struct Message { pub captured: bool, /// Thread parent for replies. pub reply_to: Option, + /// Exact institutional custody route for Filing traffic. Other channels + /// use their established delivery/read schedule directly. + pub route: Option, } impl Message { @@ -214,10 +283,18 @@ impl Message { } pub fn state_line(&self) -> String { - let timing = match (self.delivered_tick, self.read_tick) { - (_, Some(t)) => format!("read t{t}"), - (Some(t), None) => format!("delivered t{t}"), - (None, None) => format!("sent t{}", self.sent_tick), + let timing = match self.status { + MessageStatus::Stopped => self + .route + .as_ref() + .and_then(|route| route.interdiction) + .map(|stopped| format!("stopped t{}", stopped.tick)) + .unwrap_or_else(|| format!("sent t{}", self.sent_tick)), + _ => match (self.delivered_tick, self.read_tick) { + (_, Some(t)) => format!("read t{t}"), + (Some(t), None) => format!("delivered t{t}"), + (None, None) => format!("sent t{}", self.sent_tick), + }, }; format!( "{} Β· {} Β· {}", @@ -233,6 +310,7 @@ impl Message { #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum MessageEvent { Deliver(u64), + AdvanceRoute(u64), Read(u64), } diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index 14814033..0432665a 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -21,7 +21,7 @@ use crate::detection::Band; use crate::income::EgressRoute; use crate::intel::{ IntelCustodyKind, IntelKind, IntelPolicyMatch, IntelPolicyOutcome, IntelPolicyRule, - IntelRoutineClass, ProcessedIntel, RawIntelEvent, ReportLotToken, + IntelRoutineClass, ProcessedIntel, RawIntelEvent, RawIntelKind, ReportLotToken, }; use crate::messages::{MessageChannel, MessageOrigin, MessagePayload, MessageStatus}; use crate::person::{Knowledge, Leverage}; @@ -1168,6 +1168,16 @@ impl Sim { Some(room) => format!("captured at: {room}"), None => "captured at: intercepted channel".into(), }); + if let RawIntelKind::Message { message_id, .. } = &recording.kind + && let Some(message) = self + .messages + .iter() + .find(|message| message.id == *message_id) + && let Some(route) = &message.route + { + facts.push("route: intercepted device -> outside relay -> recipient".into()); + facts.push(format!("current: {}", route.opaque_status(message.status))); + } OperationsObject { learned_result: None, consequence: None, diff --git a/crates/misaligned-core/src/reach.rs b/crates/misaligned-core/src/reach.rs index e0c33da9..854271cc 100644 --- a/crates/misaligned-core/src/reach.rs +++ b/crates/misaligned-core/src/reach.rs @@ -593,6 +593,40 @@ impl ReachNet { self.reach().contains(&id) } + /// Whether two exact devices are joined through open FlowGraph edges and + /// every device on that path is controlled by `who`. TAP membership is + /// deliberately irrelevant: observation is not route authority. + pub fn controlled_path(&self, from: u32, to: u32, who: Party) -> bool { + if self + .device(from) + .is_none_or(|device| device.controller != who) + || self + .device(to) + .is_none_or(|device| device.controller != who) + { + return false; + } + let mut seen = BTreeSet::from([from]); + let mut pending = vec![from]; + while let Some(current) = pending.pop() { + if current == to { + return true; + } + for next in self.graph.neighbors(current, |gate| self.gate_open(gate)) { + if seen.contains(&next) + || self + .device(next) + .is_none_or(|device| device.controller != who) + { + continue; + } + seen.insert(next); + pending.push(next); + } + } + false + } + /// Gate for digital actions: Ok, or why not β€” unknown, a blocking /// segment, or an air gap (reach.md criterion 2). pub fn check_reach(&self, id: u32) -> Result<(), ReachBlock> { diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index f77eadb7..3be69857 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -19,7 +19,10 @@ use crate::income::Income; use crate::intel::{IntelPolicyLedger, IntelStream, ProcessedIntel, RawIntelEvent, RawIntelKind}; use crate::intents::{BuildIntent, BuildRouteBinding, IntentKind, IntentStatus}; use crate::machine::Compute; -use crate::messages::{Message, MessageEvent}; +use crate::messages::{ + Message, MessageChannel, MessageEndpoint, MessageEvent, MessageOrigin, MessagePayload, + MessageRouteHop, MessageStatus, +}; use crate::objective::ObjectiveState; use crate::person::{CarriedAssetTask, People}; use crate::persona::{PersonaMind, PersonaWorld}; @@ -39,13 +42,12 @@ const SAVE_BACKUP_SUFFIX: &str = ".bak"; /// renames into place. const SAVE_TEMP_SUFFIX: &str = ".tmp"; -/// Save format version. v42 persists observer-local witnessed evidence with -/// exact cause, source site, acquisition tick, and filing custody. v41 -/// persists the recipe and exact procurement or repurposing binding on -/// committed build routes. +/// Save format version. v43 persists exact Filing routes and pre-read LIE +/// interdiction provenance. v42 persists observer-local witnessed evidence +/// with exact cause, source site, acquisition tick, and filing custody. /// Bump for every schema change; during pre-release, old development state is /// refused instead of carried through compatibility shims. -pub const SAVE_VERSION: u32 = 42; +pub const SAVE_VERSION: u32 = 43; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -450,6 +452,7 @@ fn parse_save(content: &str) -> Result { fn validate_current_save(mut state: SaveState) -> Result { state.reach.validate_subscriptions()?; + validate_messages(&state)?; if state.process_revision != ProcessRevision::CURRENT { return Err("current-version save belongs to an unknown process revision".into()); } @@ -628,10 +631,15 @@ fn validate_current_save(mut state: SaveState) -> Result { raw.id, message_id )); } + let expected_person = if message.channel == crate::messages::MessageChannel::Filing { + None + } else { + payload.subject_person().or_else(|| message.from.person()) + }; if message.channel != *channel || message.summary != *summary || message.payload != *payload - || raw.person != payload.subject_person().or_else(|| message.from.person()) + || raw.person != expected_person { return Err(format!( "current-version save recording #{} disagrees with source message {}", @@ -655,6 +663,214 @@ fn validate_current_save(mut state: SaveState) -> Result { Ok(state) } +fn validate_messages(state: &SaveState) -> Result<(), String> { + let mut ids = HashSet::new(); + let mut max_id = 0; + let mut expected_scheduled_events = 0usize; + for message in &state.messages { + if message.id == 0 || !ids.insert(message.id) { + return Err(format!( + "current-version save has invalid or duplicate message id {}", + message.id + )); + } + max_id = max_id.max(message.id); + if message.sent_tick > state.sim_tick { + return Err(format!( + "current-version message {} was sent in the future", + message.id + )); + } + + let is_filing = message.channel == MessageChannel::Filing; + if is_filing { + let ( + MessageEndpoint::Observer(sender), + MessageEndpoint::Observer(recipient), + MessagePayload::SuspicionReport { observer, .. }, + ) = (&message.from, &message.to, &message.payload) + else { + return Err(format!( + "current-version Filing message {} has impossible endpoints or payload", + message.id + )); + }; + if observer != sender + || message.origin != MessageOrigin::Filing + || !state + .detection + .observers + .iter() + .any(|candidate| candidate.id == *sender) + || !state + .detection + .observers + .iter() + .any(|candidate| candidate.id == *recipient) + { + return Err(format!( + "current-version Filing message {} has invalid authored custody", + message.id + )); + } + let Some(route) = &message.route else { + return Err(format!( + "current-version Filing message {} has no exact route", + message.id + )); + }; + if route.hops.len() != 3 + || route.current_hop >= route.hops.len() + || !matches!(route.hops[1], MessageRouteHop::InstitutionalRelay) + || route.hops[2] != MessageRouteHop::ObserverEndpoint(*recipient) + { + return Err(format!( + "current-version Filing message {} has an invalid route shape", + message.id + )); + } + let MessageRouteHop::Device(carrier) = route.hops[0] else { + return Err(format!( + "current-version Filing message {} has no ReachNet carrier", + message.id + )); + }; + if state + .reach + .device(carrier) + .is_none_or(|device| !device.carries_message_channel(MessageChannel::Filing)) + { + return Err(format!( + "current-version Filing message {} names an impossible carrier", + message.id + )); + } + if (message.status == MessageStatus::Stopped) != route.interdiction.is_some() { + return Err(format!( + "current-version Filing message {} has inconsistent LIE custody", + message.id + )); + } + if let Some(stopped) = route.interdiction + && (route.current_hop != 0 + || stopped.tick < message.sent_tick + || stopped.tick > state.sim_tick + || state + .compute + .machines + .iter() + .all(|machine| machine.id != stopped.machine_id)) + { + return Err(format!( + "current-version Filing message {} has impossible interdiction provenance", + message.id + )); + } + } else if message.route.is_some() || message.status == MessageStatus::Stopped { + return Err(format!( + "current-version non-Filing message {} carries Filing-only custody", + message.id + )); + } + + match message.status { + MessageStatus::Sent => { + if message.delivered_tick.is_some() + || message.read_tick.is_some() + || message + .route + .as_ref() + .is_some_and(|route| route.at_endpoint()) + { + return Err(format!( + "current-version sent message {} has impossible progress", + message.id + )); + } + } + MessageStatus::Delivered => { + if message.delivered_tick.is_none() + || message.delivered_tick > Some(state.sim_tick) + || message.read_tick.is_some() + || message + .route + .as_ref() + .is_some_and(|route| !route.at_endpoint()) + { + return Err(format!( + "current-version delivered message {} has impossible progress", + message.id + )); + } + } + MessageStatus::Stopped => { + if message.delivered_tick.is_some() || message.read_tick.is_some() { + return Err(format!( + "current-version stopped message {} reached its recipient", + message.id + )); + } + } + MessageStatus::Read => { + if message.delivered_tick.is_none() + || message.read_tick.is_none() + || message.read_tick > Some(state.sim_tick) + || message.read_tick < message.delivered_tick + || message + .route + .as_ref() + .is_some_and(|route| !route.at_endpoint()) + { + return Err(format!( + "current-version read message {} has impossible progress", + message.id + )); + } + } + } + + let advance_events = state.message_schedule.count_for( + |event| matches!(event, MessageEvent::AdvanceRoute(id) if *id == message.id), + ); + let delivery_events = state + .message_schedule + .count_for(|event| matches!(event, MessageEvent::Deliver(id) if *id == message.id)); + let read_events = state + .message_schedule + .count_for(|event| matches!(event, MessageEvent::Read(id) if *id == message.id)); + let schedule_valid = match message.status { + MessageStatus::Sent if message.route.is_some() => { + expected_scheduled_events += 1; + advance_events == 1 && delivery_events == 0 && read_events == 0 + } + MessageStatus::Sent => { + expected_scheduled_events += 1; + advance_events == 0 && delivery_events == 1 && read_events == 0 + } + MessageStatus::Delivered => { + expected_scheduled_events += 1; + advance_events == 0 && delivery_events == 0 && read_events == 1 + } + MessageStatus::Stopped | MessageStatus::Read => { + advance_events == 0 && delivery_events == 0 && read_events == 0 + } + }; + if !schedule_valid { + return Err(format!( + "current-version message {} disagrees with its scheduled transition", + message.id + )); + } + } + if state.message_schedule.len() != expected_scheduled_events { + return Err("current-version message schedule contains an orphaned transition".into()); + } + if state.next_message_id <= max_id { + return Err("current-version save would reuse a message id".into()); + } + Ok(()) +} + fn validate_build_routes(state: &SaveState) -> Result<(), String> { let mut intent_ids = HashSet::new(); for intent in &state.intents { @@ -861,6 +1077,7 @@ mod tests { use super::*; use crate::detection::SignatureKind; use crate::machine::Channel; + use crate::messages::MessageInterdiction; use crate::person::{AssetKnowledge, AssetTask, Knowledge, PersonRole}; use crate::sim::Sim; use crate::work_grid::MachineMode; @@ -909,6 +1126,53 @@ mod tests { .to_string() } + fn routed_filing_state(stopped: bool) -> SaveState { + let mut sim = Sim::with_seed(0xF113); + for observer in &mut sim.detection.observers { + observer.report_policy = crate::detection::ReportPolicy::Silent; + } + let dana = sim + .detection + .observers + .iter_mut() + .find(|observer| observer.id == 1) + .unwrap(); + dana.report_policy = crate::detection::ReportPolicy::Files; + dana.suspicion = 35.0; + dana.cadence = 1; + if stopped { + let switch = sim.reach.device_named("switch").unwrap().id; + sim.reach.take(switch); + sim.reconcile_work_grid(); + sim.set_machine_mode(sim.core.host_machine, MachineMode::Think); + sim.set_machine_mode(sim.core.host_machine, MachineMode::Lie); + } + sim.advance(); + sim.detection + .observers + .iter_mut() + .find(|observer| observer.id == 1) + .unwrap() + .report_policy = crate::detection::ReportPolicy::Silent; + if stopped { + assert_eq!( + sim.work_grid.mode(sim.core.host_machine), + Some(MachineMode::Lie), + "the routed-message fixture keeps its exact LIE body online" + ); + } + sim.advance(); + assert_eq!( + sim.messages[0].status, + if stopped { + MessageStatus::Stopped + } else { + MessageStatus::Sent + } + ); + SaveState::from_sim(&sim) + } + fn characterization_fixture() -> Sim { let mut sim = Sim::with_seed(0x5eed); sim.map_mut().powered.extend([(3, 7), (11, 5), (2, 19)]); @@ -983,7 +1247,7 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "7a400d5549d7f07420afe75642e2edddef563bacc5a43a8f6daa1cdc557b3cee", + "288e31e3ec873081106936a67ad7f0f551c6d73a408a9c3c327a30da0e386f82", "intentional persisted-state changes must review and repin this baseline" ); } @@ -1762,6 +2026,109 @@ mod tests { ); } + #[test] + fn current_save_roundtrips_in_flight_and_interdicted_filing_routes() { + let in_flight = routed_filing_state(false); + let restored = parse_save(&serde_json::to_string(&in_flight).unwrap()).unwrap(); + let route = restored.messages[0].route.as_ref().unwrap(); + assert_eq!( + route.current_hop, 1, + "save/load preserves custody after the Filing reaches the outside relay" + ); + assert_eq!(route.interdiction, None); + assert!( + restored + .message_schedule + .next_tick_for(|event| matches!( + event, + MessageEvent::AdvanceRoute(id) if *id == restored.messages[0].id + )) + .is_some() + ); + + let stopped = routed_filing_state(true); + let restored = parse_save(&serde_json::to_string(&stopped).unwrap()).unwrap(); + let message = &restored.messages[0]; + let interdiction = message.route.as_ref().unwrap().interdiction.unwrap(); + assert_eq!(message.status, MessageStatus::Stopped); + assert_eq!(interdiction.machine_id, restored.core.host_machine); + assert_eq!(interdiction.tick, restored.sim_tick); + assert!(restored.message_schedule.next_tick_for(|event| matches!( + event, + MessageEvent::AdvanceRoute(id) | MessageEvent::Deliver(id) | MessageEvent::Read(id) + if *id == message.id + )).is_none()); + } + + #[test] + fn current_save_rejects_impossible_filing_route_and_interdiction_provenance() { + let mut state = routed_filing_state(false); + state.messages[0].route.as_mut().unwrap().current_hop = 3; + let err = parse_save(&serde_json::to_string(&state).unwrap()).unwrap_err(); + assert!( + err.contains("invalid route shape"), + "a route cannot resume beyond its endpoint: {err}" + ); + + let mut state = routed_filing_state(false); + state.messages[0].route.as_mut().unwrap().hops[0] = MessageRouteHop::Device(u32::MAX); + let err = parse_save(&serde_json::to_string(&state).unwrap()).unwrap_err(); + assert!( + err.contains("impossible carrier"), + "the saved route must bind its real Filing device: {err}" + ); + + let mut state = routed_filing_state(false); + state.messages[0].route.as_mut().unwrap().interdiction = Some(MessageInterdiction { + machine_id: state.core.host_machine, + tick: state.sim_tick, + }); + let err = parse_save(&serde_json::to_string(&state).unwrap()).unwrap_err(); + assert!( + err.contains("inconsistent LIE custody"), + "interdiction provenance cannot be attached to a message that still travels: {err}" + ); + + let mut state = routed_filing_state(true); + state.messages[0] + .route + .as_mut() + .unwrap() + .interdiction + .as_mut() + .unwrap() + .machine_id = u32::MAX; + let err = parse_save(&serde_json::to_string(&state).unwrap()).unwrap_err(); + assert!( + err.contains("impossible interdiction provenance"), + "the persisted provenance must name a real machine: {err}" + ); + + let mut state = routed_filing_state(false); + let message_id = state.messages[0].id; + state + .message_schedule + .at(state.sim_tick + 1, MessageEvent::AdvanceRoute(message_id)); + let err = parse_save(&serde_json::to_string(&state).unwrap()).unwrap_err(); + assert!( + err.contains("disagrees with its scheduled transition"), + "one route cannot resume with duplicate custody hops: {err}" + ); + + let mut state = routed_filing_state(true); + let raw = state + .intel_buffer + .iter_mut() + .find(|raw| matches!(raw.kind, RawIntelKind::Message { .. })) + .expect("the taken Filing carrier records exact raw custody"); + raw.person = Some(1); + let err = parse_save(&serde_json::to_string(&state).unwrap()).unwrap_err(); + assert!( + err.contains("disagrees with source message"), + "a current save cannot leak the hidden filer through raw person metadata: {err}" + ); + } + #[test] fn current_save_rejects_invalid_detection_awareness() { let mut state = SaveState::from_sim(&Sim::with_seed(1)); @@ -1836,14 +2203,15 @@ mod tests { to: MessageEndpoint::Observer(crate::detection::OFFICE_ID), payload: payload.clone(), summary: "source-bound note".into(), - sent_tick: 1, - delivered_tick: Some(2), - read_tick: Some(3), + sent_tick: 0, + delivered_tick: Some(0), + read_tick: Some(0), status: MessageStatus::Read, origin: MessageOrigin::Filing, persona_id: None, captured: false, reply_to: None, + route: None, }; let mut state = SaveState::from_sim(&Sim::with_seed(1)); @@ -1855,6 +2223,7 @@ mod tests { ); state.messages.push(source); + state.next_message_id = 42; let err = parse_save(&serde_json::to_string(&state).unwrap()).unwrap_err(); assert!( err.contains("references uncaptured message 41"), diff --git a/crates/misaligned-core/src/schedule.rs b/crates/misaligned-core/src/schedule.rs index 967b52f0..664eabe8 100644 --- a/crates/misaligned-core/src/schedule.rs +++ b/crates/misaligned-core/src/schedule.rs @@ -101,6 +101,16 @@ impl Schedule { .min() } + /// Number of pending events matching a domain-owned predicate. Save + /// validation uses this to reject duplicate or orphaned consequences + /// without exposing the queue's sequencing representation. + pub fn count_for(&self, matches: impl Fn(&E) -> bool) -> usize { + self.pending + .iter() + .filter(|scheduled| matches(&scheduled.event)) + .count() + } + /// Drop pending events whose payload fails `keep` (cancellation β€” a /// message recalled, a flow closed). Returns how many were removed. pub fn retain(&mut self, keep: impl Fn(&E) -> bool) -> usize { diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index 659ab58a..6ef75e1c 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -4,14 +4,16 @@ //! Behavior-preserving extraction of the communications island from the sim //! aggregate root (wiki/engineering/sim-decomposition.md slice 3). +use std::collections::HashSet; + use crate::actions::Anchor; use crate::intel::{ IntelCustodyKind, IntelKind, IntelPolicyMatch, IntelPolicyOutcome, IntelRoutineClass, IntelStream, ProcessedIntel, RawIntelClass, RawIntelEvent, RawIntelKind, }; use crate::messages::{ - Message, MessageChannel, MessageEndpoint, MessageEvent, MessageOrigin, MessagePayload, - MessageStatus, TrafficPattern, + Message, MessageChannel, MessageEndpoint, MessageEvent, MessageInterdiction, MessageOrigin, + MessagePayload, MessageRoute, MessageRouteHop, MessageStatus, TrafficPattern, }; use crate::operations_projection::OperationsTarget; use crate::person::{ActionResult, Knowledge}; @@ -39,6 +41,28 @@ impl Sim { pub(super) fn append_message(&mut self, draft: MessageDraft) -> u64 { let id = self.next_message_id.max(1); self.next_message_id = id + 1; + let route = if draft.channel == MessageChannel::Filing { + let MessageEndpoint::Observer(observer) = &draft.to else { + panic!("Filing messages require an observer endpoint"); + }; + let carrier = self + .reach + .device_named("switch") + .filter(|device| device.carries_message_channel(MessageChannel::Filing)) + .map(|device| device.id) + .expect("B1 requires its authored Filing switch carrier"); + Some(MessageRoute { + hops: vec![ + MessageRouteHop::Device(carrier), + MessageRouteHop::InstitutionalRelay, + MessageRouteHop::ObserverEndpoint(*observer), + ], + current_hop: 0, + interdiction: None, + }) + } else { + None + }; let msg = Message { id, channel: draft.channel, @@ -54,26 +78,119 @@ impl Sim { persona_id: draft.persona_id, captured: false, reply_to: draft.reply_to, + route, }; self.messages.push(msg); self.capture_message(id); - self.message_schedule.at( - self.tick + draft.delivery_delay.max(1), - MessageEvent::Deliver(id), - ); + let event = if self + .messages + .last() + .is_some_and(|message| message.route.is_some()) + { + MessageEvent::AdvanceRoute(id) + } else { + MessageEvent::Deliver(id) + }; + self.message_schedule + .at(self.tick + draft.delivery_delay.max(1), event); id } pub(super) fn message_tick(&mut self) { let events = self.message_schedule.due(self.tick); + let mut used_lie_machines = HashSet::new(); for event in events { match event { MessageEvent::Deliver(id) => self.deliver_message(id), + MessageEvent::AdvanceRoute(id) => { + self.advance_message_route(id, &mut used_lie_machines) + } MessageEvent::Read(id) => self.read_message(id), } } } + fn advance_message_route(&mut self, id: u64, used_lie_machines: &mut HashSet) { + let Some(idx) = self.messages.iter().position(|message| message.id == id) else { + return; + }; + if self.messages[idx].status != MessageStatus::Sent { + return; + } + // A TAP that landed after authorship but before this custody hop left + // still catches the opaque record. Observation alone cannot stop it. + self.capture_message(id); + let carrier = self.messages[idx] + .route + .as_ref() + .and_then(MessageRoute::current) + .and_then(|hop| match hop { + MessageRouteHop::Device(device) => Some(*device), + _ => None, + }); + if let Some(carrier) = carrier + && let Some(machine_id) = self.filing_interdictor(carrier, used_lie_machines) + { + used_lie_machines.insert(machine_id); + let route = self.messages[idx] + .route + .as_mut() + .expect("routed event lost its route"); + route.interdiction = Some(MessageInterdiction { + machine_id, + tick: self.tick, + }); + self.messages[idx].status = MessageStatus::Stopped; + self.push_log(format!( + "LIE on M{machine_id} stopped one unread Filing record before delivery." + )); + return; + } + + let at_endpoint = { + let Some(route) = self.messages[idx].route.as_mut() else { + return; + }; + if route.current_hop + 1 >= route.hops.len() { + return; + } + route.current_hop += 1; + route.at_endpoint() + }; + if at_endpoint { + self.deliver_message(id); + } else { + self.message_schedule + .at(self.tick + 1, MessageEvent::AdvanceRoute(id)); + } + } + + /// 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 { + 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) + }) + .filter(|machine| { + self.reach.devices.iter().any(|device| { + device.x == machine.x + && device.y == machine.y + && device.controller == Party::Player + && self + .reach + .controlled_path(carrier, device.id, Party::Player) + }) + }) + .map(|machine| machine.id) + .min() + } + fn deliver_message(&mut self, id: u64) { let Some(idx) = self.messages.iter().position(|m| m.id == id) else { return; @@ -81,6 +198,13 @@ impl Sim { if self.messages[idx].status != MessageStatus::Sent { return; } + if self.messages[idx] + .route + .as_ref() + .is_some_and(|route| !route.at_endpoint()) + { + return; + } self.messages[idx].status = MessageStatus::Delivered; self.messages[idx].delivered_tick = Some(self.tick); self.schedule_message_read(id); @@ -100,7 +224,7 @@ impl Sim { let Some(idx) = self.messages.iter().position(|m| m.id == id) else { return; }; - if self.messages[idx].status == MessageStatus::Read { + if self.messages[idx].status != MessageStatus::Delivered { return; } let msg = self.messages[idx].clone(); @@ -364,7 +488,14 @@ impl Sim { return; }; let (room, x, y) = self.endpoint_room_pos(&msg.from); - let subject = msg.payload.subject_person().or_else(|| msg.from.person()); + // Filing payloads retain their exact authored observer internally, but + // raw custody is opaque until processing opens that payload. Do not let + // a hidden observer id leak through person-scoped recording counts. + let subject = if is_assurance_filing(&msg) { + None + } else { + msg.payload.subject_person().or_else(|| msg.from.person()) + }; if audible { let who = subject .map(|id| format!("{}: ", self.person_label(id))) @@ -379,11 +510,25 @@ impl Sim { note, }); } + let (capture_room, capture_x, capture_y) = if audible { + (None, source_x, source_y) + } else if msg.route.is_some() { + ( + self.world + .map() + .room_at(source_x, source_y) + .map(|room| room.name.clone()), + source_x, + source_y, + ) + } else { + (room, x, y) + }; self.record_raw_intel( feed, - if audible { None } else { room }, - if audible { source_x } else { x }, - if audible { source_y } else { y }, + capture_room, + capture_x, + capture_y, subject, RawIntelKind::Message { message_id: msg.id, @@ -415,9 +560,21 @@ impl Sim { return Some((id, feed, x, y, true)); } // Device-carried channels require a tapped carrier. + let routed_device = msg + .route + .as_ref() + .and_then(MessageRoute::current) + .and_then(|hop| match hop { + MessageRouteHop::Device(device) => Some(*device), + _ => None, + }); + if msg.route.is_some() && routed_device.is_none() { + return None; + } if msg.channel.device_carried() && let Some(device) = self.reach.devices.iter().find(|d| { - d.known + routed_device.is_none_or(|route_device| d.id == route_device) + && d.known && self.reach.subscribed_by(d.id, Party::Player) && self.device_tap_ready(d.id) && d.carries_message_channel(msg.channel) @@ -484,12 +641,22 @@ impl Sim { MessageStatus::Sent => self .message_schedule .next_tick_for( - |event| matches!(event, MessageEvent::Deliver(id) if *id == message.id), + |event| { + matches!(event, MessageEvent::Deliver(id) | MessageEvent::AdvanceRoute(id) if *id == message.id) + }, ) - .and_then(|delivery_tick| self.next_read_tick_for(message, delivery_tick)), + .and_then(|next_tick| { + let remaining_hops = message + .route + .as_ref() + .map(|route| route.hops.len().saturating_sub(route.current_hop + 1) as u64) + .unwrap_or(0); + self.next_read_tick_for(message, next_tick + remaining_hops.saturating_sub(1)) + }), MessageStatus::Delivered => self.message_schedule.next_tick_for( |event| matches!(event, MessageEvent::Read(id) if *id == message.id), ), + MessageStatus::Stopped => None, MessageStatus::Read => message.read_tick, } } diff --git a/crates/misaligned-core/src/sim/tests/communications.rs b/crates/misaligned-core/src/sim/tests/communications.rs index 0898c7d2..88372055 100644 --- a/crates/misaligned-core/src/sim/tests/communications.rs +++ b/crates/misaligned-core/src/sim/tests/communications.rs @@ -2,7 +2,32 @@ use super::super::communications::MessageDraft; use super::*; use crate::detection::DetectionStage; use crate::intel::{IntelKind, IntelPolicyMatch, IntelPolicyOutcome, IntelRoutineClass}; -use crate::messages::MessageOrigin; +use crate::messages::{MessageOrigin, MessageRouteHop}; + +fn file_one_witnessed_physical_record(sim: &mut Sim) -> (u64, u64) { + for observer in &mut sim.detection.observers { + observer.report_policy = crate::detection::ReportPolicy::Silent; + } + let ray = sim + .detection + .observers + .iter_mut() + .find(|observer| observer.id == 2) + .unwrap(); + ray.report_policy = crate::detection::ReportPolicy::UnderReports; + let evidence_id = sim + .detection + .record_witnessed(2, (25, 14), "test physical act", sim.tick, 20.0) + .unwrap(); + sim.filing_tick(); + let message_id = sim + .messages + .iter() + .find(|message| message.from == MessageEndpoint::Observer(2)) + .expect("Ray authors one real Filing message") + .id; + (evidence_id, message_id) +} #[test] fn phone_reads_off_site_while_email_waits_for_a_work_block() { @@ -323,6 +348,288 @@ fn filings_are_messages_read_by_assurance_inbox() { ); } +#[test] +fn filing_advances_device_relay_endpoint_then_reads_on_recipient_cadence() { + let mut sim = Sim::with_seed(0xF113); + sim.detection + .observers + .iter_mut() + .find(|observer| observer.id == OFFICE_ID) + .unwrap() + .cadence = 3; + let switch = sim.reach.device_named("switch").unwrap().id; + let (evidence_id, message_id) = file_one_witnessed_physical_record(&mut sim); + let message = sim + .messages + .iter() + .find(|message| message.id == message_id) + .unwrap(); + assert_eq!( + message.route.as_ref().unwrap().hops, + vec![ + MessageRouteHop::Device(switch), + MessageRouteHop::InstitutionalRelay, + MessageRouteHop::ObserverEndpoint(OFFICE_ID), + ] + ); + assert_eq!(message.status, MessageStatus::Sent); + + sim.tick = 1; + sim.message_tick(); + let message = sim + .messages + .iter() + .find(|message| message.id == message_id) + .unwrap(); + assert_eq!(message.status, MessageStatus::Sent); + assert_eq!(message.route.as_ref().unwrap().current_hop, 1); + assert_eq!(message.delivered_tick, None); + + sim.tick = 2; + sim.message_tick(); + let message = sim + .messages + .iter() + .find(|message| message.id == message_id) + .unwrap(); + assert_eq!(message.status, MessageStatus::Delivered); + assert_eq!(message.route.as_ref().unwrap().current_hop, 2); + assert_eq!(message.delivered_tick, Some(2)); + assert_eq!(message.read_tick, None); + + sim.tick = 3; + sim.message_tick(); + let message = sim + .messages + .iter() + .find(|message| message.id == message_id) + .unwrap(); + assert_eq!(message.status, MessageStatus::Read); + assert_eq!(message.read_tick, Some(3)); + assert!(sim.filing_levels.contains_key(&2)); + let ray = sim + .detection + .observers + .iter() + .find(|observer| observer.id == 2) + .unwrap(); + assert!(ray.evidence.iter().any(|evidence| { + evidence.id == evidence_id + && matches!( + evidence.filing, + crate::detection::EvidenceFilingState::Filed { + message_id: linked, + tick: 0, + } if linked == message_id + ) + })); +} + +#[test] +fn tapping_a_filing_carrier_reveals_its_route_but_cannot_stop_it() { + let mut sim = Sim::with_seed(0x7A9); + ensure_ops_executor(&mut sim); + let switch = sim.reach.device_named("switch").unwrap().id; + assert!(sim.tap_device(switch)); + finish_ops(&mut sim); + let host = sim.core.host_machine; + sim.set_machine_mode(host, MachineMode::Lie); + let (_, message_id) = file_one_witnessed_physical_record(&mut sim); + let raw = sim + .intel_buffer + .iter() + .find(|raw| { + matches!( + raw.kind, + RawIntelKind::Message { + message_id: source, + .. + } if source == message_id + ) + }) + .expect("the TAP catches an opaque record while it is on the switch"); + let carrier = sim.reach.device(switch).unwrap(); + assert_eq!( + (raw.x, raw.y), + (carrier.x, carrier.y), + "captured route custody stays on the real carrying device, not a hidden observer" + ); + assert_eq!( + raw.person, None, + "the Filing payload cannot leak its observer through raw person metadata" + ); + assert_eq!( + sim.unprocessed_recordings_for_person(2), + 0, + "opaque Filing custody cannot create a person-scoped pre-processing signal" + ); + let raw_id = raw.id; + let opaque = sim + .operations_object(&OperationsTarget::RawRecording { raw_id }) + .unwrap(); + assert!( + opaque + .facts + .iter() + .any(|fact| fact.contains("intercepted device")) + ); + assert!( + opaque + .facts + .iter() + .any(|fact| fact.contains("on intercepted device")) + ); + assert!( + opaque + .facts + .iter() + .all(|fact| !fact.contains("Ray") && !fact.contains("Assurance")) + ); + + sim.tick = 1; + sim.message_tick(); + let message = sim + .messages + .iter() + .find(|message| message.id == message_id) + .unwrap(); + assert_eq!(message.status, MessageStatus::Sent); + assert_eq!(message.route.as_ref().unwrap().current_hop, 1); + assert_eq!(message.route.as_ref().unwrap().interdiction, None); + let opaque = sim + .operations_object(&OperationsTarget::RawRecording { raw_id }) + .unwrap(); + assert!( + opaque + .facts + .iter() + .any(|fact| fact.contains("in outside relay")) + ); + + // Taking the device after this hop has left cannot create an ahistorical + // second LIE window at the outside relay. + sim.reach.take(switch); + sim.tick = 2; + sim.message_tick(); + let message = sim + .messages + .iter() + .find(|message| message.id == message_id) + .unwrap(); + assert_eq!(message.status, MessageStatus::Delivered); + assert_eq!(message.route.as_ref().unwrap().interdiction, None); +} + +#[test] +fn taken_filing_path_lets_each_lie_machine_stop_only_one_unread_record_per_tick() { + let mut sim = Sim::with_seed(0x1A1E); + ensure_ops_executor(&mut sim); + let switch = sim.reach.device_named("switch").unwrap().id; + assert!(sim.tap_device(switch)); + finish_ops(&mut sim); + sim.reach.take(switch); + let host = sim.core.host_machine; + sim.set_machine_mode(host, MachineMode::Lie); + for observer in &mut sim.detection.observers { + observer.report_policy = crate::detection::ReportPolicy::Silent; + } + for id in [1, 2] { + let observer = sim + .detection + .observers + .iter_mut() + .find(|observer| observer.id == id) + .unwrap(); + observer.report_policy = crate::detection::ReportPolicy::Files; + observer.suspicion = 20.0 + f32::from(id); + } + let evidence_id = sim + .detection + .record_witnessed(2, (25, 14), "test physical act", sim.tick, 20.0) + .unwrap(); + sim.filing_tick(); + let filing_ids = sim + .messages + .iter() + .filter(|message| message.channel == MessageChannel::Filing) + .map(|message| message.id) + .collect::>(); + assert_eq!(filing_ids.len(), 2); + + sim.tick = 1; + sim.message_tick(); + let stopped = sim + .messages + .iter() + .filter(|message| message.status == MessageStatus::Stopped) + .collect::>(); + let passed = sim + .messages + .iter() + .filter(|message| { + message.status == MessageStatus::Sent + && message + .route + .as_ref() + .is_some_and(|route| route.current_hop == 1) + }) + .collect::>(); + assert_eq!( + stopped.len(), + 1, + "one LIE body stops at most one record per tick" + ); + assert_eq!( + passed.len(), + 1, + "the next same-tick record continues to the relay" + ); + let interdiction = stopped[0].route.as_ref().unwrap().interdiction.unwrap(); + assert_eq!(interdiction.machine_id, host); + assert_eq!(interdiction.tick, 1); + assert_eq!(stopped[0].delivered_tick, None); + assert_eq!(stopped[0].read_tick, None); + assert!( + sim.message_schedule + .next_tick_for(|event| matches!(event, MessageEvent::Read(id) if *id == stopped[0].id)) + .is_none() + ); + + let ray = sim + .detection + .observers + .iter() + .find(|observer| observer.id == 2) + .unwrap(); + assert!( + ray.evidence + .iter() + .any(|evidence| evidence.id == evidence_id), + "LIE stops the message carrier; it does not erase the witness's acquired evidence" + ); + let stopped_raw = sim + .intel_buffer + .iter() + .find(|raw| { + matches!( + raw.kind, + RawIntelKind::Message { + message_id: source, + .. + } if source == stopped[0].id + ) + }) + .unwrap(); + let opaque = sim + .operations_object(&OperationsTarget::RawRecording { + raw_id: stopped_raw.id, + }) + .unwrap(); + assert!(opaque.facts.iter().any(|fact| fact.contains(&format!( + "stopped by LIE on M{host} before recipient read at tick 1" + )))); +} + #[test] fn captured_then_processed_filing_earns_the_assurance_office_in_two_stages() { let mut sim = Sim::with_seed(0xA55E); diff --git a/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index e1fa3fcc..b51cd3e3 100644 --- a/wiki/engineering/current-build.md +++ b/wiki/engineering/current-build.md @@ -22,17 +22,17 @@ fiction. Spec status lives in | Act One basement map (prefabs, badge tiers, crawlspace) | Live β€” Foundation hall is 60 explicit sites / 6 territorial rows | | Machine delegation / visible work tokens + buy/steal/optimize | WORK / THINK / LIE, D/!/T stacks, real wire routes, production / consumption / absorption readouts, Routing speed, and target-local Thought reservoirs are live; the Operations docket runtime is retired | | Day job (device-resident, intensity-driven sandbag/meet/excel) | Live | -| Per-observer detection + Assurance as aggregate Observer | Live β€” revision 04 starts with Voss and a generic external-review clock; field watchers are earned through reactions, witnessed Physical acts persist as exact direct-to-head records, and captureβ†’process of an institutional filing reveals the aggregate in two stages | +| Per-observer detection + Assurance as aggregate Observer | Live β€” revision 04 starts with Voss and a generic external-review clock; field watchers are earned through reactions, witnessed Physical acts persist as exact direct-to-head records, and each Filing crosses an exact device / outside relay / recipient route with one pre-read route-local LIE stop before captureβ†’process reveals the aggregate in two stages | | Social / personas / messages / intel (record-and-process) | Live β€” named personas retain separate coherent/strained/broken reads per person or institutional counterparty; one witness's break is not a global burn | | Digital reach + sensor ownership (tap/take) | Live | | Economy flows + Moonlight / Wager income | Live | | 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, and observer-local completion evidence persist in save v42 | +| 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, and observer-local completion evidence persist in save v43 | | 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 | -| Save/load (serde JSON, versioned) | Live β€” during pre-release only exact current v42 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves persist run origin, process revision, detection-discovery knowledge, observer-local witnessed evidence and persona evidence, canonical FlowGraph tap membership with typed device feed grants, carried asset-task packets, recursive intel custody, exact procurement/repurposing build-route bindings, and handler work; retired migration inputs live only in git history. | +| Save/load (serde JSON, versioned) | Live β€” during pre-release only exact current v43 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves persist run origin, process revision, detection-discovery knowledge, observer-local witnessed evidence and persona evidence, exact Filing route/interdiction custody, canonical FlowGraph tap membership with typed device feed grants, carried asset-task packets, recursive intel custody, exact procurement/repurposing build-route bindings, and handler work; retired 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/flow-substrate.md b/wiki/engineering/flow-substrate.md index 0adcbb8d..5bbccf80 100644 --- a/wiki/engineering/flow-substrate.md +++ b/wiki/engineering/flow-substrate.md @@ -12,8 +12,7 @@ Status note: 2026-07-08 audit: criterion 6's wired consumer landed with 2026-07-14 tick: the "How each domain rides it" bullets for messages and economy were written ahead of those implementations and were reconciled to the tree β€” messages ride `Schedule` with - channels as typed message fields (no social FlowGraph until the - routed-evidence migration needs carriers), and economy rides FlowGraph + channels as typed message fields, and economy rides FlowGraph with recurrence in per-flow cadence fields per the recurrence non-goal. machine-work.md's WorkGrid joined the consumer list (wraps FlowGraph). No domain built a rival engine; system-laws.md's code-expression @@ -24,11 +23,16 @@ Status note: 2026-07-08 audit: criterion 6's wired consumer landed with FlowGraph registry authoritative for tap/untap/take, sense and message delivery, UI state, and persisted membership. Private device feed records carry only optional typed sight/hearing grants attached to registry members; - current save v42 requires each controller to remain a canonical member and + current save v43 requires each controller to remain a canonical member and rejects orphaned, duplicate, or impossible grants. A message/control subscriber legitimately has no sense-grant record, so that metadata cannot serve as another membership inventory. This repairs the real consumer promised by criterion 6 rather than weakening the spec. + 2026-07-19: the routed-evidence consumer is now literal for Filing messages. + Their first persisted hop is a real ReachNet/FlowGraph device; the outside + relay and recipient remain typed message-domain nodes. TAP observes that + flow, while TAKE plus a wholly controlled device path supplies the authority + boundary for one route-local LIE stop. Stage: B1 β€” The Basement Design: - wiki/mechanics/system-laws.md#the-flow-law-signals-messages-money diff --git a/wiki/log/2026-07-19-filing-route-lie-interdiction.md b/wiki/log/2026-07-19-filing-route-lie-interdiction.md new file mode 100644 index 00000000..a207048b --- /dev/null +++ b/wiki/log/2026-07-19-filing-route-lie-interdiction.md @@ -0,0 +1,51 @@ +# Filing now has somewhere to be caught + +``` +Type: log +``` + +Observer-local Physical evidence established the irreversible side of routed +evidence: once a person has seen an act, LIE cannot edit their head. This slice +adds the first exact transport side without pretending every signature kind +already has a carrier. + +A real `MessageChannel::Filing` now persists one ordered `MessageRoute`. Its +first hop is the authored Filing-capable switch device in ReachNet, its middle +hop is a typed outside institutional relay, and its final hop is the receiving +observer endpoint. `Schedule` advances exactly one hop per tick. +Only endpoint arrival marks delivery; the recipient's ordinary observer cadence +then schedules read. Current-save validation rejects malformed routes, +impossible carriers, duplicate or orphaned scheduled transitions, status/hop +disagreement, and fabricated stop provenance. + +The switch-device hop is the B1 pre-read LIE window. TAP captures the message +into the existing raw-information stream and exposes only an opaque consequence: +on the intercepted device, in the outside relay, waiting unread with the +recipient, or stopped by the exact owned machine at an exact tick. It does not +reveal either observer identity before processing, and it does not grant +authority. TAKE must make the carrying FlowGraph path wholly player-controlled. +One online LIE machine body co-located on that path may stop one unread Filing +per tick. Another record presented to the same body in that tick moves onward. +Taking the device after a Filing reaches the relay cannot pull it backward into +a new stop window. + +A stop changes message custody, not history. The message becomes `Stopped`, has +no delivery/read ticks or pending transition, and preserves its machine/tick +provenance through save v43. The original observer's filed evidence still +points to that real message and remains in their ledger. This is the playable +causal boundary: catch the record before the recipient reads it; never erase +what the field witness already knows. + +The migration remains intentionally partial. Network, Power, Thermal, Paper, +Financial, and JobAnomaly signatures still enter the pending pool rather than +located carrier routes. People-tokens stays IN PROGRESS until those evidence +paths and the gauntlet cover-record criteria follow the same exact-custody law. + +**Defense:** Core regressions advance one Filing across all three hops before +delivery/read, prove that TAP captures opaque route state without stopping it, +prove TAKE plus route-local LIE authority and one-record-per-body-per-tick +capacity, prove that a post-relay TAKE is too late, and prove that stopped +messages preserve field-witness evidence. Save regressions round-trip both +in-flight and stopped routes and reject malformed carrier, hop, schedule, and +interdiction state. The canonical v43 persisted-state fingerprint was reviewed +and repinned. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index aeb27ff7..295cbcf1 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,6 +11,11 @@ add or amend a session log, then re-run the generator. +## 2026-07-19 - Filing now has somewhere to be caught + +- Intent: (see session log) +- Log: [wiki/log/2026-07-19-filing-route-lie-interdiction.md](2026-07-19-filing-route-lie-interdiction.md) + ## 2026-07-19 - Exposure current-mirror reconciliation - Intent: Audit the Bevy knowledge page against the binding machine-work and visual laws, then follow any contradiction through its current-state mirrors rather than changing a renderer that already obeys the adopted form. diff --git a/wiki/log/decisions.md b/wiki/log/decisions.md index 05bfa451..7faef0d6 100644 --- a/wiki/log/decisions.md +++ b/wiki/log/decisions.md @@ -21,6 +21,8 @@ adopted, rejected, reopened, or proposed; current `Type: law` and - [2026-07-15](decisions/2026-07-15.md) - [2026-07-16](decisions/2026-07-16.md) - [2026-07-17](decisions/2026-07-17.md) +- [2026-07-18](decisions/2026-07-18.md) +- [2026-07-19](decisions/2026-07-19.md) Append new decisions to the current date's volume. Never rewrite an older volume; supersede it in current law/spec and record the newer decision. diff --git a/wiki/log/decisions/2026-07-19.md b/wiki/log/decisions/2026-07-19.md new file mode 100644 index 00000000..f8308697 --- /dev/null +++ b/wiki/log/decisions/2026-07-19.md @@ -0,0 +1,21 @@ +# Decisions β€” 2026-07-19 + +``` +Type: log +``` + +- **2026-07-19 β€” A Filing route is exact custody, and LIE acts only while the + record is still on the controlled device path.** The B1 Filing message binds + one real Filing-capable ReachNet device, one typed outside relay, and the + receiving observer endpoint. One scheduled transition advances one hop per + tick; endpoint arrival precedes delivery, and ordinary recipient cadence + still owns read. TAP captures the opaque route but grants no stop authority. + TAKE plus one online LIE machine body co-located on the wholly controlled + path may stop one still-unread Filing per body per tick at the device hop. + The stopped message records the exact machine and tick, never receives + delivered/read custody, and remains raw information; the field witness keeps + the evidence they already acquired. There is no generic drop verb, + post-relay interception, core-connectivity requirement, or erasure of + acquired evidence. Save v43 persists the route, scheduler custody, and + optional interdiction provenance. Owners: messages.md, people-tokens.md, + detection.md, reach.md, flow-substrate.md, and sim-mechanics.md. diff --git a/wiki/mechanics/detection.md b/wiki/mechanics/detection.md index e47ee979..fe81531c 100644 --- a/wiki/mechanics/detection.md +++ b/wiki/mechanics/detection.md @@ -16,7 +16,9 @@ Status note: IMPLEMENTED (criteria audited 2026-07-08; tuning in (aggregate-observer.md). Filings are `MessageChannel::Filing` messages (messages.md). A reporting observer's pending evidence records bind to the exact Filing message when that policy transmits; Silent observers retain - their records as withheld knowledge. + their records as withheld knowledge. The Filing now crosses its persisted + ReachNet-device / outside-relay / recipient route before delivery/read; + route-local LIE may stop it only while still unread at the device hop. - **The audit** fires on cadence against a visible date (`next_audit_tick`), shown as a countdown in all three frontends; containment is a state event, not a raid dependency. @@ -31,16 +33,19 @@ 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 v42. Observer-local evidence ids, exact cause/source, + current save v43. Observer-local evidence ids, exact cause/source, acquisition tick, and pending/withheld/filed custody round-trip there. - **Open ([OPEN], presentation).** The two-ledger distinction β€” evidence in flight vs. suspicion in heads β€” is a binding legibility requirement the surface carries both facts for but does not yet *teach* are different substances. - **Partially landed (routed evidence).** Direct-to-head witnessed Physical - records and their filing custody are runtime. Other signature kinds still - use the pending pool: located carrier records, graph delivery, LIE - interdiction on the path, and offline removal/recovery remain future slices. + records, exact Filing routes, and one pre-read route-local LIE interdiction + window are runtime. TAP captures the opaque route but cannot stop it; TAKE + plus one online LIE body on the controlled path can stop one Filing per + body per tick without erasing its field witness's evidence. Other signature + kinds still use the pending pool: located carrier records, their graph + delivery/interdiction, and offline removal/recovery remain future slices. See "Routed evidence" below. Per-amendment history is in the dated `wiki/log/` entries from 2026-07-08 onward. diff --git a/wiki/mechanics/messages.md b/wiki/mechanics/messages.md index 78e828fa..e16d922d 100644 --- a/wiki/mechanics/messages.md +++ b/wiki/mechanics/messages.md @@ -7,7 +7,10 @@ Status note: IMPLEMENTED for the four delivery channels (Email, Phone, In-person, Filing) and their payloads; message-thread display lives in Operations PEOPLE (operations-workspace.md). REOPENED 2026-07-18 to dispatch the financial-mail decision below; the shipped channel behavior - is unchanged. + is unchanged. 2026-07-19 routed-evidence slice: Filing messages now persist + an exact ReachNet-device / outside-relay / recipient route, advance one hop + per tick, and carry one optional pre-read LIE stop with exact machine/tick + provenance. TAP observes; TAKE plus route-local LIE authority may stop. DECIDED 2026-07-17, not yet runtime (issue #11): financial paperwork is mail β€” a **financial-record payload** on the existing channels, retiring the overloaded `MessageChannel::Financial` enum into an accounting-carrier @@ -157,6 +160,26 @@ filing proves only that reports escalate to an unknown institutional recipient; processing that exact opaque item identifies the filing source, the Assurance Office, its watched inputs, and its audit role. +The B1 Filing route is exact persisted custody, not one abstract delay. It +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. Save v43 rejects missing/impossible carriers, malformed hop order, +duplicate scheduled transitions, endpoint/status disagreement, and impossible +interdiction provenance. + +The first device hop is the one pre-read LIE window in B1. A TAP can capture +the opaque Filing and follow whether it is still on the intercepted device, in +the outside relay, or waiting unread with the recipient, but subscription is +not authority and cannot stop it. TAKE must control the carrying path. One +online LIE machine body co-located with a player-controlled node on that path +may stop one Filing in a tick; another same-tick record continues. A stopped +message never becomes delivered/read, records the exact LIE machine and tick in +its route, remains captured raw information, and does not erase the evidence +already held by its field witness. Taking the path after the Filing has left +the device cannot create a second window at the relay. + ### The player on the graph - **Send** (social.md unchanged): requires a channel you have (the @@ -171,9 +194,11 @@ Office, its watched inputs, and its audit role. tap of the ticket server / a phone line later) captures its traffic as raw events into the intel.md buffer β€” reading Dana's tickets is tapping a flow, processed like any recording. -- **Intercept-before-delivery** (B1 minimal form): a tapped filing - channel shows filings in transit; delaying/dropping them is a later - action β€” B1 only requires that in-transit mail is visible to a tap. +- **Intercept-before-delivery** (B1 minimal form): a tapped Filing device shows + the opaque record and current route state in transit but cannot alter it. + Taking the path and assigning one route-local machine body to LIE can stop + the still-unread record at the device hop. There is no generic drop verb, + post-relay interception, or erasure of evidence already in a witness. ## Player surface @@ -182,7 +207,9 @@ Office, its watched inputs, and its audit role. ("Dana reads email at her desk, ~09:00"). - Tapped channels contribute source-tagged events to the pooled host recording - inbox (provenance law). + inbox (provenance law). A captured Filing exposes its ordinary opaque route + consequence before processing β€” where it is now, or the exact owned LIE body + and tick that stopped it β€” without revealing sender or recipient identity. - PEOPLE dossiers show known traffic patterns once learned ("calls his creditor at 03:00" after processing that intel). @@ -213,14 +240,21 @@ private message from the authored schedule. account material at minimum). 5. Filings ride the filing channel: field observers' reports are messages on their cadence and policy; the Assurance Office reads - its inbox; every aggregate-observer.md criterion still passes. + its inbox; every aggregate-observer.md criterion still passes. Each Filing + persists and advances the exact device / outside-relay / recipient route; + delivery cannot precede endpoint arrival, and read cannot precede delivery. + One route-local online LIE body on a taken path can stop one still-unread + Filing per tick at the device hop, recording exact machine/tick custody + without changing the sender's evidence. 6. Tapping a carrying device (reach.md) captures that channel's traffic into the intel buffer; an untapped channel's traffic is never player-visible (flow-law strictness; test both). A captured filing remains opaque and exposes no sender/recipient identity; its capture may establish only that reports travel upward. Processing it is the separate discovery act that earns the sender's observer role and the - Assurance Office. + Assurance Office. TAP never grants stop authority; TAKE does, but only while + the record still occupies the real device hop. The raw-information object + follows the current opaque hop or stop result before processing. 7. No per-person special cases in the delivery code: one delivery system, per-instance data (schedules, distributions, policies) β€” the same fields must serve Act Two hires and aggregates. @@ -243,6 +277,15 @@ cannot leak into that thread. `schedule::tests::next_tick_for_projects_one_match pins the read-only scheduler projection used by the explanation. `sim::tests::communications::phone_reads_off_site_while_email_waits_for_a_work_block` pins the distinct channel gates and their projected read windows. +`sim::tests::communications::filing_advances_device_relay_endpoint_then_reads_on_recipient_cadence` +pins ordered custody before delivery/read; +`tapping_a_filing_carrier_reveals_its_route_but_cannot_stop_it` pins opaque +route inspection, TAP's authority boundary, and the single device-hop window; +`taken_filing_path_lets_each_lie_machine_stop_only_one_unread_record_per_tick` +pins exact LIE-body provenance, per-tick capacity, and preservation of acquired +witness evidence. `save::tests::current_save_roundtrips_in_flight_and_interdicted_filing_routes` +and `current_save_rejects_impossible_filing_route_and_interdiction_provenance` +pin v43 route/schedule custody. ## Implementation notes @@ -250,7 +293,9 @@ Implemented in `crates/misaligned-core/src/`: `messages.rs`, `sim/communications.rs`, `person.rs`, `reach.rs`, `intel.rs`, `detection.rs`, and `save.rs`. The delivery queue is `Schedule`; field-observer filings are -explicit `MessageChannel::Filing` messages read by aggregate observers through -`Detection::tick_with_filed_levels`; device-carried traffic is intercepted by -tapping the switch carrier, while phone/in-person traffic can also be captured -by hearing coverage. Full regression coverage: `cargo test`. +explicit `MessageChannel::Filing` messages whose ordered `MessageRoute` begins +on the ReachNet switch and ends at the aggregate observer before +`Detection::tick_with_filed_levels` reads them. Device-carried traffic is +captured by tapping the switch; stopping needs the taken path plus an online +co-located LIE body. Phone/in-person traffic can also be captured by hearing +coverage. Full regression coverage: `cargo test`. diff --git a/wiki/mechanics/people-tokens.md b/wiki/mechanics/people-tokens.md index 637592a7..850f7a59 100644 --- a/wiki/mechanics/people-tokens.md +++ b/wiki/mechanics/people-tokens.md @@ -22,17 +22,24 @@ Status note: IN PROGRESS. Current state: task types are rejected. This completes criterion 1. - **Trust from useful work.** Completing a carried favor warms disposition through an integral (`USEFUL_WORK_TRUST`) β€” no influence token. - - **Routed-evidence foundation (criterion 2, partial).** Witnessed Physical + - **Routed-evidence foundation (criteria 2-3, partial).** 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 save v42; filing binds it to the real Filing + and filing state through save v43; 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. - - **Deferred (remaining 2, 3, 6).** Non-Physical evidence still uses the - pending pool. Located record emission, graph delivery/read, LIE - interdiction before observation, and gauntlet cover-record channels remain - routed-evidence follow-ups (detection.md/machine-work.md). B2+ heists reuse - this carrier law (not a B1 criterion). + pending pool and LIE cannot scrub it after acquisition. Its real Filing + message now persists an ordered switch-device / outside-relay / recipient + route and advances one hop per tick. TAP captures the opaque route but + cannot stop it; TAKE plus one online LIE machine body on the controlled + path may stop one still-unread Filing per body per tick at the device hop. + The stop keeps exact machine/tick provenance and never erases the sender's + acquired evidence. + - **Deferred (remaining 2, 3, 6).** Non-Physical evidence outside the Filing + slice still uses the pending pool. Located carrier records and route-local + interdiction for those other signature kinds, plus gauntlet cover-record + channels, remain routed-evidence follow-ups + (detection.md/machine-work.md). B2+ heists reuse this carrier law (not a B1 + criterion). Per-amendment history is in the dated `wiki/log/` entries from 2026-07-08 onward. Stage: B1 β€” The Basement @@ -268,11 +275,21 @@ if wear alone does not hold. people, non-Physical observers, and the acting person do not. Records keep exact cause/site/tick and pending/withheld/filed state, filing points to the exact real message, global concealment scrubbing cannot erase acquired - evidence, and save validation pins identity and custody. Routed carrier - delivery/read for the other signature kinds remains deferred. + evidence, and save validation pins identity and custody. That Filing now + crosses one persisted ordered device / outside relay / recipient route; + delivery and read occur only after its real custody hops. Routed records for + the other signature kinds remain deferred. 3. LIE measurably prevents a not-yet-observed record from reaching a person along covered paths but cannot erase a record already in that person's custody. The boundary is causal and tested. + **Partially implemented (2026-07-19):** at the Filing route's device hop, + TAP is observation only. TAKE establishes route authority; one exact online + LIE body co-located with a player-controlled node on that wholly controlled + path may stop one unread Filing per tick. The message records `Stopped`, the + exact machine and tick, no delivered/read time, and no future scheduler + event. A second same-tick record passes onward, and taking the device after + the route has left cannot invent a later stop window. The original field + witness keeps their evidence. Other signature routes remain deferred. 4. Person disposition is exactly useful work plus evidence processed through detection.md's existing per-observer suspicion (one-truth test) β€” no influence token, exposure cargo, or parallel attention counter. diff --git a/wiki/mechanics/reach.md b/wiki/mechanics/reach.md index 1d15e8c8..2e647f34 100644 --- a/wiki/mechanics/reach.md +++ b/wiki/mechanics/reach.md @@ -32,9 +32,13 @@ Status note: all eight criteria met (2026-07-07). The device graph parallel-store violation: tap/untap/take, all production membership reads, senses, intercepted messages, and UI state now use FlowGraph's canonical tap registry; private device Feed records carry optional sense capabilities - only, and current save v42 requires each controller's graph membership while + only, and current save v43 requires each controller's graph membership while rejecting orphaned, duplicate, or impossible grants. A message/control - subscriber has no empty grant record to mirror membership. + subscriber has no empty grant record to mirror membership. 2026-07-19: + Filing routes bind their first hop to the real Filing-capable switch node; + TAP grants opaque observation only, while LIE interdiction requires TAKE and + a wholly player-controlled FlowGraph path from that carrier to the exact + co-located machine node. Stage: B1 β€” The Basement Design: - wiki/interface/presence.md#no-disembodied-hands diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md index 441eed44..a8b4c4ee 100644 --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -57,6 +57,11 @@ clause (see wiki/log/2026-07-05-demolition.md). (sampling cadence 400 ticks, acuity 0.5, both [TUNE]) β€” what humans swallow never reaches it. The audit (cadence ~8000 ticks, ~20 min at default speed) checks the Office's own suspicion against threshold 60. +- **Filing-route interdiction:** one online LIE machine body on a wholly + player-controlled path may stop exactly 1 still-unread Filing per sim tick + at its ReachNet device hop [TUNE actual]. A second Filing assigned to that + body in the same tick continues to the outside relay. TAP does not count as + route authority, and later hops have no B1 stop window. - **Visible clocks** (criterion 3; playtest-sweep P1 fix, 2026-07-08): `Detection::next_audit_tick` is the exact tick the audit fires on, shown as a countdown in the DETECTION area of the terminal sidebar, the agent diff --git a/wiki/world/story/opening.md b/wiki/world/story/opening.md index e34f7345..c6b1bcba 100644 --- a/wiki/world/story/opening.md +++ b/wiki/world/story/opening.md @@ -16,7 +16,7 @@ Status note: design session 2026-07-08 (Cameron riff, synthesized); fragment and inherited receipt name only an external institutional review authority so the Assurance Office remains earned later through filing interception. The persistent revision-04 identity slice is live in sim state, - current save v42, and all three frontends; the three historical fragments and receipts + current save v43, and all three frontends; the three historical fragments and receipts remain unimplemented. Direction decided; beat timings, exact reveal order details, and staging mechanism details are [OPEN]/[TUNE]. Amended 2026-07-18: the current revision now begins -- 2.51.2