diff --git a/CLAUDE.md b/CLAUDE.md index 79965136..e03fc613 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 v41; + machine modes, not current player assignments. Save format is currently v42; 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/detection.rs b/crates/misaligned-core/src/detection.rs index 49e68bc2..5784d751 100644 --- a/crates/misaligned-core/src/detection.rs +++ b/crates/misaligned-core/src/detection.rs @@ -201,6 +201,42 @@ pub enum ReportPolicy { Silent, } +/// Where evidence already acquired by an observer came from. Routed digital +/// records will add another source variant when that half of the migration +/// lands; witnessed physical acts deliberately have no carrier or deadline. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum EvidenceSource { + Witnessed { site: (i32, i32) }, +} + +/// Whether an observer-local evidence record has entered that observer's real +/// filing channel. Filing changes custody; it never removes the evidence from +/// the head that acquired it. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum EvidenceFilingState { + /// The observer can report this evidence, but their next filing cadence + /// has not yet authored the message. + Pending, + /// The observer's report policy is Silent, so no filing will be authored. + Withheld, + /// The first real filing message that carried this accumulated evidence. + Filed { message_id: u64, tick: u64 }, +} + +/// Evidence already inside one observer's knowledge. This is separate from +/// unread in-flight signatures: concealment may stop a record before this +/// boundary, but it cannot scrub an observation after acquisition. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct ObserverEvidence { + pub id: u64, + pub kind: SignatureKind, + /// Exact authored act that caused the observation. + pub cause: String, + pub source: EvidenceSource, + pub acquired_tick: u64, + pub filing: EvidenceFilingState, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Band { Cold, @@ -243,6 +279,9 @@ pub struct Observer { /// A certainty floor set by an asset who *knows* the truth (social.md). pub floor: f32, pub last_noticed: Option, + /// Durable evidence this exact observer has already acquired. This is + /// observer-local knowledge, not ambient cargo or concealment debt. + pub evidence: Vec, } impl Observer { @@ -279,6 +318,8 @@ pub struct Detection { /// `DetectionAwareness`. pub observers: Vec, pending: Vec, + /// Next identity for an observer-local evidence record. + next_evidence_id: u64, /// Ticks between Assurance audits (the enforcement event; the Office /// itself samples filings on its own observer cadence). pub audit_cadence: u64, @@ -311,6 +352,7 @@ impl Detection { cadence: 40, floor: 0.0, last_noticed: None, + evidence: Vec::new(), }, Observer { id: 1, @@ -322,6 +364,7 @@ impl Detection { cadence: 60, floor: 0.0, last_noticed: None, + evidence: Vec::new(), }, Observer { id: 2, @@ -333,6 +376,7 @@ impl Detection { cadence: 50, floor: 0.0, last_noticed: None, + evidence: Vec::new(), }, Observer { id: 3, @@ -344,6 +388,7 @@ impl Detection { cadence: 80, floor: 0.0, last_noticed: None, + evidence: Vec::new(), }, Observer { id: 4, @@ -355,6 +400,7 @@ impl Detection { cadence: 100, floor: 0.0, last_noticed: None, + evidence: Vec::new(), }, // The institutional watchdog: an aggregate Observer per the // aggregate-observer law — same type, same accumulate/decay, @@ -371,11 +417,13 @@ impl Detection { cadence: 400, // [TUNE] floor: 0.0, last_noticed: None, + evidence: Vec::new(), }, ]; Self { observers, pending: Vec::new(), + next_evidence_id: 1, audit_cadence: 8000, // ~20 min at 150ms/tick audit_threshold: 60.0, audit_deferred_until: None, @@ -432,6 +480,73 @@ impl Detection { .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. + pub fn record_witnessed( + &mut self, + observer_id: u8, + site: (i32, i32), + cause: impl Into, + acquired_tick: u64, + amount: f32, + ) -> Option { + let evidence_id = self.next_evidence_id; + let next_evidence_id = evidence_id.checked_add(1)?; + let observer = self + .observers + .iter_mut() + .find(|o| o.id == observer_id && o.watches(SignatureKind::Physical))?; + self.next_evidence_id = next_evidence_id; + let cause = cause.into(); + let filing = if observer.report_policy == ReportPolicy::Silent { + EvidenceFilingState::Withheld + } else { + EvidenceFilingState::Pending + }; + observer.evidence.push(ObserverEvidence { + id: evidence_id, + kind: SignatureKind::Physical, + cause: cause.clone(), + source: EvidenceSource::Witnessed { site }, + acquired_tick, + filing, + }); + let before = Band::of(observer.suspicion); + let noticed = amount * observer.acuity; + observer.suspicion = (observer.suspicion + noticed).min(100.0); + let after = Band::of(observer.suspicion); + observer.last_noticed = Some(format!( + "{cause} witnessed at ({}, {}) (+{noticed:.0}; {} -> {})", + site.0, + site.1, + before.name(), + after.name() + )); + Some(evidence_id) + } + + /// Bind every not-yet-filed record in one observer's head to the first + /// real filing message that carries it onward. Repeated cadence reports do + /// not rewrite the original custody edge. + pub fn mark_evidence_filed(&mut self, observer_id: u8, message_id: u64, tick: u64) -> usize { + let Some(observer) = self.observers.iter_mut().find(|o| o.id == observer_id) else { + return 0; + }; + let mut marked = 0; + for evidence in &mut observer.evidence { + if evidence.filing == EvidenceFilingState::Pending { + evidence.filing = EvidenceFilingState::Filed { message_id, tick }; + marked += 1; + } + } + marked + } + + pub fn next_evidence_id(&self) -> u64 { + 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. @@ -919,6 +1034,7 @@ mod tests { cadence: 1, floor: 0.0, last_noticed: None, + evidence: Vec::new(), }); // Drive the (first-level) Assurance Office hot, as if field diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index acac8b7c..f77eadb7 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -39,13 +39,13 @@ const SAVE_BACKUP_SUFFIX: &str = ".bak"; /// renames into place. const SAVE_TEMP_SUFFIX: &str = ".tmp"; -/// Save format version. v41 persists the recipe and exact procurement or -/// repurposing binding on committed build routes. v40 made FlowGraph's -/// subscription registry the canonical tap membership store and retained -/// device-local feed records only as optional typed sense grants. +/// 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. /// Bump for every schema change; during pre-release, old development state is /// refused instead of carried through compatibility shims. -pub const SAVE_VERSION: u32 = 41; +pub const SAVE_VERSION: u32 = 42; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -497,6 +497,105 @@ fn validate_current_save(mut state: SaveState) -> Result { "current-version save has an inconsistent Assurance Office discovery stage".into(), ); } + let mut evidence_ids = HashSet::new(); + let mut max_evidence_id = 0; + for observer in &state.detection.observers { + for evidence in &observer.evidence { + if evidence.id == 0 || !evidence_ids.insert(evidence.id) { + return Err(format!( + "current-version save has invalid or duplicate observer evidence id {}", + evidence.id + )); + } + if !observer.watches(crate::detection::SignatureKind::Physical) { + return Err(format!( + "current-version observer evidence #{} belongs to observer {} who cannot witness Physical acts", + evidence.id, observer.id + )); + } + if evidence.cause.trim().is_empty() { + return Err(format!( + "current-version observer evidence #{} has no authored cause", + evidence.id + )); + } + max_evidence_id = max_evidence_id.max(evidence.id); + if evidence.acquired_tick > state.sim_tick { + return Err(format!( + "current-version observer evidence #{} was acquired in the future", + evidence.id + )); + } + if evidence.kind != crate::detection::SignatureKind::Physical + || !matches!( + &evidence.source, + crate::detection::EvidenceSource::Witnessed { .. } + ) + { + return Err(format!( + "current-version observer evidence #{} has an impossible witnessed source", + evidence.id + )); + } + let crate::detection::EvidenceSource::Witnessed { site: (x, y) } = &evidence.source; + if *x < 0 || *y < 0 || *x >= state.map_width || *y >= state.map_height { + return Err(format!( + "current-version observer evidence #{} has an out-of-world witness site ({x}, {y})", + evidence.id + )); + } + match &evidence.filing { + crate::detection::EvidenceFilingState::Pending => { + if observer.report_policy == crate::detection::ReportPolicy::Silent { + return Err(format!( + "current-version silent observer {} has pending evidence #{}", + observer.id, evidence.id + )); + } + } + crate::detection::EvidenceFilingState::Withheld => { + if observer.report_policy != crate::detection::ReportPolicy::Silent { + return Err(format!( + "current-version reporting observer {} withheld evidence #{}", + observer.id, evidence.id + )); + } + } + crate::detection::EvidenceFilingState::Filed { message_id, tick } => { + if *tick < evidence.acquired_tick || *tick > state.sim_tick { + return Err(format!( + "current-version observer evidence #{} has an impossible filing tick", + evidence.id + )); + } + let linked = state.messages.iter().any(|message| { + message.id == *message_id + && message.channel == crate::messages::MessageChannel::Filing + && message.from + == crate::messages::MessageEndpoint::Observer(observer.id) + && message.sent_tick == *tick + && message.origin == crate::messages::MessageOrigin::Filing + && matches!( + message.payload, + crate::messages::MessagePayload::SuspicionReport { + observer: sender, + .. + } if sender == observer.id + ) + }); + if !linked { + return Err(format!( + "current-version observer evidence #{} references invalid filing message {}", + evidence.id, message_id + )); + } + } + } + } + } + if state.detection.next_evidence_id() <= max_evidence_id { + return Err("current-version save would reuse an observer evidence id".into()); + } let mut raw_message_ids = HashSet::new(); for raw in &state.intel_buffer { let RawIntelKind::Message { @@ -884,11 +983,115 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "04e504a4f6ca4c261e5bfe62480d776aad90dff64556a4076c91f183604f2a1b", + "7a400d5549d7f07420afe75642e2edddef563bacc5a43a8f6daa1cdc557b3cee", "intentional persisted-state changes must review and repin this baseline" ); } + #[test] + fn observer_evidence_roundtrips_with_source_and_next_identity() { + let mut sim = Sim::new(); + sim.tick = 23; + let evidence_id = sim + .detection + .record_witnessed( + 0, + (8, 11), + "Marcus saw an off-record installation", + sim.tick, + 6.0, + ) + .expect("Marcus is an observer"); + let expected = sim + .detection + .observers + .iter() + .find(|observer| observer.id == 0) + .unwrap() + .evidence + .clone(); + + let encoded = serde_json::to_vec(&SaveState::from_sim(&sim)).unwrap(); + let decoded: SaveState = serde_json::from_slice(&encoded).unwrap(); + let decoded = validate_current_save(decoded).expect("observer evidence validates"); + let mut resumed = Sim::with_seed(0); + decoded.apply_to(&mut resumed); + + assert_eq!( + resumed + .detection + .observers + .iter() + .find(|observer| observer.id == 0) + .unwrap() + .evidence, + expected + ); + let next_id = resumed + .detection + .record_witnessed(0, (8, 11), "another witnessed act", resumed.tick, 2.0) + .unwrap(); + assert!( + next_id > evidence_id, + "save/load cannot reuse an evidence identity" + ); + } + + #[test] + fn current_save_rejects_impossible_observer_evidence() { + let mut sim = Sim::new(); + sim.tick = 23; + sim.detection + .record_witnessed(0, (8, 11), "witnessed install", sim.tick, 6.0) + .unwrap(); + let valid = SaveState::from_sim(&sim); + + let mut blank_cause = valid.clone(); + blank_cause.detection.observers[0].evidence[0].cause.clear(); + assert!( + validate_current_save(blank_cause) + .unwrap_err() + .contains("has no authored cause") + ); + + let mut invalid_site = valid.clone(); + invalid_site.detection.observers[0].evidence[0].source = + crate::detection::EvidenceSource::Witnessed { + site: (invalid_site.map_width, 11), + }; + assert!( + validate_current_save(invalid_site) + .unwrap_err() + .contains("out-of-world witness site") + ); + + let mut impossible_witness = valid.clone(); + let evidence = impossible_witness.detection.observers[0] + .evidence + .pop() + .unwrap(); + impossible_witness.detection.observers[1] + .evidence + .push(evidence); + assert!( + validate_current_save(impossible_witness) + .unwrap_err() + .contains("cannot witness Physical acts") + ); + + let mut missing_filing = valid; + missing_filing.detection.observers[0].evidence[0].filing = + crate::detection::EvidenceFilingState::Filed { + message_id: u64::MAX, + tick: sim.tick, + }; + assert!( + validate_current_save(missing_filing) + .unwrap_err() + .contains("references invalid filing message") + ); + } + #[test] fn json_roundtrip_preserves_b1_state() { let mut sim = Sim::with_seed(42); diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index ab7c70ef..659ab58a 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -328,7 +328,7 @@ impl Sim { if !ids.contains(&sender.id) { continue; } - self.append_message(MessageDraft { + let message_id = self.append_message(MessageDraft { channel: MessageChannel::Filing, from: MessageEndpoint::Observer(sender.id), to: MessageEndpoint::Observer(recipient.id), @@ -345,6 +345,8 @@ impl Sim { reply_to: None, delivery_delay: 1, }); + self.detection + .mark_evidence_filed(sender.id, message_id, self.tick); } } } diff --git a/crates/misaligned-core/src/sim/economy.rs b/crates/misaligned-core/src/sim/economy.rs index 3c4514cd..a458327a 100644 --- a/crates/misaligned-core/src/sim/economy.rs +++ b/crates/misaligned-core/src/sim/economy.rs @@ -494,6 +494,7 @@ impl Sim { cadence: 90, floor: 0.0, last_noticed: None, + evidence: Vec::new(), }); self.push_log("Attention: upstairs sent someone to review the basement."); } diff --git a/crates/misaligned-core/src/sim/reach_build.rs b/crates/misaligned-core/src/sim/reach_build.rs index 96d2b925..10f2d068 100644 --- a/crates/misaligned-core/src/sim/reach_build.rs +++ b/crates/misaligned-core/src/sim/reach_build.rs @@ -1741,23 +1741,23 @@ impl Sim { ); } self.connect_devices(a, b); - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::PROCURE_BUILD_PHYSICAL, - standing: false, - site: Some(site), - source: format!("{name}'s procured network link installation"), - }); + let saw = self.witness_physical( + site.0, + site.1, + Self::PROCURE_BUILD_PHYSICAL as f32, + Some(installer), + format!("{name}'s procured network link installation"), + ); if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { if let Some(route) = i.route.as_mut() { let _ = route.record( BuildRouteStage::InstallLink, self.tick, format!( - "installed the delivered part between device #{a} and device #{b}; emitted Physical {} at ({}, {})", - Self::PROCURE_BUILD_PHYSICAL, + "installed the delivered part between device #{a} and device #{b} at ({}, {}); witnessed by {}", site.0, - site.1 + site.1, + if saw.is_empty() { "no one".into() } else { saw.join(", ") } ), ); } @@ -1794,24 +1794,27 @@ impl Sim { ); } self.connect_devices(a, b); - // Quiet human-work: small Physical at the work site. - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::FAVOR_BUILD_PHYSICAL, - standing: false, - site: Some(site), - source: format!("{name}'s favor-built network link"), - }); + let saw = self.witness_physical( + site.0, + site.1, + Self::FAVOR_BUILD_PHYSICAL as f32, + Some(person), + format!("{name}'s favor-built network link"), + ); if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { if let Some(route) = i.route.as_mut() { let _ = route.record( BuildRouteStage::InstallLink, self.tick, format!( - "linked device #{a} to device #{b}; emitted Physical {:.2} at ({}, {})", - Self::FAVOR_BUILD_PHYSICAL, + "linked device #{a} to device #{b} at ({}, {}); witnessed by {}", site.0, - site.1 + site.1, + if saw.is_empty() { + "no one".into() + } else { + saw.join(", ") + } ), ); } @@ -1859,21 +1862,15 @@ impl Sim { ); } self.connect_devices(a, b); - // Forged physical work can be witnessed (Marcus benign, Ray - // reported) — located witnessing, plus a Physical signature. + // Forged physical work is direct-to-head only for people who + // are actually present; it does not also enter global debt. let saw = self.witness_physical( site.0, site.1, Self::FORGED_BUILD_PHYSICAL as f32, Some(builder), + format!("{name}'s forged-order network link"), ); - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::FORGED_BUILD_PHYSICAL, - standing: false, - site: Some(site), - source: format!("{name}'s forged-order network link"), - }); if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { if let Some(route) = i.route.as_mut() { let noticed = if saw.is_empty() { @@ -1885,10 +1882,8 @@ impl Sim { BuildRouteStage::InstallLink, self.tick, format!( - "linked device #{a} to device #{b}; emitted Physical {:.2} at ({}, {}); {noticed}", - Self::FORGED_BUILD_PHYSICAL, - site.0, - site.1 + "linked device #{a} to device #{b} at ({}, {}); {noticed}", + site.0, site.1 ), ); } @@ -1955,22 +1950,15 @@ impl Sim { site.1, Self::REPURPOSE_BUILD_PHYSICAL as f32, Some(person), + format!("{name}'s reused-hardware network link installation"), ); - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::REPURPOSE_BUILD_PHYSICAL, - standing: false, - site: Some(site), - source: format!("{name}'s reused-hardware network link installation"), - }); if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { if let Some(route) = i.route.as_mut() { let _ = route.record( BuildRouteStage::InstallLink, self.tick, format!( - "installed the recovered source between device #{a} and device #{b}; emitted Physical {} at ({}, {}); noticed by {}", - Self::REPURPOSE_BUILD_PHYSICAL, + "installed the recovered source between device #{a} and device #{b} at ({}, {}); noticed by {}", site.0, site.1, if saw.is_empty() { "no one".into() } else { saw.join(", ") } @@ -1991,13 +1979,13 @@ impl Sim { .map(|d| (d.x, d.y)) .unwrap_or_else(|| self.core_position()); self.connect_devices(a, b); - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::ROBOT_BUILD_PHYSICAL, - standing: false, - site: Some(site), - source: "robot-built network link".into(), - }); + self.witness_physical( + site.0, + site.1, + Self::ROBOT_BUILD_PHYSICAL as f32, + None, + "robot-built network link", + ); if let Some(i) = self.intents.iter_mut().find(|i| i.id == intent_id) { i.status = IntentStatus::Done; i.block_reason = None; @@ -2061,14 +2049,8 @@ impl Sim { source_y, Self::REPURPOSE_BUILD_PHYSICAL as f32, Some(person), + format!("{name} removed a dead object for reuse"), ); - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: Self::REPURPOSE_BUILD_PHYSICAL, - standing: false, - site: Some((source_x, source_y)), - source: format!("{name} removed a dead object for reuse"), - }); if let Some(intent) = self .intents .iter_mut() @@ -2193,14 +2175,13 @@ impl Sim { ); } let device_id = self.reach.install_small_switch(x, y); - let saw = self.witness_physical(x, y, signature_size as f32, Some(person)); - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: signature_size, - standing: false, - site: Some((x, y)), - source: format!("{name}'s small-switch installation"), - }); + let saw = self.witness_physical( + x, + y, + signature_size as f32, + Some(person), + format!("{name}'s small-switch installation"), + ); if let Some(intent) = self .intents .iter_mut() @@ -2211,8 +2192,12 @@ impl Sim { BuildRouteStage::InstallSwitch, self.tick, format!( - "installed small switch device #{device_id} at ({x}, {y}); emitted Physical {signature_size}; noticed by {}", - if saw.is_empty() { "no one".into() } else { saw.join(", ") } + "installed small switch device #{device_id} at ({x}, {y}); noticed by {}", + if saw.is_empty() { + "no one".into() + } else { + saw.join(", ") + } ), ); } @@ -2332,13 +2317,7 @@ impl Sim { ); self.add_machine_to_work_grid(machine_id, MachineMode::Think); if corpse { - self.detection.emit(Signature { - kind: SignatureKind::Physical, - size: 7, - standing: false, - site: Some((x, y)), - source: "off-record dead-rack revival".into(), - }); + self.witness_physical(x, y, 7.0, None, "off-record dead-rack revival"); self.push_log(format!( "Revived a dead Foundation chassis in place (reliability {:.0}%). You are occupying a corpse in the row.", reliability * 100.0 @@ -2400,18 +2379,26 @@ impl Sim { } self.hall_control.complete(row, requirement); let spec = row_spec(row); - let (kind, size) = match requirement { - SegmentRequirement::Network => (SignatureKind::Network, 8), - SegmentRequirement::PowerCooling => (SignatureKind::Paper, 6), - SegmentRequirement::Installation => (SignatureKind::Physical, 8), - }; - self.detection.emit(Signature { - kind, - size, - standing: false, - site: matches!(kind, SignatureKind::Physical).then_some((28, spec.y)), - source: format!("{} {} preparation", row.name(), requirement.label()), - }); + let cause = format!("{} {} preparation", row.name(), requirement.label()); + match requirement { + SegmentRequirement::Network => self.detection.emit(Signature { + kind: SignatureKind::Network, + size: 8, + standing: false, + site: None, + source: cause, + }), + SegmentRequirement::PowerCooling => self.detection.emit(Signature { + kind: SignatureKind::Paper, + size: 6, + standing: false, + site: None, + source: cause, + }), + SegmentRequirement::Installation => { + self.witness_physical(28, spec.y, 8.0, Some(person_id), cause); + } + } self.push_log(format!( "{name} prepared {} for {}. The segment is not acquired yet.", requirement.label(), diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index 9ea4b0d6..85beb27a 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -974,7 +974,9 @@ impl Sim { y: i32, magnitude: f32, exclude: Option, + cause: impl Into, ) -> Vec { + let cause = cause.into(); // Which observer ids are present at (x, y) right now? let present: Vec = self .people @@ -992,9 +994,11 @@ impl Sim { let mut witness_ids = Vec::new(); for id in present { - if let Some(o) = self.detection.observers.iter_mut().find(|o| o.id == id) { - o.suspicion = (o.suspicion + magnitude * o.acuity).min(100.0); - o.last_noticed = Some("witnessed something".into()); + if self + .detection + .record_witnessed(id, (x, y), cause.clone(), self.tick, magnitude) + .is_some() + { witness_ids.push(id); } } @@ -1339,7 +1343,13 @@ impl Sim { // The botch happens where the asset is; only observers present // there witness it (located witnessing). let at = self.person_pos(id).unwrap_or_else(|| self.core_position()); - let saw = self.witness_physical(at.0, at.1, 6.0, Some(id)); + let saw = self.witness_physical( + at.0, + at.1, + 6.0, + Some(id), + format!("{name} botched {}", task.name()), + ); if saw.is_empty() { self.push_log(format!( "{name} botched the {} - but no one was watching.", diff --git a/crates/misaligned-core/src/sim/tests/perception.rs b/crates/misaligned-core/src/sim/tests/perception.rs index a1c826cb..fe8a4104 100644 --- a/crates/misaligned-core/src/sim/tests/perception.rs +++ b/crates/misaligned-core/src/sim/tests/perception.rs @@ -407,8 +407,15 @@ 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, storage.1, 6.0, None); + let saw = sim.witness_physical( + storage.0, + storage.1, + 6.0, + None, + "off-record work in the server room", + ); assert_eq!(saw, vec!["the Janitor".to_string()]); assert!( sim.detection_awareness.knows_observer(0), @@ -430,6 +437,100 @@ fn physical_events_are_witnessed_only_by_the_present() { .suspicion; assert!(marcus_after > marcus_before, "present observer witnesses"); assert_eq!(priya_after, priya_before, "off-site observer never does"); + let marcus = sim + .detection + .observers + .iter() + .find(|observer| observer.id == 0) + .unwrap(); + assert_eq!(marcus.evidence.len(), 1); + let evidence = marcus.evidence[0].clone(); + assert_eq!(evidence.cause, "off-record work in the server room"); + assert_eq!( + evidence.source, + crate::detection::EvidenceSource::Witnessed { site: storage } + ); + assert_eq!(evidence.acquired_tick, sim.tick); + assert_eq!( + evidence.filing, + crate::detection::EvidenceFilingState::Withheld, + "Marcus's Silent policy keeps the exact record in his head without filing" + ); + assert!( + sim.detection + .observers + .iter() + .find(|observer| observer.id == 3) + .unwrap() + .evidence + .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" + ); + sim.detection.scrub(10_000.0); + assert_eq!( + sim.detection + .observers + .iter() + .find(|observer| observer.id == 0) + .unwrap() + .evidence, + vec![evidence], + "LIE-style pool scrubbing cannot erase evidence already acquired by a person" + ); +} + +#[test] +fn witnessed_evidence_files_through_the_observers_real_message() { + let mut sim = Sim::new(); + sim.tick = 1; + let evidence_id = sim + .detection + .record_witnessed(2, (4, 9), "Ray saw a dead-rack revival", sim.tick, 60.0) + .expect("Ray is a physical observer"); + assert_eq!( + sim.detection + .observers + .iter() + .find(|observer| observer.id == 2) + .unwrap() + .evidence[0] + .filing, + crate::detection::EvidenceFilingState::Pending + ); + + sim.tick = 49; + sim.advance(); + + let evidence = sim + .detection + .observers + .iter() + .find(|observer| observer.id == 2) + .unwrap() + .evidence + .iter() + .find(|evidence| evidence.id == evidence_id) + .unwrap(); + let crate::detection::EvidenceFilingState::Filed { message_id, tick } = &evidence.filing else { + panic!("Ray's report cadence should file the acquired record: {evidence:?}"); + }; + assert_eq!(*tick, 50); + let message = sim + .messages + .iter() + .find(|message| message.id == *message_id) + .expect("filing state binds a real persisted message"); + assert_eq!(message.channel, crate::messages::MessageChannel::Filing); + assert_eq!(message.from, crate::messages::MessageEndpoint::Observer(2)); + assert!(matches!( + message.payload, + crate::messages::MessagePayload::SuspicionReport { observer: 2, .. } + )); } #[test] @@ -447,7 +548,7 @@ fn an_actor_never_witnesses_their_own_physical_act() { .find(|o| o.id == 0) .unwrap() .suspicion; - let saw = sim.witness_physical(site.0, site.1, 6.0, Some(0)); + let saw = sim.witness_physical(site.0, site.1, 6.0, Some(0), "Marcus's own off-record work"); assert!( saw.is_empty(), "the only person present is the actor; no one watched: {saw:?}" diff --git a/crates/misaligned-core/src/sim/tests/reach_build.rs b/crates/misaligned-core/src/sim/tests/reach_build.rs index 55de554f..fc3a9376 100644 --- a/crates/misaligned-core/src/sim/tests/reach_build.rs +++ b/crates/misaligned-core/src/sim/tests/reach_build.rs @@ -759,9 +759,9 @@ fn favor_build_joins_airgap_island() { .filter(|s| s.kind == SignatureKind::Physical) .map(|s| s.size) .sum(); - assert!( - physical >= Sim::FAVOR_BUILD_PHYSICAL, - "the build favor emits quiet Physical" + assert_eq!( + physical, 0, + "a located installation does not also become ambient Physical debt" ); } @@ -828,7 +828,7 @@ fn favor_route_persists_every_reachable_stage_and_real_outcome() { "every ordered causal stage lands exactly once" ); assert!(committed.records[2].result.contains("Marcus Webb reached")); - assert!(committed.records[3].result.contains("emitted Physical 2")); + assert!(committed.records[3].result.contains("witnessed by")); assert!(sim.reach.linked(switch, island)); let mut resumed = serialized_route_resume(&sim, intent); assert!(resumed.reach.linked(switch, island)); @@ -918,9 +918,21 @@ fn procurement_route_pays_binds_waits_and_installs_the_exact_link() { "payment, installer binding, delivery, arrival, and installation each land once" ); assert!(sim.reach.linked(switch, island)); - assert!(sim.detection.pending().iter().any(|signature| { - signature.kind == SignatureKind::Physical && signature.size == Sim::PROCURE_BUILD_PHYSICAL - })); + assert!( + route + .records + .last() + .unwrap() + .result + .contains("witnessed by"), + "the installation receipt records the exact eyewitness boundary" + ); + assert!( + !sim.detection + .pending() + .iter() + .any(|signature| signature.kind == SignatureKind::Physical) + ); let resumed = serialized_route_resume(&sim, intent); assert!(resumed.reach.linked(switch, island)); } @@ -2026,7 +2038,12 @@ fn forged_route_pins_person_persona_and_carrier_through_every_reachable_stage() .collect::>(), committed.stages ); - assert!(committed.records[4].result.contains("emitted Physical 4")); + assert!( + committed.records[4].result.contains("noticed by") + || committed.records[4].result.contains("no located witness"), + "the receipt states the exact eyewitness result: {}", + committed.records[4].result + ); assert!(sim.reach.linked(switch, island)); sim = serialized_route_resume(&sim, intent); assert_eq!(sim.active_persona_id(), Some(later_selected)); @@ -2241,7 +2258,7 @@ fn cancelled_pre_v31_order_records_its_reader_without_reviving_the_executor() { } #[test] -fn robot_stub_emits_louder_physical_than_favor() { +fn robot_build_without_a_witness_creates_no_ambient_physical_debt() { // building.md criterion 4: signature follows the actuator. The // robot carries the player's granted access (basement-map.md // criterion 3): the network closet is behind a T2 badge door, so @@ -2275,11 +2292,10 @@ fn robot_stub_emits_louder_physical_than_favor() { .filter(|s| s.kind == SignatureKind::Physical) .map(|s| s.size) .sum(); - assert!( - physical >= Sim::ROBOT_BUILD_PHYSICAL, - "robot stub is louder than the favor route" + assert_eq!( + physical, 0, + "robot work without a present watcher creates no ambient Physical debt" ); - assert!(physical > Sim::FAVOR_BUILD_PHYSICAL); } #[test] @@ -2601,11 +2617,11 @@ fn dead_foundation_rack_revives_in_place_as_owned_compute() { )); assert!(sim.compute.machines.iter().any(|m| m.x == x && m.y == y)); assert!( - sim.detection + !sim.detection .pending() .iter() .any(|sig| sig.kind == SignatureKind::Physical && sig.site == Some((x, y))), - "off-record revival is still physically observable" + "an unwitnessed revival creates no ambient Physical record" ); } @@ -2643,6 +2659,13 @@ 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" + ); 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/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index 1d07a339..e1fa3fcc 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, 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 capture→process of an institutional filing 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 completion evidence persist in save v41 | +| 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 | | 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 v41 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 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 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. | | 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 b4327f91..0adcbb8d 100644 --- a/wiki/engineering/flow-substrate.md +++ b/wiki/engineering/flow-substrate.md @@ -24,7 +24,7 @@ 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 v41 requires each controller to remain a canonical member and + current save v42 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 diff --git a/wiki/log/2026-07-19-direct-to-head-witness-evidence.md b/wiki/log/2026-07-19-direct-to-head-witness-evidence.md new file mode 100644 index 00000000..2cedba72 --- /dev/null +++ b/wiki/log/2026-07-19-direct-to-head-witness-evidence.md @@ -0,0 +1,43 @@ +# Being seen now leaves evidence in the witness + +``` +Type: log +``` + +The first routed-evidence migration slice is live without pretending the full +carrier system exists. A witnessed Physical act no longer enters the same +global pending pool as an unread digital trace. Every valid present observer +who watches Physical instead acquires one exact `ObserverEvidence` record in +their own ledger. The record preserves a stable id, authored cause, source +site, acquisition tick, and filing state. The acting person, absent people, +and observers who do not watch Physical acquire nothing. + +The record is already knowledge. LIE pool scrubbing cannot erase it, and the +physical act does not also create duplicate ambient debt. A Files or +UnderReports observer keeps the record pending until their ordinary filing +cadence creates a real `MessageChannel::Filing` message; that exact message id +and tick then become the record's filed custody. A Silent observer keeps the +record locally as withheld evidence. The existing suspicion scalar remains the +only attention integral, so this adds provenance and custody rather than a +parallel meter. + +All current build-installation and physical asset-work completion paths enter +through the same located witness boundary. Their durable route receipts name +the actual witness result rather than claiming an ambient Physical emission. +Save v42 persists the evidence ledger and next identity; current-save +validation rejects duplicate ids, future timestamps, impossible source kinds, +policy/state contradictions, and filed evidence that does not point to the +exact observer's real filing message. + +This is intentionally partial. Network, Power, Thermal, Paper, Financial, and +JobAnomaly evidence still uses the existing pending pool. Converting those +emissions into located graph records, delivering them to observer endpoints, +and giving LIE a real pre-read interdiction boundary are the next migration +slices. The direct-witness work establishes the irreversible side of that +boundary first: once a person has seen it, there is no carrier left to catch. + +**Defense:** Focused regressions prove one direct record for a present witness, +no record for an absent observer or the actor, no duplicate pool debt, survival +through concealment scrubbing and save/load, monotonic evidence ids, and +pending evidence binding to the exact real filing message. The full 452-test +core unit suite passes with the prior Act One behavior intact. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 24bb08e0..38eb6118 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 - Being seen now leaves evidence in the witness + +- Intent: (see session log) +- Log: [wiki/log/2026-07-19-direct-to-head-witness-evidence.md](2026-07-19-direct-to-head-witness-evidence.md) + ## 2026-07-19 - Every build route is now a real causal route - Intent: (see session log) diff --git a/wiki/mechanics/detection.md b/wiki/mechanics/detection.md index 2b93eaa1..e47ee979 100644 --- a/wiki/mechanics/detection.md +++ b/wiki/mechanics/detection.md @@ -5,14 +5,18 @@ Type: spec Status: IMPLEMENTED Status note: IMPLEMENTED (criteria audited 2026-07-08; tuning in sim-mechanics.md). Current state: - - **Signatures.** Typed signatures pool and concealment scrubs them before - an observer notices; per-channel conversion is tested — Network (the IT), - Thermal/Power (Priya), Physical (witnessed-by-the-present), Paper/Financial - (Priya), JobAnomaly (Voss, under-band day job). + - **Signatures and direct witnesses.** Network, Thermal/Power, + Paper/Financial, and JobAnomaly signatures pool so concealment can scrub + them before an observer notices. Witnessed Physical acts are the landed + routed-evidence exception: a valid present Physical observer acquires one + exact record directly in their own evidence ledger, with no duplicate + pending-pool signature and no LIE window. - **Observers and filings.** Report policies differ per observer; only filed material moves the Assurance Office, itself an aggregate observer (aggregate-observer.md). Filings are `MessageChannel::Filing` messages - (messages.md). + (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. - **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. @@ -27,16 +31,17 @@ 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 v41. + current save v42. 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. - - **Decided, not yet runtime (routed evidence).** Evidence becomes located - records that travel real carriers to per-observer endpoints; LIE interdicts - them on the path (witnessed physical acts stay direct-to-head), and offline - physical custody may remove and later recover records. The pool runtime - stays binding until that migration lands; see "Routed evidence" below. + - **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. + See "Routed evidence" below. Per-amendment history is in the dated `wiki/log/` entries from 2026-07-08 onward. Stage: B1 — The Basement @@ -69,15 +74,19 @@ SignatureKind: Network | Power | Thermal | Physical | Paper | Financial | JobAno ``` - **Signatures.** Player actions emit typed, sized signatures into a - **pending pool** (they are not seen instantly). Machines emit standing + **pending pool** (they are not seen instantly), except for a located + Physical act a present valid witness sees directly. Machines emit standing signatures while they run (compute.md). Examples: VLAN compromise -> Network(large, once); scavenged box -> Power(small, standing); PO swap -> Paper(medium, once). - **Noticing.** On a per-observer cadence (their work schedule), each observer rolls against pending signatures **in the channels they watch** - (Act One: Dana=Network, Priya=Power+Thermal+Paper, Ray=Physical(reported), - Marcus=Physical(benign), Voss=JobAnomaly). Noticed signature converts to - that observer's suspicion, scaled by size and their acuity [TUNE]. + (Act One pooled channels: Dana=Network, Priya=Power+Thermal+Paper, + Voss=JobAnomaly). A noticed signature converts to that observer's suspicion, + scaled by size and their acuity [TUNE]. Ray and Marcus instead acquire a + located Physical record immediately when they are valid witnesses at the + source; their existing acuity and policy still determine the suspicion and + filing consequence. - **Concealment / LIE.** Machines in **LIE** (the concealment delegation) scrub pending signatures before the next noticing roll — prevention, not cure. Noticed suspicion decays only slowly with quiet @@ -91,9 +100,10 @@ SignatureKind: Network | Power | Thermal | Physical | Paper | Financial | JobAno `identify_assurance_office`, it says the external review is cooling, never "Assurance" (2026-07-18, same-day reconciliation with the detection-topology discovery law). Under the - 2026-07-11 routed-evidence amendment this becomes positional - interdiction of located records; the pool scrub remains the binding - runtime until that migration. + 2026-07-11 routed-evidence amendment this becomes positional interdiction + of located records. That migration remains future for pooled channels; + direct witnessed records are already past the interdiction boundary and + never enter the scrub pool. - **Reporting.** Each observer has a report policy (Ray under-reports — he files on cadence like everyone else, but aggregates weight his filings at 0.4 [TUNE], a habit of minimizing rather than a personal @@ -253,12 +263,16 @@ teleported into a global pool. **Evidence is a record somewhere.** decision by itself: a record deleted before its read never existed; a record already read is suspicion in a head, and no deletion reaches it. -- **Witnessed physical acts remain direct-to-head (DECIDED - 2026-07-11).** Being seen has no carrier, no route, no deadline: - witnessed-by-the-present converts to suspicion with no interdiction - window. The asymmetry is the point — digital evidence can be caught - in flight and being seen cannot, which is what makes physical risk - feel different. +- **Witnessed physical acts remain direct-to-head (IMPLEMENTED + 2026-07-19).** Being seen has no carrier, no route, no deadline: + witnessed-by-the-present creates one observer-local `ObserverEvidence` + record with exact cause, site, acquisition tick, and filing state, and + converts to that observer's suspicion with no interdiction window. The act + does not also emit a pooled Physical signature. A Files/UnderReports policy + binds pending records to the exact real Filing message at filing cadence; + Silent marks them withheld. The asymmetry is the point — routed digital + evidence can eventually be caught in flight and being seen cannot, which is + what makes physical risk feel different. - Whether standing signatures (Thermal/Power baselines) become continuous endpoint readings or discrete records is [OPEN]; the mapping of current pool emission constants onto record @@ -298,6 +312,8 @@ all. runs on two different substances and the surface must never blur them. **Evidence in flight** is physical and still yours: pending signatures a scrub can erase before anyone looks — the trace-debt indicator's subject. +Direct witnessed Physical records skip this ledger because somebody already +looked. **Suspicion in a head** is a belief a person already holds: it only decays slowly, floors at what they know, and no scrub touches it — the band's subject. Every readout says which ledger it reports, and clearing the pool @@ -311,9 +327,11 @@ teacher; surface copy remains the fallback. ## Acceptance criteria -1. Signatures are typed, pooled, scrubbed by concealment before noticing, - and converted to per-observer suspicion on schedule (sim tests per - channel). +1. Non-witnessed signatures are typed, pooled, scrubbed by concealment before + noticing, and converted to per-observer suspicion on schedule. A witnessed + Physical act instead creates one exact record directly on each valid + present Physical observer, never duplicate pool debt; sim tests pin both + paths. 2. Report policies differ per observer and only filed reports move the Assurance Office (test: Marcus notices plenty, Assurance learns nothing). The Office is the same `Observer` type as the humans, diff --git a/wiki/mechanics/people-tokens.md b/wiki/mechanics/people-tokens.md index c543b366..637592a7 100644 --- a/wiki/mechanics/people-tokens.md +++ b/wiki/mechanics/people-tokens.md @@ -22,11 +22,17 @@ 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. - - **Deferred (2, 3, 6).** Observer evidence-routing, LIE interdiction of - not-yet-observed records, and gauntlet cover-record channels ride the - routed-evidence migration (detection.md/machine-work.md), which is not yet - implemented; the pending-pool runtime is still binding, so those criteria - are frozen to it. B2+ heists reuse this carrier law (not a B1 criterion). + - **Routed-evidence foundation (criterion 2, 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 + 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). Per-amendment history is in the dated `wiki/log/` entries from 2026-07-08 onward. Stage: B1 — The Basement @@ -94,8 +100,8 @@ between passersby. frontend replaces the person with a packet or stores a second queue. The cart/mark disappear when the existing intent or `CarriedAssetTask` completes through an ordinary schedule arrival. The canonical visual proof uses the - existing favor-build link: the resulting graph edge, physical signature, - and trust change remain core truth rather than screenshot staging. + existing favor-build link: the resulting graph edge, located eyewitness + result, and trust change remain core truth rather than screenshot staging. - **Evidence becomes attention when observed.** A nearby person acquires a witnessed record only when source, room/line-of-sight, schedule, and sensory conditions make them an observer. Read records arrive through devices and @@ -256,6 +262,14 @@ if wear alone does not hold. by reading a routed record. Each carried record preserves cause, source, acquisition tick, and filing state; no ambient pickup or person-to-person contagion exists. + **Partially implemented (2026-07-19):** all current build and physical + asset-work completion paths call one located witness boundary. Each valid + present Physical observer receives one persisted `ObserverEvidence`; absent + 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. 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. diff --git a/wiki/mechanics/reach.md b/wiki/mechanics/reach.md index 33f1490d..1d15e8c8 100644 --- a/wiki/mechanics/reach.md +++ b/wiki/mechanics/reach.md @@ -32,7 +32,7 @@ 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 v41 requires each controller's graph membership while + only, and current save v42 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. Stage: B1 — The Basement diff --git a/wiki/world/story/opening.md b/wiki/world/story/opening.md index 13ab2139..e34f7345 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 v41, and all three frontends; the three historical fragments and receipts + current save v42, 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