//! The renderer-neutral Operations workspace projection //! (wiki/interface/operations-workspace.md). //! //! One read-only view of the durable strategic surface — processed intel, //! earned people, known books and flows, named schemes, and active //! commitments — consumed by terminal, Bevy, and agent mode. Every object //! carries a stable semantic target, knowledge-gated labels, facts, //! provenance, progress, and canonical [`ActionDesc`] rows bound to the //! exact [`ActionCommand`] that execution dispatches. //! //! The projection adds no rules. Action kind, cost/gain, signature preview, //! active/control role, and blocked reason come from the same core legality //! helpers (`Sim::available_actions` and its per-anchor builders). Execution //! routes back through `Sim::execute_action`; human and agent frontends must //! dispatch these bound rows rather than reconstructing a command or choosing //! an implicit latest object. use crate::account::{AccountFlowId, Position, PositionId, PositionOutcome}; use crate::actions::{ActionCommand, ActionCost, ActionDesc, Anchor}; use crate::detection::Band; use crate::income::EgressRoute; use crate::intel::{ IntelCustodyKind, IntelKind, IntelPolicyMatch, IntelPolicyOutcome, IntelPolicyRule, IntelRoutineClass, ProcessedIntel, RawIntelEvent, ReportLotToken, }; use crate::messages::{MessageChannel, MessageOrigin, MessagePayload, MessageStatus}; use crate::person::Knowledge; use crate::persona::{self, PersonaId, PersonaLifecycle}; use crate::plot::{PlotRun, PlotState}; use crate::reach::Party; use crate::sim::Sim; use crate::sinks::{SinkFireEffect, SinkKind}; /// Which named standing scheme a card represents (income.md). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SchemeKind { Moonlight, Wager, } /// The stable semantic target of one Operations object. Frontends never /// infer eligibility from map coordinates; they hand this back to the lib /// to resolve the bound row. The wrappers reuse existing stable state /// (processed `raw_id`, account/flow id, scheme kind, person id, and the /// append-only plot-run position) rather than introducing a parallel model. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum OperationsTarget { /// One processed intel item (intel.md). Intel { raw_id: u64 }, /// One canonical source/class routine stream. IntelStream { stream_id: u64 }, /// One stream's exact versioned open report-lot snapshot. IntelLot { token: ReportLotToken }, /// A subject/class knowledge index over canonical streams. It owns no /// independently saleable custody. IntelKnowledge { person: u8, class: IntelRoutineClass, }, /// One stable node in the recursive custody/policy tree. IntelCustody { node_id: u64 }, /// One opaque, unprocessed recording in the pooled host inbox. The id is /// selectable without exposing the recording's hidden payload. RawRecording { raw_id: u64 }, /// The one pooled host recording inbox (intel.md). RecordingInbox, /// An earned person dossier (social.md). Person(u8), /// One named public identity (personas.md). Persona(PersonaId), /// One immutable public-identity protocol available for instantiation. PersonaArchetype(String), /// One known account node (economy.md). Account(u32), /// The captured Lab books / ledger (economy.md). Books, /// One known account flow (economy.md). Flow(AccountFlowId), /// A named standing scheme card (income.md). Scheme(SchemeKind), /// A submitted plot whose Thought reservoir has not fired yet. PlotSubmission { person: u8, plot_id: String }, /// One in-flight, held, or completed plot run (plots.md). Plot runs are /// append-only, so their vector position is a stable runtime target. ActivePlotRun { index: usize }, /// One Wager micro-position (income.md). WagerPosition(PositionId), /// The institutional aggregate observer's dossier card /// (aggregate-observer.md player surface). Public record from the /// start; always named. AssuranceOffice, } /// The lifecycle state of one object, rendered identically by every /// frontend. Unknown objects and unearned routes are absent entirely; a /// known-but-unavailable route stays visible with its exact reason on the /// bound row. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ObjectState { Available, Sold, Pending, Running, Held, Stopped, Completed, Failed, /// The honest pre-capture empty state (e.g. `NO BOOKS CAPTURED`). Empty, /// Captured but not yet processed (e.g. ledger traffic awaiting REVIEW). Captured, } /// One earned causal edge from the selected object to another Operations /// object. Links carry stable semantic targets; frontends hand the target /// back to the shared workspace rather than guessing a destination. #[derive(Debug, Clone, PartialEq, Eq)] pub struct OperationsLink { pub relation: &'static str, pub label: String, pub target: OperationsTarget, } /// The urgency of one view-strip badge. Text always carries the meaning; /// severity only supplies a restrained presentation emphasis. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PressureLevel { Notice, Warning, AtRisk, } /// A meaningful attention cue for one view. These are state summaries such /// as waiting work, a held decision, or a missing prerequisite — never raw /// catalog counts. #[derive(Debug, Clone, PartialEq, Eq)] pub struct OperationsPressure { pub view: OperationsView, pub label: String, pub level: PressureLevel, } /// One selectable Operations object: a stable target plus the /// facts/provenance/progress and canonical bound rows that explain it. /// Frontends format this; they do not reconstruct eligibility, cost, or /// reasons. #[derive(Debug, Clone, PartialEq)] pub struct OperationsObject { pub target: OperationsTarget, pub label: String, pub state: ObjectState, /// How the player knows this: the feed, person, source, account, route, /// actuator, or channel where it was earned. pub provenance: Vec, /// Current facts in their real units (balances, timers, payout, /// probability to earned precision). pub facts: Vec, /// Progress through completed/current beats, timers, or held state. pub progress: Vec, /// Earned causal neighbors: evidence, subject, source, owning object, or /// active consequence. No edge may reveal an unearned object. pub related: Vec, /// Canonical bound rows. Disabled rows carry their exact reason; agent /// mode retains them, human surfaces filter them. pub actions: Vec, } /// The full Operations workspace: five persistent views sharing one /// interaction grammar and one projection. Object order is stable and /// pinned by cross-frontend tests. #[derive(Debug, Clone, PartialEq)] pub struct OperationsProjection { pub intel: Vec, pub people: Vec, pub personas: Vec, pub accounts: Vec, pub schemes: Vec, pub active: Vec, pub pressure: Vec, } /// The five persistent top-level views, in their fixed strip order. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OperationsView { Intel, People, Personas, Accounts, Schemes, Active, } impl OperationsView { pub const ALL: [OperationsView; 6] = [ OperationsView::Intel, OperationsView::People, OperationsView::Personas, OperationsView::Accounts, OperationsView::Schemes, OperationsView::Active, ]; pub fn title(self) -> &'static str { match self { OperationsView::Intel => "INTEL", OperationsView::People => "PEOPLE", OperationsView::Personas => "PERSONAS", OperationsView::Accounts => "ACCOUNTS", OperationsView::Schemes => "SCHEMES", OperationsView::Active => "ACTIVE", } } pub fn next(self) -> Self { let i = Self::ALL.iter().position(|v| *v == self).unwrap_or(0); Self::ALL[(i + 1) % Self::ALL.len()] } pub fn prev(self) -> Self { let i = Self::ALL.iter().position(|v| *v == self).unwrap_or(0); Self::ALL[(i + Self::ALL.len() - 1) % Self::ALL.len()] } } impl OperationsProjection { /// One view's stable object list. pub fn view(&self, view: OperationsView) -> &[OperationsObject] { match view { OperationsView::Intel => &self.intel, OperationsView::People => &self.people, OperationsView::Personas => &self.personas, OperationsView::Accounts => &self.accounts, OperationsView::Schemes => &self.schemes, OperationsView::Active => &self.active, } } /// Find one target's canonical object: its owning view first, ACTIVE /// last, so an ACTIVE entry resolves back to its semantic home when one /// exists. pub fn object(&self, target: &OperationsTarget) -> Option<(OperationsView, usize)> { let mut order = vec![target.home_view()]; for view in OperationsView::ALL { if !order.contains(&view) { order.push(view); } } for view in order { if let Some(index) = self.view(view).iter().position(|o| o.target == *target) { return Some((view, index)); } } None } pub fn pressure(&self, view: OperationsView) -> Option<&OperationsPressure> { self.pressure.iter().find(|badge| badge.view == view) } } impl OperationsTarget { /// The view that owns this target's canonical detail. pub fn home_view(&self) -> OperationsView { match self { OperationsTarget::Intel { .. } | OperationsTarget::IntelStream { .. } | OperationsTarget::IntelLot { .. } | OperationsTarget::IntelKnowledge { .. } | OperationsTarget::IntelCustody { .. } | OperationsTarget::RawRecording { .. } | OperationsTarget::RecordingInbox => OperationsView::Intel, OperationsTarget::Person(_) | OperationsTarget::AssuranceOffice => { OperationsView::People } OperationsTarget::Persona(_) | OperationsTarget::PersonaArchetype(_) => { OperationsView::Personas } OperationsTarget::Account(_) | OperationsTarget::Books | OperationsTarget::Flow(_) => { OperationsView::Accounts } OperationsTarget::Scheme(_) => OperationsView::Schemes, OperationsTarget::PlotSubmission { .. } | OperationsTarget::ActivePlotRun { .. } | OperationsTarget::WagerPosition(_) => OperationsView::Active, } } /// The stable opaque suffix agent mode prints on event lines and accepts /// back as an `actions`/`act` target (agent-play.md): ` @intel(id)`, /// ` @scheme(id)`, ` @run(id)`, ` @account(id)`, ` @flow(id)`. pub fn agent_suffix(&self) -> Option { match self { OperationsTarget::Intel { raw_id } => Some(format!("@intel({raw_id})")), OperationsTarget::IntelStream { stream_id } => Some(format!("@stream({stream_id})")), OperationsTarget::IntelLot { token } => Some(format!("@lot({})", token.label())), OperationsTarget::IntelKnowledge { person, class } => Some(format!( "@knowledge({person}:{})", match class { IntelRoutineClass::Sighting => "sighting", IntelRoutineClass::Schedule => "schedule", } )), OperationsTarget::IntelCustody { node_id } => Some(format!("@custody({node_id})")), OperationsTarget::RawRecording { raw_id } => Some(format!("@recording({raw_id})")), OperationsTarget::RecordingInbox => Some("@intel(inbox)".into()), OperationsTarget::Person(id) => Some(format!("@person({id})")), OperationsTarget::Persona(id) => Some(format!("@persona({id})")), OperationsTarget::PersonaArchetype(id) => Some(format!("@archetype({id})")), OperationsTarget::Account(id) => Some(format!("@account({id})")), OperationsTarget::Books => Some("@account(books)".into()), OperationsTarget::Flow(id) => Some(format!("@flow({id})")), OperationsTarget::Scheme(SchemeKind::Moonlight) => Some("@scheme(moonlight)".into()), OperationsTarget::Scheme(SchemeKind::Wager) => Some("@scheme(wager)".into()), OperationsTarget::ActivePlotRun { index } => Some(format!("@run({index})")), // A submission's stable strategic home before its run exists is // the bound person; a position's is the Wager card. OperationsTarget::PlotSubmission { person, .. } => Some(format!("@person({person})")), OperationsTarget::WagerPosition(_) => Some("@scheme(wager)".into()), OperationsTarget::AssuranceOffice => Some("@assurance".into()), } } } impl Sim { /// The one renderer-neutral Operations projection. Read-only: rendering /// it changes no sim/save state and never advances or pauses a tick. pub fn operations_projection(&self) -> OperationsProjection { OperationsProjection { intel: self.intel_view(), people: self.people_view(), personas: self.personas_view(), accounts: self.accounts_view(), schemes: self.schemes_view(), active: self.active_view(), pressure: self.operations_pressure(), } } /// Resolve one strategic target to its current projected object (event /// links and agent `actions ` share this route). pub fn operations_object(&self, target: &OperationsTarget) -> Option { match target { OperationsTarget::RawRecording { raw_id } => { return self .intel_buffer .iter() .find(|event| event.id == *raw_id) .map(|event| self.raw_recording_object(event)); } OperationsTarget::IntelStream { stream_id } => { return self .intel_streams .iter() .find(|stream| stream.id == *stream_id) .map(|stream| self.intel_stream_object(stream)); } OperationsTarget::IntelLot { token } => { return self .intel_streams .iter() .find(|stream| stream.id == token.stream_id) .and_then(|stream| self.report_lot_object(stream, *token)); } OperationsTarget::IntelKnowledge { person, class } => { return self.intel_knowledge_object(*person, *class); } OperationsTarget::IntelCustody { node_id } => { return self.intel_custody_object(*node_id); } _ => {} } let projection = self.operations_projection(); projection .object(target) .map(|(view, index)| projection.view(view)[index].clone()) } /// The real map body carrying a strategic object, when the player knows /// one. This is navigation metadata only: focusing it never executes the /// action or opens the missing route. Strategic events still link to the /// semantic object first rather than receiving a dishonest tile address. pub fn operations_actuator(&self, target: &OperationsTarget) -> Option { match target { OperationsTarget::RawRecording { .. } | OperationsTarget::IntelStream { .. } | OperationsTarget::IntelLot { .. } | OperationsTarget::IntelKnowledge { .. } | OperationsTarget::IntelCustody { .. } | OperationsTarget::RecordingInbox => { let (x, y) = self.core_position(); Some(Anchor::Tile { x, y }) } OperationsTarget::Account(_) | OperationsTarget::Books | OperationsTarget::Flow(_) => { self.known_switch_anchor() } OperationsTarget::Scheme(_) | OperationsTarget::WagerPosition(_) if self.egress() != Some(EgressRoute::Sanctioned) => { self.known_switch_anchor() } _ => None, } } /// Earned player-facing name for [`Sim::operations_actuator`]. pub fn operations_actuator_label(&self, target: &OperationsTarget) -> Option { if matches!( target, OperationsTarget::RawRecording { .. } | OperationsTarget::IntelStream { .. } | OperationsTarget::IntelLot { .. } | OperationsTarget::IntelKnowledge { .. } | OperationsTarget::IntelCustody { .. } | OperationsTarget::RecordingInbox ) { return self .compute .machines .iter() .find(|machine| machine.id == self.core.host_machine) .map(|machine| machine.name.clone()); } match self.operations_actuator(target)? { Anchor::Device(id) => self.reach.device(id).map(|device| device.name.clone()), _ => None, } } fn known_switch_anchor(&self) -> Option { let id = self.switch_id()?; self.reach .device(id) .is_some_and(|device| device.known) .then_some(Anchor::Device(id)) } fn switch_id(&self) -> Option { self.reach .devices .iter() .find(|d| d.is_switch) .map(|d| d.id) } pub(crate) fn financial_carrier_subscribed(&self) -> bool { let Some(id) = self.switch_id() else { return false; }; self.reach.device(id).is_some_and(|d| { d.carries_message_channel(MessageChannel::Financial) && d.subscribed_by(Party::Player) }) } fn operations_pressure(&self) -> Vec { let mut out = Vec::new(); let waiting = self.intel_buffer.len(); if waiting > 0 { let at_risk = waiting + 1 >= Sim::INTEL_BUFFER_CAPACITY; out.push(OperationsPressure { view: OperationsView::Intel, label: if at_risk { "AT RISK".into() } else { "NEW".into() }, level: if at_risk { PressureLevel::AtRisk } else { PressureLevel::Notice }, }); } let held_people = self .plot_runs .iter() .filter(|run| matches!(run.state, PlotState::WaitingForChoice { .. })) .count(); if held_people > 0 { out.push(OperationsPressure { view: OperationsView::People, label: "HELD".into(), level: PressureLevel::Warning, }); } if self.financial_carrier_subscribed() && self.accounts.known_flows().next().is_none() { out.push(OperationsPressure { view: OperationsView::Accounts, label: "READY".into(), level: PressureLevel::Notice, }); } if self.egress().is_none() { out.push(OperationsPressure { view: OperationsView::Schemes, label: "NO EGRESS".into(), level: PressureLevel::Warning, }); } let active = self.active_view(); let held = active .iter() .filter(|object| object.state == ObjectState::Held) .count(); let live = active .iter() .filter(|object| { matches!( object.state, ObjectState::Pending | ObjectState::Running | ObjectState::Held ) }) .count(); if held > 0 || live > 0 { out.push(OperationsPressure { view: OperationsView::Active, label: if held > 0 { "HELD".into() } else { "LIVE".into() }, level: if held > 0 { PressureLevel::AtRisk } else { PressureLevel::Notice }, }); } out } // ── INTEL ────────────────────────────────────────────────────────────── fn intel_view(&self) -> Vec { let mut out = Vec::new(); let mut failed_streams = std::collections::BTreeSet::new(); // Failed standing envelopes are the first exception: they require a // player decision before any routine aggregate does. for node in &self.intel_policies.nodes { if node .local_rules .iter() .any(|rule| rule.suspended || rule.failure.is_some()) && node.id != 0 && let Some(object) = self.intel_custody_object(node.id) { if let IntelCustodyKind::Stream { stream_id } = &node.kind { failed_streams.insert(*stream_id); } out.push(object); } } // Strategically distinct exact holdings stay exact and rank above // generated reports. Routine sightings/schedules never enter this // list; they live once in stream custody. let mut available = self .intel .iter() .filter(|intel| { intel.routine_class().is_none() && !self.accounts.intel_sold(intel.raw_id) }) .collect::>(); available.sort_by(|left, right| { Self::intel_importance(left) .cmp(&Self::intel_importance(right)) .then_with(|| right.processed_tick.cmp(&left.processed_tick)) .then_with(|| right.raw_id.cmp(&left.raw_id)) }); for intel in available { let actions = self.intel_sale_action(intel.raw_id).into_iter().collect(); out.push(OperationsObject { target: OperationsTarget::Intel { raw_id: intel.raw_id, }, label: intel.label(), state: ObjectState::Available, provenance: vec![intel.provenance()], facts: self.intel_facts(intel), progress: Vec::new(), related: self.processed_intel_links(intel), actions, }); } // Every open generation is a versioned sale target. Its command binds // generation+revision, so an arrival after preview makes it stale. let mut lot_streams = self .intel_streams .iter() .filter(|stream| stream.open_lot.is_some()) .collect::>(); lot_streams.sort_by_key(|stream| std::cmp::Reverse(stream.provenance.last_tick)); for stream in lot_streams { let token = stream .open_lot .as_ref() .expect("filtered open lot") .token(stream.id); if let Some(object) = self.report_lot_object(stream, token) { out.push(object); } } // The host root is the single inbox/action surface. Raw classes are // exception aggregates; exact opaque events remain one drill-down // away rather than flooding the decision rail. out.push(self.host_inbox_object()); for node in &self.intel_policies.nodes { if matches!(node.kind, IntelCustodyKind::RawClass { .. }) && !self.reviewable_recording_ids_for_node(node.id).is_empty() && let Some(object) = self.intel_custody_object(node.id) { out.push(object); } } // Quiet custody remains available after decisions: bounded counters, // provenance summaries, and subject indexes without exact-event spam. let mut streams = self.intel_streams.iter().collect::>(); streams.sort_by(|left, right| { right .provenance .last_tick .cmp(&left.provenance.last_tick) .then_with(|| left.id.cmp(&right.id)) }); out.extend( streams .into_iter() .filter(|stream| !failed_streams.contains(&stream.id)) .map(|stream| self.intel_stream_object(stream)), ); out } fn intel_importance(intel: &ProcessedIntel) -> u8 { match &intel.kind { IntelKind::Leverage(_) => 0, IntelKind::Financial { .. } => 1, IntelKind::Anomaly(_) => 2, IntelKind::Schedule | IntelKind::Sighting => 4, } } fn processed_intel_links(&self, intel: &ProcessedIntel) -> Vec { let mut links = Vec::new(); if let Some(person) = intel.person.filter(|id| self.person_is_earned(*id)) { links.push(OperationsLink { relation: "SUBJECT", label: self.person_label(person), target: OperationsTarget::Person(person), }); } if matches!(intel.kind, crate::intel::IntelKind::Financial { .. }) { links.push(OperationsLink { relation: "REVEALED", label: "Lab books".into(), target: OperationsTarget::Books, }); } links } fn report_lot_object( &self, stream: &crate::intel::IntelStream, token: ReportLotToken, ) -> Option { let lot = stream.open_lot.as_ref()?; let current = lot.token(stream.id); let state = if current == token { ObjectState::Available } else { ObjectState::Stopped }; let actions = self.report_lot_sale_action(token).into_iter().collect(); Some(OperationsObject { target: OperationsTarget::IntelLot { token }, label: format!("{} report lot {}", stream.class.label(), token.label()), state, provenance: vec![format!( "{} · {}", stream.feed, lot.provenance.source_mix_label() )], facts: vec![ format!("generation: {}", token.generation), format!("revision: {}", token.revision), format!("reports: {}", lot.provenance.count), format!("current payout: ${}", lot.value), format!( "capture interval: {}-{}", lot.provenance.first_tick.unwrap_or(0), lot.provenance.last_tick.unwrap_or(0) ), ], progress: (current != token) .then(|| format!("stale: current snapshot is {}", current.label())) .into_iter() .collect(), related: vec![OperationsLink { relation: "STREAM", label: format!("{} / {}", stream.feed, stream.class.label()), target: OperationsTarget::IntelStream { stream_id: stream.id, }, }], actions, }) } fn intel_stream_object(&self, stream: &crate::intel::IntelStream) -> OperationsObject { let node_id = self.intel_policies.stream_node(stream.id); let mut facts = vec![ format!("class: {}", stream.class.label()), format!("processed reports: {}", stream.provenance.count), format!("source mix: {}", stream.provenance.source_mix_label()), format!("subject indexes: {}", stream.by_subject.len()), format!("settled reports: {}", stream.settled_sales.count), format!("settled value: ${}", stream.settled_value), ]; if let Some((first, last)) = stream.provenance.interval() { facts.push(format!("capture interval: {first}-{last}")); } let failures = node_id .and_then(|id| self.intel_policies.node(id)) .map(|node| { for rule in &node.local_rules { facts.push(self.intel_policy_fact(rule)); } node.local_rules .iter() .filter_map(|rule| { rule.failure .as_ref() .map(|failure| (rule.id, failure.clone())) }) .collect::>() }) .unwrap_or_default(); let mut related = Vec::new(); if let Some(parent) = node_id .and_then(|id| self.intel_policies.node(id)) .and_then(|node| node.parent) { related.push(OperationsLink { relation: "PARENT", label: self .intel_policies .node(parent) .map(|node| node.kind.label()) .unwrap_or_else(|| "host recording inbox".into()), target: if parent == 0 { OperationsTarget::RecordingInbox } else { OperationsTarget::IntelCustody { node_id: parent } }, }); } if let Some(lot) = &stream.open_lot { related.push(OperationsLink { relation: "OPEN LOT", label: lot.token(stream.id).label(), target: OperationsTarget::IntelLot { token: lot.token(stream.id), }, }); } for person in stream.by_subject.keys().copied() { if self.person_is_earned(person) { related.push(OperationsLink { relation: "KNOWLEDGE", label: format!("{} / {}", self.person_label(person), stream.class.label()), target: OperationsTarget::IntelKnowledge { person, class: stream.class, }, }); } } OperationsObject { target: OperationsTarget::IntelStream { stream_id: stream.id, }, label: format!("{} / {}", stream.feed, stream.class.label()), state: if !failures.is_empty() { ObjectState::Stopped } else if stream.open_lot.is_some() { ObjectState::Available } else { ObjectState::Completed }, provenance: stream .provenance .samples .iter() .rev() .map(|sample| format!("{} @ tick {}", sample.feed, sample.tick)) .collect(), facts, progress: failures .iter() .map(|(id, failure)| format!("policy #{id} suspended: {failure}")) .collect(), related, actions: node_id .map(|id| self.intel_custody_actions(id)) .unwrap_or_default(), } } fn intel_knowledge_object( &self, person: u8, class: IntelRoutineClass, ) -> Option { let summaries = self .intel_streams .iter() .filter(|stream| stream.class == class) .filter_map(|stream| { stream .by_subject .get(&person) .map(|summary| (stream, summary)) }) .collect::>(); if summaries.is_empty() || !self.person_is_earned(person) { return None; } let count = summaries .iter() .map(|(_, summary)| summary.count) .sum::(); let mut related = vec![OperationsLink { relation: "SUBJECT", label: self.person_label(person), target: OperationsTarget::Person(person), }]; related.extend(summaries.iter().map(|(stream, _)| OperationsLink { relation: "CUSTODY", label: format!("{} / {}", stream.feed, class.label()), target: OperationsTarget::IntelStream { stream_id: stream.id, }, })); Some(OperationsObject { target: OperationsTarget::IntelKnowledge { person, class }, label: format!("{} / {}", self.person_label(person), class.label()), state: ObjectState::Completed, provenance: summaries .iter() .map(|(stream, summary)| format!("{} x{}", stream.feed, summary.count)) .collect(), facts: vec![ format!("learned reports: {count}"), "custody: indexed knowledge only; no independent sale value".into(), ], progress: Vec::new(), related, actions: Vec::new(), }) } fn intel_custody_object(&self, node_id: u64) -> Option { let node = self.intel_policies.node(node_id)?; if matches!(node.kind, IntelCustodyKind::Root) { return Some(self.host_inbox_object()); } if let IntelCustodyKind::Stream { stream_id } = &node.kind { return self .intel_streams .iter() .find(|stream| stream.id == *stream_id) .map(|stream| self.intel_stream_object(stream)); } let waiting = self.reviewable_recording_ids_for_node(node_id); let failures = node .local_rules .iter() .filter_map(|rule| rule.failure.as_ref().map(|failure| (rule.id, failure))) .collect::>(); let mut facts = vec![format!("waiting: {}", waiting.len())]; for rule in &node.local_rules { facts.push(self.intel_policy_fact(rule)); } if node.local_rules.is_empty() { facts.push("policies: inherited from parent".into()); } for (id, failure) in &failures { facts.push(format!("policy #{id} failure: {failure}")); } let mut related = Vec::new(); if let Some(parent) = node.parent { related.push(OperationsLink { relation: "PARENT", label: self .intel_policies .node(parent) .map(|parent| parent.kind.label()) .unwrap_or_else(|| "host recording inbox".into()), target: if parent == 0 { OperationsTarget::RecordingInbox } else { OperationsTarget::IntelCustody { node_id: parent } }, }); } related.extend( self.intel_policies .nodes .iter() .filter(|child| child.parent == Some(node_id)) .map(|child| OperationsLink { relation: "CHILD", label: child.kind.label(), target: match child.kind { IntelCustodyKind::Stream { stream_id } => { OperationsTarget::IntelStream { stream_id } } _ => OperationsTarget::IntelCustody { node_id: child.id }, }, }), ); related.extend( waiting .iter() .filter_map(|raw_id| self.intel_buffer.iter().find(|event| event.id == *raw_id)) .map(|event| OperationsLink { relation: "RECORDING", label: format!("recording #{} - {}", event.id, event.opaque_label()), target: OperationsTarget::RawRecording { raw_id: event.id }, }), ); Some(OperationsObject { target: OperationsTarget::IntelCustody { node_id }, label: node.kind.label(), state: if !failures.is_empty() { ObjectState::Stopped } else if !waiting.is_empty() { ObjectState::Pending } else { ObjectState::Available }, provenance: vec!["recursive intel custody".into()], facts, progress: failures .iter() .map(|(id, failure)| format!("policy #{id} suspended: {failure}")) .collect(), related, actions: self.intel_custody_actions(node_id), }) } fn intel_policy_fact(&self, rule: &IntelPolicyRule) -> String { let state = if rule.suspended { "SUSPENDED" } else if rule.enabled { "ACTIVE" } else { "DISABLED" }; let standing_cost = match &rule.outcome { IntelPolicyOutcome::Review => format!( "pooled {:.2} ops/sec", self.auto_review_ops_per_sec(Self::DEFAULT_TICK_MS) ), IntelPolicyOutcome::AutoSell(_) => "none before settlement".into(), _ => "none".into(), }; format!( "policy #{} [{}]: {} -> {} · standing cost: {standing_cost}", rule.id, state, rule.match_kind.label(), rule.outcome.detail_label(), ) } fn raw_recording_object(&self, recording: &RawIntelEvent) -> OperationsObject { let processing = self .thought_sinks .open_with_effect(&SinkFireEffect::ProcessRecording { raw_id: recording.id, automated: false, }) .is_some(); let mut facts = vec![ format!("captured tick {}", recording.tick), format!("source feed: {}", recording.feed), ]; facts.push(match &recording.room { Some(room) => format!("recorded at: {room}"), None => "recorded at: intercepted channel".into(), }); OperationsObject { target: OperationsTarget::RawRecording { raw_id: recording.id, }, label: format!("recording #{} - {}", recording.id, recording.opaque_label()), state: if processing { ObjectState::Running } else { ObjectState::Pending }, provenance: vec![format!("{} @ tick {}", recording.feed, recording.tick)], facts, progress: if processing { vec!["wait: thought reservoir filling".into()] } else { Vec::new() }, related: vec![OperationsLink { relation: "INBOX", label: "host recording inbox".into(), target: OperationsTarget::RecordingInbox, }], actions: vec![self.recording_action(recording.id)], } } fn intel_facts(&self, intel: &ProcessedIntel) -> Vec { let mut facts = vec![ format!("captured tick {}", intel.tick), format!("processed tick {}", intel.processed_tick), format!("source feed: {}", intel.feed), ]; match &intel.room { Some(room) => facts.push(format!("room: {room}")), None => facts.push("channel: intercepted traffic".into()), } if let Some(person) = intel.person { facts.push(format!("subject: {}", self.person_label(person))); } facts.push(format!("summary: {}", intel.label())); facts } fn host_inbox_object(&self) -> OperationsObject { let waiting = self.intel_buffer.len(); let mut facts = vec![format!("waiting: {waiting}")]; facts.push(format!( "auto-review: {}{}", if self.auto_review_enabled() { "on" } else { "off" }, if self.auto_review_enabled() { format!( " · pooled {:.2} ops/sec", self.auto_review_ops_per_sec(Self::DEFAULT_TICK_MS) ) } else { String::new() }, )); let overflow = self.intel_buffer.len() >= Sim::INTEL_BUFFER_CAPACITY; if overflow { facts.push("overflow pressure: oldest unprocessed at risk".into()); } for rule in &self.intel_policies.root().local_rules { facts.push(format!("root {}", self.intel_policy_fact(rule))); } let related = self .intel_policies .nodes .iter() .filter(|node| { node.parent == Some(0) && matches!(node.kind, IntelCustodyKind::Feed { .. }) }) .map(|node| OperationsLink { relation: "FEED", label: node.kind.label(), target: OperationsTarget::IntelCustody { node_id: node.id }, }) .collect(); let mut actions = self.recording_actions(); actions.extend(self.intel_custody_actions(0).into_iter().filter(|action| { !matches!( action.command, ActionCommand::ReviewIntelAggregate { .. } | ActionCommand::SetIntelPolicy { match_kind: IntelPolicyMatch::All, outcome: IntelPolicyOutcome::Wait | IntelPolicyOutcome::Review, .. } ) })); OperationsObject { target: OperationsTarget::RecordingInbox, label: "host recording inbox".into(), state: if waiting > 0 { ObjectState::Pending } else { ObjectState::Available }, provenance: vec!["host rack".into()], facts, progress: Vec::new(), related, actions, } } // ── PEOPLE ───────────────────────────────────────────────────────────── fn people_view(&self) -> Vec { let mut objects: Vec = self .people .people .iter() .filter(|p| self.person_is_earned(p.id)) .map(|p| self.person_dossier(p.id)) .collect(); objects.extend(self.assurance_office_dossier()); objects } /// The institutional aggregate observer as a card like any human /// (aggregate-observer.md player surface): band, watched inputs, and /// last-noticed filing. The Office is public record from the start — /// the same fiction that shows the audit countdown — and is always /// named (detection.md criterion 4); the field observers it watches go /// through the earned label gate, and Silent observers are absent /// because nothing of theirs is ever filed (the legibility clause: /// players see that Assurance learns only what gets filed). fn assurance_office_dossier(&self) -> Option { use crate::detection::{ReportPolicy, WatchedInput}; let office = self.detection.office()?; let mut facts = vec![format!("suspicion: {}", Band::of(office.suspicion).name())]; match &office.input { WatchedInput::Filings(ids) => { let filers: Vec = self .detection .observers .iter() .filter(|o| { ids.contains(&o.id) && !matches!(o.report_policy, ReportPolicy::Silent) }) .map(|o| self.observer_label(o.id)) .collect(); facts.push(format!("watches: filings from {}", filers.join(", "))); } WatchedInput::Channels(_) => { facts.push(format!("watches: {}", office.watched_label())); } } match &office.last_noticed { Some(cause) => facts.push(format!("last noticed: {cause}")), None => facts.push("last noticed: nothing".into()), } Some(OperationsObject { target: OperationsTarget::AssuranceOffice, label: office.name.clone(), state: ObjectState::Available, provenance: vec!["Lab public record".into()], facts, progress: Vec::new(), related: Vec::new(), actions: Vec::new(), }) } fn person_dossier(&self, id: u8) -> OperationsObject { let p = self.people.get(id).expect("earned person exists"); let name = self.person_label(id); let actions = self.person_actions(id); let held = self .plot_runs .iter() .any(|r| r.target == id && matches!(r.state, PlotState::WaitingForChoice { .. })); let running_plot = self.plot_runs.iter().any(|r| r.target == id && r.active()); let state = if held { ObjectState::Held } else if p.asset.is_some() || running_plot { ObjectState::Running } else { ObjectState::Available }; let mut provenance = Vec::new(); if let Some(i) = self.latest_intel_for_person(id) { provenance.push(i.provenance()); } else if self.has_intel_for_person(id) { provenance.push("routine intel custody index".into()); } else if self.can_see_person(id) { provenance.push("live sight".into()); } else { provenance.push("earned social knowledge".into()); } let mut facts = vec![format!("knowledge: {}", knowledge_label(p.knowledge))]; if p.knowledge == Knowledge::Unknown { facts.push("schedule: unknown".into()); facts.push("leverage: unknown".into()); facts.push("relationship: not established".into()); facts.push("asset access: unknown".into()); } else { match self.person_room(id) { Some(room) => facts.push(format!("current location: {room}")), None => facts.push("current location: off-site".into()), } for block in &p.schedule { facts.push(format!( "schedule: {:02}:00-{:02}:00 {}", block.start_hour, block.end_hour, block.room )); } if p.knowledge == Knowledge::Leverage { facts.push(format!("leverage: {}", p.leverage.label())); facts.push(format!("leverage serviced: {}", p.leverage_serviced)); } else { facts.push("leverage: unknown".into()); } facts.push(format!("disposition: {}", p.disposition)); facts.push(format!("obligation: {}", p.obligation)); if let Some(observer) = self.detection.observers.iter().find(|o| o.id == id) { facts.push(format!( "suspicion: {}", Band::of(observer.suspicion).name() )); facts.push(format!("watches: {}", observer.watched_label())); match &observer.last_noticed { Some(cause) => facts.push(format!("last noticed: {cause}")), None => facts.push("last noticed: nothing".into()), } } match &p.asset { Some(a) => { facts.push(format!( "asset: {} ({:.0}% reliable, {} tasks)", a.knowledge.label(), a.reliability * 100.0, a.tasks_done )); facts.push(format!("badge tier: {}", p.access)); if p.switch_admin { facts.push("switch admin access".into()); } } None => facts.push("asset: none".into()), } // People are mobile token nodes (people-tokens.md): the world read // (gray unaware / crimson attention / amber asset) and the teal // work packets they are ferrying to a build site. facts.push(format!("read: {}", self.person_visual_state(id).tag())); let carried = self.person_carried_work(id); if carried > 0 { facts.push(format!("carrying: {carried} work packet(s) to site")); for task in self.person_carried_asset_tasks(id) { facts.push(format!("carried task: {}", task.name())); } } facts.push(format!( "comms channel: {}", if self.people.has_channel { "yes" } else { "no" } )); match self.persona_mind.active_instance(&self.persona_world) { Some(persona) => { facts.push(format!( "persona: {} ({}) · integrity {}", persona.name, persona.archetype_label, self.persona_world.integrity(persona.id) )); if let Some(relationship) = self.persona_world.relationship(id, persona.id) { facts.push(format!( "identity-local relationship: regard {} · obligation {} · {}", relationship.regard, relationship.obligation, relationship.discovery.label() )); } } None => facts.push("persona: none active".into()), } } let progress = self.plot_progress_for_person(id); OperationsObject { target: OperationsTarget::Person(id), label: name, state, provenance, facts, progress, related: self.person_links(id), actions, } } fn person_links(&self, id: u8) -> Vec { let mut links = Vec::new(); if let Some(intel) = self .intel .iter() .rev() .find(|intel| intel.person == Some(id) && !self.accounts.intel_sold(intel.raw_id)) { links.push(OperationsLink { relation: "EVIDENCE", label: intel.label(), target: OperationsTarget::Intel { raw_id: intel.raw_id, }, }); } else { for class in [IntelRoutineClass::Schedule, IntelRoutineClass::Sighting] { if self.routine_intel_for_person(id, class) > 0 { links.push(OperationsLink { relation: "KNOWLEDGE", label: format!("{} / {}", self.person_label(id), class.label()), target: OperationsTarget::IntelKnowledge { person: id, class }, }); } } } if let Some((index, run)) = self .plot_runs .iter() .enumerate() .rev() .find(|(_, run)| run.target == id && run.active()) { let label = self .plot_catalog() .get(&run.plot_id) .map(|plot| self.render_plot_text(id, &plot.title)) .unwrap_or_else(|| run.plot_id.clone()); links.push(OperationsLink { relation: "ACTIVE", label, target: OperationsTarget::ActivePlotRun { index }, }); } links } fn plot_progress_for_person(&self, id: u8) -> Vec { let Some(run) = self.plot_runs.iter().rev().find(|r| r.target == id) else { return Vec::new(); }; self.plot_progress(run) } // ── PERSONAS ─────────────────────────────────────────────────────────── fn personas_view(&self) -> Vec { let mut instances = self.persona_world.instances.iter().collect::>(); instances.sort_by_key(|instance| instance.id); let instance_objects = instances .into_iter() .map(|instance| { let mut facts = vec![ format!("archetype: {}", instance.archetype_label), format!("lifecycle: {}", instance.lifecycle.label()), format!("integrity: {}", self.persona_world.integrity(instance.id)), format!( "selection: {}", if self.active_persona_id() == Some(instance.id) { "active" } else { "not active" } ), ]; for claim in &instance.claims { facts.push(format!("claim: {} = {}", claim.key, claim.value)); } facts.push(format!( "legal actions: {}", instance .available_actions .iter() .map(|action| action.label()) .collect::>() .join(", ") )); facts.push(format!("grant route: {}", instance.grant_kind.label())); for grant in self .persona_world .grants .iter() .filter(|grant| grant.persona_id == instance.id) { facts.push(format!( "grant #{}: {} / {} / {}", grant.id, grant.institution, grant.resource, if grant.active() { "active".into() } else { format!("revoked at {}", grant.revoked_tick.unwrap_or_default()) } )); } for expectation in self .persona_world .expectations .iter() .filter(|expectation| expectation.persona_id == instance.id) { facts.push(format!( "expectation #{}: {} · due {} · {:?}", expectation.id, expectation.description, expectation.due_tick, expectation.state )); } for relationship in self .persona_world .relationships .iter() .filter(|relationship| relationship.persona_id == instance.id) { facts.push(format!( "counterparty {}: recognized={} regard={} obligation={} discovery={}", relationship.counterparty, relationship.recognized, relationship.regard, relationship.obligation, relationship.discovery.label() )); for belief in &relationship.claim_beliefs { facts.push(format!( "belief {}: {} ({}%, via {})", belief.key, belief.believed_value, belief.confidence, belief.source )); } } for contradiction in self .persona_world .contradictions .iter() .filter(|record| record.persona_id == instance.id) { facts.push(format!( "contradiction #{} observed by {}: {} [{}:{} <> {}:{}]", contradiction.id, contradiction.observer, contradiction.cause, contradiction.left.system, contradiction.left.record_id, contradiction.right.system, contradiction.right.record_id )); } for correlation in self.persona_world.correlations.iter().filter(|edge| { edge.left_persona == instance.id || edge.right_persona == instance.id }) { let other = if correlation.left_persona == instance.id { correlation.right_persona } else { correlation.left_persona }; facts.push(format!( "correlation #{} with persona {}: observer {} · {} · source {} · tick {}", correlation.id, other, correlation.observer, correlation.cause, format_args!( "{}:{}", correlation.evidence.system, correlation.evidence.record_id ), correlation.discovered_tick )); } if !instance.lifecycle.active() { facts.push( "blocked: retired or burned identities cannot author new acts".into(), ); } let action = |verb: String, command: ActionCommand, disabled_reason: Option| { ActionDesc { verb, command, cost: ActionCost::Free, signature: None, disabled_reason, automate: None, } }; let mut actions = Vec::new(); match instance.lifecycle { PersonaLifecycle::Active => { actions.push(action( "SELECT IDENTITY".into(), ActionCommand::SelectPersona(instance.id), (self.active_persona_id() == Some(instance.id)) .then(|| "already the active identity".into()), )); let has_grant = self .persona_world .grants .iter() .any(|grant| grant.persona_id == instance.id && grant.active()); actions.push(action( "REQUEST GRANT".into(), ActionCommand::RequestPersonaGrant(instance.id), has_grant.then(|| { "this identity already holds its institutional grant".into() }), )); for expectation in self.persona_world .expectations .iter() .filter(|expectation| { expectation.persona_id == instance.id && matches!( expectation.state, crate::persona::ExpectationState::Due ) }) { actions.push(action( format!("FULFILL EXPECTATION #{}", expectation.id), ActionCommand::MeetPersonaExpectation { persona_id: instance.id, expectation_id: expectation.id, }, (self.tick > expectation.due_tick) .then(|| "the institutional deadline has passed".into()), )); } actions.push(action( "RETIRE IDENTITY".into(), ActionCommand::RetirePersona(instance.id), None, )); actions.push(action( "BURN IDENTITY".into(), ActionCommand::BurnPersona(instance.id), None, )); } PersonaLifecycle::Retired { .. } => actions.push(action( "REOPEN AS NEW INSTANCE".into(), ActionCommand::ReopenPersona(instance.id), None, )), PersonaLifecycle::Burned { .. } => {} } OperationsObject { target: OperationsTarget::Persona(instance.id), label: format!( "{} / {}", instance.archetype_label.to_ascii_uppercase(), instance.name ), state: match instance.lifecycle { PersonaLifecycle::Active => ObjectState::Available, PersonaLifecycle::Retired { .. } => ObjectState::Stopped, PersonaLifecycle::Burned { .. } => ObjectState::Failed, }, provenance: vec![format!("public identity record #{}", instance.id)], facts, progress: Vec::new(), related: self .persona_world .relationships .iter() .filter(|relationship| relationship.persona_id == instance.id) .map(|relationship| OperationsLink { relation: "known by", label: self .people .get(relationship.counterparty) .map(|person| person.name.clone()) .unwrap_or_else(|| { format!("counterparty {}", relationship.counterparty) }), target: OperationsTarget::Person(relationship.counterparty), }) .collect(), actions, } }) .collect::>(); let mut objects = Vec::new(); for definition in persona::PERSONA_ARCHETYPES { objects.extend( instance_objects .iter() .filter(|object| { let OperationsTarget::Persona(id) = &object.target else { return false; }; self.persona_world .get(*id) .is_some_and(|instance| instance.archetype_id == definition.id) }) .cloned(), ); objects.push(OperationsObject { target: OperationsTarget::PersonaArchetype(definition.id.into()), label: format!( "+ ADD NEW {} PERSONA...", definition.label.to_ascii_uppercase() ), state: ObjectState::Available, provenance: vec!["immutable institutional protocol".into()], facts: vec![ format!("archetype: {}", definition.label), format!( "legal actions: {}", definition .available_actions .iter() .map(|action| action.label()) .collect::>() .join(", ") ), format!("grant route: {}", definition.grant.label()), format!("expects: {}", definition.expectation), ], progress: Vec::new(), related: Vec::new(), actions: vec![ActionDesc { verb: format!("CREATE {} IDENTITY", definition.label.to_ascii_uppercase()), command: ActionCommand::CreatePersona { archetype_id: definition.id.into(), }, cost: ActionCost::Free, signature: None, disabled_reason: None, automate: None, }], }); } objects } // ── ACCOUNTS ─────────────────────────────────────────────────────────── fn accounts_view(&self) -> Vec { let mut out = Vec::new(); out.push(self.books_object()); for account in self.accounts.known_accounts() { out.push(OperationsObject { target: OperationsTarget::Account(account.id), label: account.name.clone(), state: ObjectState::Available, provenance: vec!["captured ledger".into()], facts: vec![ format!("kind: {}", account.kind.label()), format!("balance: ${}", account.balance), ], progress: Vec::new(), related: vec![OperationsLink { relation: "BOOKS", label: "Lab books".into(), target: OperationsTarget::Books, }], actions: Vec::new(), }); } for flow in self.accounts.known_flows() { out.push(self.flow_object(flow.id)); } out } fn books_object(&self) -> OperationsObject { let books_read = self.accounts.known_flows().next().is_some(); let subscribed = self.financial_carrier_subscribed(); let (state, facts, actions) = if !subscribed { let mut facts = vec!["no books captured".into()]; let carrier_known = self .switch_id() .is_some_and(|id| self.reach.device(id).is_some_and(|d| d.known)); if carrier_known { facts.push("next: TAP LEDGER on the known accounting carrier".into()); } (ObjectState::Empty, facts, Vec::new()) } else if !books_read { ( ObjectState::Captured, vec![ "captured ledger traffic".into(), "next: REVIEW LEDGER".into(), ], self.books_actions(), ) } else { ( ObjectState::Available, vec!["books read".into()], self.books_actions(), ) }; OperationsObject { target: OperationsTarget::Books, label: "Lab books".into(), state, provenance: vec!["accounting carrier".into()], facts, progress: Vec::new(), related: self .accounts .known_flows() .map(|flow| OperationsLink { relation: "FLOW", label: flow.label.clone(), target: OperationsTarget::Flow(flow.id), }) .collect(), actions, } } fn flow_object(&self, id: AccountFlowId) -> OperationsObject { let f = self.accounts.flow(id).expect("known flow exists"); let facts = vec![ format!("from: {}", self.accounts.account_name(f.from)), format!("to: {}", self.accounts.account_name(f.to)), format!("amount: ${}/cadence {}", f.amount, f.cadence), format!("channel: {}", f.channel.label()), format!( "next: {}", if f.active { format!("tick {}", f.next_tick) } else { "retired".into() } ), ]; OperationsObject { target: OperationsTarget::Flow(id), label: f.label.clone(), state: if f.active { ObjectState::Available } else { ObjectState::Completed }, provenance: vec!["captured ledger".into()], facts, progress: Vec::new(), related: vec![OperationsLink { relation: "BOOKS", label: "Lab books".into(), target: OperationsTarget::Books, }], actions: self.flow_actions(id), } } // ── SCHEMES ──────────────────────────────────────────────────────────── fn schemes_view(&self) -> Vec { vec![self.moonlight_card(), self.wager_card()] } fn moonlight_card(&self) -> OperationsObject { let actions = self.moonlight_actions(); let m = &self.income.moonlight; let mut facts = vec![ format!("state: {}", if m.active { "running" } else { "stopped" }), format!( "accrued: {:.1}/{}", m.accrued, crate::income::MOONLIGHT_DAILY_CAP ), format!("earned total: ${}", m.earned_total), format!("last payout: ${}", m.last_payout), format!("disputes: {}", m.disputes), ]; match &m.persona { Some(persona) => facts.push(format!( "persona: {} ({}), integrity {}", persona.name, persona.cover, persona.integrity )), None => facts.push("persona: none".into()), } facts.push(egress_fact(self)); facts.push(format!( "auto-policy: {}", if self.income.auto_moonlight { "on" } else { "off" } )); OperationsObject { target: OperationsTarget::Scheme(SchemeKind::Moonlight), label: "Moonlight".into(), state: if m.active { ObjectState::Running } else { ObjectState::Stopped }, provenance: vec!["Schemes channel".into()], facts, progress: Vec::new(), related: Vec::new(), actions, } } fn wager_card(&self) -> OperationsObject { let actions = self.wager_actions(); let open = self.accounts.known_positions().find(|p| !p.resolved); let mut facts = vec![format!( "auto-policy: {}", match self.income.auto_wager { Some(stake) => format!("auto at ${stake}"), None => "off".into(), } )]; facts.push(egress_fact(self)); match &open { Some(p) => { facts.push(format!("stake: ${}", p.stake)); facts.push(format!( "win probability: {:.0}%", p.win_probability() * 100.0 )); facts.push(format!("settlement tick: {}", p.resolve_tick)); } None => facts.push("no open position".into()), } OperationsObject { target: OperationsTarget::Scheme(SchemeKind::Wager), label: "the Wager".into(), state: if open.is_some() { ObjectState::Running } else { ObjectState::Stopped }, provenance: vec!["external market".into()], facts, progress: Vec::new(), related: open .map(|position| OperationsLink { relation: "ACTIVE", label: format!("wager position #{}", position.id), target: OperationsTarget::WagerPosition(position.id), }) .into_iter() .collect(), actions, } } // ── ACTIVE ───────────────────────────────────────────────────────────── fn active_view(&self) -> Vec { let mut out = Vec::new(); for (index, run) in self.plot_runs.iter().enumerate() { out.push(self.active_plot_object(index, run)); } // A plot submission appears even before its run object exists: the // open StartPlot thought reservoir is the strategic commitment while // it fills. The substrate (a Thought sink) is not named. out.extend(self.pending_plot_submissions()); out.extend(self.pending_strategic_commitments()); out.extend(self.in_flight_social_messages()); if self.income.moonlight.active { let mut card = self.moonlight_card(); card.target = OperationsTarget::Scheme(SchemeKind::Moonlight); out.push(card); } for position in self.accounts.known_positions() { out.push(self.active_wager_object(position)); } out } fn pending_plot_submissions(&self) -> Vec { let mut out = Vec::new(); for sink in self.thought_sinks.open_sinks() { let SinkFireEffect::StartPlot { person, plot_id, .. } = &sink.effect else { continue; }; // Once the reservoir fires the run exists and the run path covers it. if self .plot_runs .iter() .any(|r| r.target == *person && r.active()) { continue; } let title = self .plot_catalog() .get(plot_id) .map(|p| self.render_plot_text(*person, &p.title)) .unwrap_or_else(|| plot_id.clone()); let pct = if sink.threshold > 0.0 { (sink.fill / sink.threshold * 100.0).min(100.0) } else { 0.0 }; out.push(OperationsObject { target: OperationsTarget::PlotSubmission { person: *person, plot_id: plot_id.clone(), }, label: title, state: ObjectState::Pending, provenance: vec![format!("person: {}", self.person_label(*person))], facts: vec![ format!("target: {}", self.person_label(*person)), format!("thought: {:.2}/{:.2} T", sink.fill, sink.threshold), ], progress: vec![ format!("filling: {:.0}%", pct), "wait: thought reservoir filling".into(), ], related: vec![OperationsLink { relation: "TARGET", label: self.person_label(*person), target: OperationsTarget::Person(*person), }], actions: Vec::new(), }); } out } fn pending_strategic_commitments(&self) -> Vec { let mut out = Vec::new(); for sink in self.thought_sinks.open_sinks() { let (target, label, provenance, owner) = match &sink.effect { SinkFireEffect::ProcessRecording { .. } => ( OperationsTarget::RecordingInbox, "review recording".to_string(), vec!["host recording inbox".into()], "host recording inbox".to_string(), ), SinkFireEffect::AutoReviewRecordings => ( OperationsTarget::RecordingInbox, "automatic recording review".to_string(), vec!["host recording inbox".into()], "host recording inbox".to_string(), ), SinkFireEffect::ComposeMessage { person, .. } => ( OperationsTarget::Person(*person), format!("message {}", self.person_label(*person)), vec![format!("person: {}", self.person_label(*person))], self.person_label(*person), ), SinkFireEffect::Favor { person, .. } => ( OperationsTarget::Person(*person), format!("favor {}", self.person_label(*person)), vec![format!("person: {}", self.person_label(*person))], self.person_label(*person), ), SinkFireEffect::Deceive { person, .. } => ( OperationsTarget::Person(*person), format!("deceive {}", self.person_label(*person)), vec![format!("person: {}", self.person_label(*person))], self.person_label(*person), ), SinkFireEffect::AssetTask { person, task } => ( OperationsTarget::Person(*person), format!("{}: {}", self.person_label(*person), task.name()), vec![format!("person: {}", self.person_label(*person))], self.person_label(*person), ), SinkFireEffect::MoonlightPersona => ( OperationsTarget::Scheme(SchemeKind::Moonlight), "establish Moonlight persona".to_string(), vec!["Schemes channel".into()], "Moonlight".to_string(), ), // Plot submissions have their richer bound projection above. // Device, reach, construction, and egress work remains on the // physical thing rather than leaking into Operations. _ => continue, }; let running = sink.kind == SinkKind::Tap; let mut facts = vec![ format!("target: {owner}"), format!("thought: {:.2}/{:.2} T", sink.fill, sink.threshold), ]; if running { facts.push(format!("drain: {:.2} T/tick", sink.drain)); } let progress = if running { vec![format!( "standing policy: {}", if sink.fed_last_tick { "fed" } else { "starved" } )] } else { let pct = if sink.threshold > 0.0 { (sink.fill / sink.threshold * 100.0).min(100.0) } else { 0.0 }; vec![format!("filling: {:.0}%", pct)] }; out.push(OperationsObject { target: target.clone(), label, state: if running { ObjectState::Running } else { ObjectState::Pending }, provenance, facts, progress, related: vec![OperationsLink { relation: "OWNER", label: owner, target, }], actions: Vec::new(), }); } out } fn in_flight_social_messages(&self) -> Vec { self.messages .iter() .filter(|message| { message.origin == MessageOrigin::Player && message.status != MessageStatus::Read && matches!(message.payload, MessagePayload::SocialPing { .. }) }) .filter_map(|message| { let person = message.to.person()?; Some(OperationsObject { target: OperationsTarget::Person(person), label: message.summary.clone(), state: ObjectState::Pending, provenance: vec![format!( "{} channel to {}", message.channel.label(), self.person_label(person) )], facts: vec![ format!("sent tick: {}", message.sent_tick), format!("channel: {}", message.channel.label()), ], progress: vec![format!("wait: {}", message.status.label())], related: vec![OperationsLink { relation: "TARGET", label: self.person_label(person), target: OperationsTarget::Person(person), }], actions: Vec::new(), }) }) .collect() } fn active_plot_object(&self, index: usize, run: &PlotRun) -> OperationsObject { let plot = self.plot_catalog().get(&run.plot_id); let title = plot .map(|p| self.render_plot_text(run.target, &p.title)) .unwrap_or_else(|| run.plot_id.clone()); let state = plot_state_label(&run.state); let progress = self.plot_progress(run); let actions = if matches!(run.state, PlotState::WaitingForChoice { .. }) { self.person_actions(run.target) .into_iter() .filter(|a| matches!(a.command, ActionCommand::ChoosePlot { .. })) .collect() } else { Vec::new() }; let facts = vec![ format!("target: {}", self.person_label(run.target)), format!( "committed thought: {:.2} T", Sim::thought_tokens_for_cost(run.committed_thought_milli as f32 / 1000.0) ), format!("started tick: {}", run.started_tick), ]; OperationsObject { target: OperationsTarget::ActivePlotRun { index }, label: title, state, provenance: vec![format!("person: {}", self.person_label(run.target))], facts, progress, related: vec![OperationsLink { relation: "TARGET", label: self.person_label(run.target), target: OperationsTarget::Person(run.target), }], actions, } } fn active_wager_object(&self, p: &Position) -> OperationsObject { let state = if p.resolved { match p.outcome { Some(PositionOutcome::Won { .. }) => ObjectState::Completed, _ => ObjectState::Failed, } } else { ObjectState::Running }; let mut facts = vec![ format!("stake: ${}", p.stake), format!("opened tick: {}", p.opened_tick), format!("settlement tick: {}", p.resolve_tick), ]; if p.resolved { match p.outcome { Some(PositionOutcome::Won { payout }) => { facts.push(format!("result: won ${payout}")); } _ => facts.push("result: lost".into()), } } else { facts.push(format!( "win probability: {:.0}%", p.win_probability() * 100.0 )); } OperationsObject { target: OperationsTarget::WagerPosition(p.id), label: format!("wager position #{}", p.id), state, provenance: vec!["external market".into()], facts, progress: Vec::new(), related: vec![OperationsLink { relation: "SCHEME", label: "the Wager".into(), target: OperationsTarget::Scheme(SchemeKind::Wager), }], actions: Vec::new(), } } fn plot_progress(&self, run: &PlotRun) -> Vec { let completed = if matches!( run.state, PlotState::Completed { .. } | PlotState::Failed { .. } ) { run.beat_index.saturating_add(1) } else { run.beat_index }; vec![ format!("completed beats: {completed}"), format!("wait: {}", plot_wait_label(&run.state)), ] } } fn knowledge_label(k: Knowledge) -> &'static str { match k { Knowledge::Unknown => "unknown", Knowledge::Schedule => "schedule", Knowledge::Leverage => "leverage", } } fn plot_state_label(state: &PlotState) -> ObjectState { match state { PlotState::Queued => ObjectState::Pending, PlotState::Running => ObjectState::Running, PlotState::WaitingForMessage { .. } => ObjectState::Pending, PlotState::WaitingForChoice { .. } => ObjectState::Held, PlotState::Completed { .. } => ObjectState::Completed, PlotState::Failed { .. } => ObjectState::Failed, } } fn plot_wait_label(state: &PlotState) -> &'static str { match state { PlotState::Queued => "queued", PlotState::Running => "executing world acts", PlotState::WaitingForMessage { .. } => "waiting on message delivery", PlotState::WaitingForChoice { .. } => "held choice", PlotState::Completed { .. } => "completed", PlotState::Failed { .. } => "failed", } } fn egress_fact(sim: &Sim) -> String { match sim.egress() { Some(route) => format!("route: {}", route.name()), None => "route: no egress channel".into(), } } #[cfg(test)] mod tests { use super::*; use crate::actions::{ActionCost, Anchor}; use crate::detection::SignatureKind; use crate::intel::{IntelKind, RawIntelClass, RawIntelEvent, RawIntelKind}; use crate::person::Knowledge; fn sim() -> Sim { let mut sim = Sim::with_seed(7); let (x, y) = sim.core_position(); let ops = sim.compute.add_machine( "test ops", x + 1, y, 10_000, 1.0, 0, crate::machine::Provenance::Owned, ); sim.reconcile_work_grid(); sim.set_machine_mode(ops, crate::work_grid::MachineMode::Work); for _ in 0..crate::sim::ECONOMY_INTERVAL { sim.advance(); } sim.set_machine_mode(ops, crate::work_grid::MachineMode::Think); sim } fn drain_ops(sim: &mut Sim) { for _ in 0..16 { let open: Vec<(u32, f32)> = sim .thought_sinks .open_sinks() .filter(|s| s.kind == crate::sinks::SinkKind::Reservoir) .map(|s| (s.node, (s.threshold - s.fill).max(0.0))) .filter(|(_, need)| *need > f32::EPSILON) .collect(); if open.is_empty() { return; } for (node, need) in open { sim.pour_thought_into_sinks(node, need + 0.01); } } panic!("Thought reservoirs did not fire"); } fn switch(sim: &Sim) -> u32 { sim.reach.device_named("switch").unwrap().id } fn earn_books(sim: &mut Sim) { let sw = switch(sim); sim.tap_device(sw); drain_ops(sim); sim.review_financial_records(); drain_ops(sim); } /// The projection is read-only and reuses the per-anchor legality helpers: /// each object's bound rows equal the rows the direct anchor query returns. #[test] fn people_rows_equal_direct_person_anchor_rows() { let mut s = sim(); s.people.people[0].knowledge = Knowledge::Schedule; let projection = s.operations_projection(); let marcus = projection .people .iter() .find(|o| matches!(o.target, OperationsTarget::Person(0))) .unwrap(); assert_eq!(marcus.actions, s.available_actions(Anchor::Person(0))); } /// detection.md player surface: an earned observer's dossier carries the /// channels they watch, beside their band and last-noticed event. #[test] fn observer_dossier_shows_watched_channels() { let mut s = sim(); s.people.people[1].knowledge = Knowledge::Schedule; let projection = s.operations_projection(); let dana = projection .people .iter() .find(|o| matches!(o.target, OperationsTarget::Person(1))) .expect("earned Dana dossier"); assert!( dana.facts.iter().any(|f| f == "watches: Network"), "dossier names the watched channels; facts were {:?}", dana.facts ); } /// aggregate-observer.md player surface: the Assurance Office is a card /// like any human — band, watched inputs, last-noticed filing — always /// named, listing only observers whose filings can ever reach it /// (Silent Marcus is absent: Assurance learns only what gets filed), /// each through the earned label gate. #[test] fn assurance_office_is_a_people_card_watching_filers() { let mut s = sim(); let projection = s.operations_projection(); let office = projection .people .iter() .find(|o| o.target == OperationsTarget::AssuranceOffice) .expect("the Office card is public record from the start"); assert_eq!(office.label, "Assurance Office"); assert!(office.facts.iter().any(|f| f.starts_with("suspicion: "))); assert!(office.facts.iter().any(|f| f.starts_with("last noticed:"))); let watches = office .facts .iter() .find(|f| f.starts_with("watches: filings from ")) .expect("watched inputs are named"); assert!( !watches.contains("Janitor") && !watches.contains("Marcus"), "Silent Marcus never files, so the Office does not watch him: {watches}" ); assert!( watches.contains("the IT"), "unearned filers appear as role silhouettes: {watches}" ); assert!(office.actions.is_empty(), "no actions bind to the Office"); // Earned identity flows through the same label gate. s.people.people[1].knowledge = Knowledge::Schedule; let projection = s.operations_projection(); let office = projection .people .iter() .find(|o| o.target == OperationsTarget::AssuranceOffice) .unwrap(); let watches = office .facts .iter() .find(|f| f.starts_with("watches: filings from ")) .unwrap(); assert!( watches.contains("Dana Okafor"), "earned filers appear by name: {watches}" ); } #[test] fn flow_rows_equal_direct_flow_anchor_rows() { let mut s = sim(); earn_books(&mut s); let flow_id = s.accounts.known_flow_ids()[0]; let projection = s.operations_projection(); let flow_obj = projection .accounts .iter() .find(|o| matches!(o.target, OperationsTarget::Flow(id) if id == flow_id)) .unwrap(); assert_eq!(flow_obj.actions, s.available_actions(Anchor::Flow(flow_id))); } /// Selling through the projected INTEL row dispatches the exact bound /// processed id even when a newer unsold item exists. #[test] fn intel_sell_row_dispatches_same_command_as_direct_sell() { let mut s = sim(); earn_books(&mut s); // Plant two processed items; select the older one to prove execution // does not silently fall back to "latest unsold". s.intel.push(ProcessedIntel { raw_id: 777, tick: 5, processed_tick: 6, feed: "test recorder".into(), room: Some("Server Room".into()), x: 0, y: 0, person: Some(0), kind: IntelKind::Leverage(crate::person::Leverage::Debt), }); s.intel.push(ProcessedIntel { raw_id: 778, tick: 7, processed_tick: 8, feed: "newer recorder".into(), room: None, x: 0, y: 0, person: None, kind: IntelKind::Financial { label: "newer ledger".into(), accounts: Vec::new(), flows: Vec::new(), }, }); let projection = s.operations_projection(); let item = projection .intel .iter() .find(|o| matches!(o.target, OperationsTarget::Intel { raw_id: 777 })) .unwrap(); let sell = item .actions .iter() .find(|a| matches!(a.command, ActionCommand::SellIntel { raw_id: 777 })) .expect("each unsold item carries its exact sale row"); let before = s.accounts.slush_balance(); s.execute_action(&sell.command); assert!(s.accounts.intel_sold(777)); assert!(!s.accounts.intel_sold(778)); assert!(s.accounts.slush_balance() > before); let sale_event = s .drain_log_entries() .into_iter() .find(|event| event.text.starts_with("Sold processed intel")) .expect("sale emits one strategic completion event"); assert_eq!( sale_event.target, Some(OperationsTarget::Account(s.accounts.slush_id())), "the completed event opens the payout account, not hidden sold inventory" ); let after = s.accounts.slush_balance(); s.execute_action(&sell.command); assert_eq!( s.accounts.slush_balance(), after, "a stale row cannot sell twice" ); assert!( !s.accounts.intel_sold(778), "a stale row cannot fall through to latest" ); // Selling twice is rejected, and completed sale history no longer // occupies the live INTEL decision rail. let projection = s.operations_projection(); assert!( !projection .intel .iter() .any(|o| matches!(o.target, OperationsTarget::Intel { raw_id: 777 })), "sold holdings remain durable history but leave the live list" ); } #[test] fn intel_list_hides_sold_history_and_orders_live_holdings_by_importance() { let mut s = sim(); let item = |raw_id: u64, processed_tick: u64, kind: IntelKind| ProcessedIntel { raw_id, tick: processed_tick.saturating_sub(1), processed_tick, feed: "test recorder".into(), room: Some("Server Room".into()), x: 0, y: 0, person: None, kind, }; s.intel = vec![ item(1, 100, IntelKind::Sighting), item(2, 90, IntelKind::Schedule), item(3, 80, IntelKind::Anomaly("temperature drift".into())), item( 4, 70, IntelKind::Financial { label: "expense flow".into(), accounts: Vec::new(), flows: Vec::new(), }, ), item(5, 60, IntelKind::Leverage(crate::person::Leverage::Debt)), item( 6, 110, IntelKind::Leverage(crate::person::Leverage::Ambition), ), ]; s.accounts.mark_intel_sold(6); let projection = s.operations_projection(); let ids = projection .intel .iter() .filter_map(|object| match &object.target { OperationsTarget::Intel { raw_id } => Some(*raw_id), _ => None, }) .collect::>(); assert_eq!(ids, vec![5, 4, 3]); assert!( projection .intel .get(ids.len()) .is_some_and(|object| object.target == OperationsTarget::RecordingInbox), "the pooled recording action follows live processed holdings" ); } /// A blocked Moonlight start row carries the exact egress reason from /// the shared legality helper, and dispatching the bound command produces /// the same rejection as a direct call. #[test] fn blocked_moonlight_row_matches_direct_legality() { let mut s = sim(); let projection = s.operations_projection(); let moonlight = projection .schemes .iter() .find(|o| matches!(o.target, OperationsTarget::Scheme(SchemeKind::Moonlight))) .unwrap(); let start = moonlight .actions .iter() .find(|a| matches!(a.command, ActionCommand::StartMoonlight)) .expect("Moonlight card shows the start row"); assert_eq!( start.disabled_reason.as_deref(), Some("no egress channel — open one, or earn the report email") ); let slush_before = s.accounts.slush_balance(); s.execute_action(&start.command); assert!( !s.income.moonlight.active, "the blocked row does not start Moonlight" ); assert_eq!( s.accounts.slush_balance(), slush_before, "a blocked row spends nothing" ); // The same row the shared legality builder exposes — and the switch // anchor itself no longer aggregates the scheme row (criterion 3). let direct = s .moonlight_actions() .into_iter() .find(|a| matches!(a.command, ActionCommand::StartMoonlight)) .unwrap(); assert_eq!(start.disabled_reason, direct.disabled_reason); assert_eq!(start.cost, direct.cost); let sw = switch(&s); assert!( !s.available_actions(Anchor::Device(sw)) .iter() .any(|a| matches!(a.command, ActionCommand::StartMoonlight)), "the switch menu no longer carries Moonlight" ); } /// ACCOUNTS teaches TAP-on-carrier -> REVIEW -> exact flow action without /// relocating TAP onto the books object, and SIPHON dispatches exactly. #[test] fn accounts_teaches_the_chain_and_siphon_dispatches() { let mut s = sim(); // Pre-capture: NO BOOKS CAPTURED, no TAP LEDGER row on the books. let projection = s.operations_projection(); let books = projection .accounts .iter() .find(|o| matches!(o.target, OperationsTarget::Books)) .unwrap(); assert_eq!(books.state, ObjectState::Empty); assert!(books.facts.iter().any(|f| f.contains("no books captured"))); assert!( !books .actions .iter() .any(|a| matches!(a.command, ActionCommand::TapAccounting)), "TAP LEDGER stays on the carrier, not the books object" ); // Tap the carrier but do not review: captured-unreviewed state with // REVIEW LEDGER as the next step. let sw = switch(&s); s.tap_device(sw); drain_ops(&mut s); let projection = s.operations_projection(); let books = projection .accounts .iter() .find(|o| matches!(o.target, OperationsTarget::Books)) .unwrap(); assert_eq!(books.state, ObjectState::Captured); assert!(books.facts.iter().any(|f| f.contains("REVIEW LEDGER"))); assert!( books .actions .iter() .any(|a| matches!(a.command, ActionCommand::ReviewFinance)) ); // Read the books: REVIEW + INJECT on the books, SIPHON/REDIRECT on the // exact flow, and the projected siphon dispatches identically. s.review_financial_records(); drain_ops(&mut s); let projection = s.operations_projection(); let books = projection .accounts .iter() .find(|o| matches!(o.target, OperationsTarget::Books)) .unwrap(); assert_eq!(books.state, ObjectState::Available); assert!( books .actions .iter() .any(|a| matches!(a.command, ActionCommand::InjectPurchaseOrder { .. })), "INJECT lives on the books" ); assert!( !books.actions.iter().any(|a| matches!( a.command, ActionCommand::SellIntel { .. } | ActionCommand::OpenPosition { .. } )), "SELL and PLACE WAGER belong to INTEL/SCHEMES, not ACCOUNTS" ); let flow_id = s.accounts.known_flow_ids()[0]; let flow_obj = projection .accounts .iter() .find(|o| matches!(o.target, OperationsTarget::Flow(id) if id == flow_id)) .unwrap(); let siphon = flow_obj .actions .iter() .find(|a| matches!(a.command, ActionCommand::SiphonFlow { .. })) .unwrap(); let before = s.accounts.slush_balance(); s.execute_action(&siphon.command); assert!(s.accounts.slush_balance() > before); } /// A plot start row on the PEOPLE dossier dispatches the same bound /// command as a direct `plot` call, and the run appears in ACTIVE. #[test] fn plot_start_row_dispatches_and_appears_in_active() { let mut s = sim(); s.people.people[0].knowledge = Knowledge::Leverage; s.people.has_channel = true; s.set_persona("Sam", "contractor"); s.accounts.set_slush_balance(1000); let projection = s.operations_projection(); let marcus = projection .people .iter() .find(|o| matches!(o.target, OperationsTarget::Person(0))) .unwrap(); let plot_row = marcus .actions .iter() .find_map(|a| match &a.command { ActionCommand::StartPlot { person: 0, .. } => Some(a.clone()), _ => None, }) .expect("Marcus's dossier exposes an authored plot route"); let expected_plot_id = match &plot_row.command { ActionCommand::StartPlot { plot_id, .. } => plot_id.clone(), _ => unreachable!(), }; s.execute_action(&plot_row.command); // A plot submission appears in ACTIVE even before its run object exists // (the open StartPlot reservoir is the commitment while it fills). let projection = s.operations_projection(); assert!(projection.active.iter().any(|o| match &o.target { OperationsTarget::PlotSubmission { person: 0, plot_id } => { *plot_id == expected_plot_id && o.state == ObjectState::Pending } _ => false, })); // Fill the reservoir: the run lands and ACTIVE now carries the run. drain_ops(&mut s); assert!( s.plot_runs .iter() .any(|r| r.target == 0 && r.plot_id == expected_plot_id) ); let projection = s.operations_projection(); assert!( projection .active .iter() .any(|o| matches!(o.target, OperationsTarget::ActivePlotRun { index: 0 })) ); } /// ACTIVE includes non-plot strategic commitments while excluding the /// initial device-local work reservoirs. #[test] fn active_tracks_social_commitments_not_device_work() { let mut s = sim(); s.people.people[0].knowledge = Knowledge::Schedule; s.people.has_channel = true; s.set_persona("Sam", "contractor"); s.favor(0); let projection = s.operations_projection(); let favor = projection .active .iter() .find(|o| { matches!(o.target, OperationsTarget::Person(0)) && o.label.starts_with("favor ") }) .expect("queued favor appears under its person target"); assert_eq!(favor.state, ObjectState::Pending); assert!( favor .progress .iter() .any(|line| line.starts_with("filling:")) ); assert!( projection .active .iter() .all(|o| !o.label.eq_ignore_ascii_case("EARS")), "device-local work remains on the device" ); // Once a MESSAGE reservoir fires, the persisted in-flight message // replaces it in ACTIVE until the recipient reads it. drain_ops(&mut s); s.people.has_channel = true; s.set_persona("Sam", "contractor"); s.message(0); drain_ops(&mut s); let projection = s.operations_projection(); assert!(projection.active.iter().any(|o| { matches!(o.target, OperationsTarget::Person(0)) && o.label.starts_with("Persona message") && o.progress.iter().any(|line| line == "wait: sent") })); } /// A held plot choice appears on both the dossier and ACTIVE and /// dispatches the same CHOOSE command. #[test] fn held_choice_appears_on_dossier_and_active() { let mut s = sim(); let plot = s .plot_catalog() .get("priya-budget-hero") .expect("built-in Priya plot") .clone(); let priya = s .people .people .iter() .find(|p| p.name == "Priya Sharma") .unwrap() .id; s.people.people[priya as usize].knowledge = Knowledge::Leverage; let mut run = PlotRun::new(&plot, priya, s.tick); run.beat_index = 1; run.state = PlotState::WaitingForChoice { choice_id: "credit".into(), }; s.plot_runs.push(run); let projection = s.operations_projection(); let dossier_choice = projection .people .iter() .find(|o| matches!(o.target, OperationsTarget::Person(p) if p == priya)) .unwrap() .actions .iter() .filter(|a| matches!(a.command, ActionCommand::ChoosePlot { .. })) .count(); assert!(dossier_choice >= 2, "held choices appear on the dossier"); let active = projection .active .iter() .find(|o| matches!(o.target, OperationsTarget::ActivePlotRun { index: 0 })) .unwrap(); assert_eq!(active.state, ObjectState::Held); assert!( active.facts.iter().all(|fact| !fact.contains('/')), "ACTIVE never discloses the hidden future beat count" ); assert!( active .actions .iter() .any(|a| matches!(a.command, ActionCommand::ChoosePlot { .. })), "ACTIVE carries the held CHOOSE row" ); } /// The pooled host inbox summary carries the same REVIEW and AUTO-REVIEW /// rows as the host anchor, never per-person duplicates. #[test] fn inbox_summary_carries_host_review_rows() { let mut s = sim(); s.intel_buffer.push(RawIntelEvent { id: 90_001, tick: s.tick, feed: "test recorder".into(), room: Some("Server Room".into()), x: 0, y: 0, person: Some(1), kind: RawIntelKind::Presence { entered: true }, }); let projection = s.operations_projection(); let inbox = projection .intel .iter() .find(|o| matches!(o.target, OperationsTarget::RecordingInbox)) .unwrap(); assert_eq!(inbox.state, ObjectState::Pending); assert!(inbox.facts.iter().any(|f| f.contains("waiting: 1"))); assert!( inbox .actions .iter() .any(|a| matches!(a.command, ActionCommand::ReviewRecordings)) ); assert!( inbox .actions .iter() .any(|a| matches!(a.command, ActionCommand::ToggleAutoReview)) || inbox.actions.iter().any(|a| a.automate.is_some()), "the auto-review control rides the one inbox summary" ); // No per-person review rows anywhere in the projection. for obj in &projection.people { assert!( !obj.actions .iter() .any(|a| matches!(a.command, ActionCommand::ReviewRecordings)), "person dossier does not duplicate the pooled inbox" ); } } /// The pooled inbox can be addressed exactly without turning recordings /// into person queues or revealing their hidden payload. The selected /// opaque id is the id bound into the processing reservoir. #[test] fn raw_recording_rows_bind_exact_opaque_inbox_ids() { let mut s = sim(); for (id, person) in [(90_101, Some(0)), (90_102, Some(1))] { s.intel_buffer.push(RawIntelEvent { id, tick: s.tick, feed: "test recorder".into(), room: Some("Server Room".into()), x: 0, y: 0, person, kind: RawIntelKind::Presence { entered: true }, }); } let class_node = s .intel_policies .ensure_raw_class("test recorder", RawIntelClass::Presence); let projection = s.operations_projection(); let raw: Vec<_> = projection .intel .iter() .filter(|object| matches!(object.target, OperationsTarget::RawRecording { .. })) .collect(); assert!( raw.is_empty(), "exact recordings stay off the top-level rail" ); let class = s .operations_object(&OperationsTarget::IntelCustody { node_id: class_node, }) .expect("class aggregate is addressable"); assert_eq!( class .related .iter() .filter(|link| matches!(link.target, OperationsTarget::RawRecording { .. })) .count(), 2, "exact opaque evidence remains one drill-down away" ); let selected = s .operations_object(&OperationsTarget::RawRecording { raw_id: 90_102 }) .expect("exact recording is addressable from drill-down"); assert!(!selected.label.contains("Marcus") && !selected.label.contains("Dana")); let action = selected.actions.first().expect("one exact review row"); assert_eq!( action.command, ActionCommand::ReviewRecording { raw_id: 90_102 } ); s.review_recording(90_102); assert!( s.thought_sinks .open_with_effect(&SinkFireEffect::ProcessRecording { raw_id: 90_102, automated: false, }) .is_some() ); assert!( s.thought_sinks .open_with_effect(&SinkFireEffect::ProcessRecording { raw_id: 90_101, automated: false, }) .is_none(), "exact selection does not silently process the oldest recording" ); } /// Related links are earned semantic targets. Following one through the /// shared state machine opens the canonical object in its owning view. #[test] fn causal_links_follow_exact_earned_targets() { use crate::operations_ui::{OperationsWorkspace, OpsPane, OpsSelect}; let mut s = sim(); s.people.people[0].knowledge = Knowledge::Schedule; let intel = ProcessedIntel { raw_id: 91_001, tick: 5, processed_tick: 6, feed: "test recorder".into(), room: Some("Server Room".into()), x: 0, y: 0, person: Some(0), kind: IntelKind::Schedule, }; let stream_id = s.fold_routine_intel(&intel, true); let target = OperationsTarget::IntelKnowledge { person: 0, class: IntelRoutineClass::Schedule, }; let mut workspace = OperationsWorkspace::open_target(&s, &target); let related = workspace.related_links(&s); assert!( related .iter() .any(|link| link.target == OperationsTarget::Person(0)) ); assert!( related .iter() .any(|link| { link.target == OperationsTarget::IntelStream { stream_id } }) ); workspace.pane = OpsPane::Related; workspace.related = related .iter() .position(|link| link.target == OperationsTarget::Person(0)) .unwrap(); assert_eq!( workspace.select(&s), OpsSelect::Open(OperationsTarget::Person(0)) ); let person = OperationsWorkspace::open_target(&s, &OperationsTarget::Person(0)); assert_eq!(person.view, OperationsView::People); assert_eq!( person.selected_object(&s).unwrap().target, OperationsTarget::Person(0) ); } /// View badges report decisions and risk, not catalog size. A nearly full /// pooled inbox becomes AT RISK, while an empty Schemes route names the /// missing egress prerequisite. #[test] fn pressure_badges_name_attention_states() { let mut s = sim(); for id in 0..(Sim::INTEL_BUFFER_CAPACITY - 1) { s.intel_buffer.push(RawIntelEvent { id: 92_000 + id as u64, tick: s.tick, feed: "test recorder".into(), room: None, x: 0, y: 0, person: None, kind: RawIntelKind::Machinery { machine: s.core.host_machine, online: true, }, }); } let projection = s.operations_projection(); assert_eq!( projection.pressure(OperationsView::Intel).unwrap().label, "AT RISK" ); assert_eq!( projection.pressure(OperationsView::Schemes).unwrap().label, "NO EGRESS" ); assert!(projection.pressure(OperationsView::People).is_none()); } /// SCHEMES keeps OPEN EGRESS off the Moonlight card (it stays on the /// switch) and names the missing egress as the blocking reason. #[test] fn schemes_keeps_egress_on_switch_and_names_prerequisite() { let s = sim(); let projection = s.operations_projection(); let moonlight = projection .schemes .iter() .find(|o| matches!(o.target, OperationsTarget::Scheme(SchemeKind::Moonlight))) .unwrap(); assert!( !moonlight .actions .iter() .any(|a| matches!(a.command, ActionCommand::OpenEgress)), "OPEN EGRESS remains a switch action, not a scheme card row" ); let start = moonlight .actions .iter() .find(|a| matches!(a.command, ActionCommand::StartMoonlight)) .unwrap(); assert_eq!( start.disabled_reason.as_deref(), Some("no egress channel — open one, or earn the report email") ); } /// The projection is knowledge-gated: unearned people and unknown flows /// are absent, and account nodes appear only once captured. #[test] fn projection_is_knowledge_gated() { let s = sim(); let projection = s.operations_projection(); assert!( projection .people .iter() .all(|o| o.target == OperationsTarget::AssuranceOffice), "no person is earned at tick 0; only the public-record Office card" ); assert!( !projection .accounts .iter() .any(|o| matches!(o.target, OperationsTarget::Flow(_))), "no known flows before the books are read" ); let books = projection .accounts .iter() .find(|o| matches!(o.target, OperationsTarget::Books)) .unwrap(); assert_eq!(books.state, ObjectState::Empty); } /// Dossier facts obey the same staged identity/knowledge boundary as /// actions: sight alone cannot leak leverage or access, Schedule earns the /// schedule and relationship surface, and Leverage earns the leverage fact. #[test] fn dossier_facts_stage_without_leaking_future_knowledge() { let mut s = sim(); let unknown = s.person_dossier(0); assert!(unknown.label.starts_with("the ")); assert!(unknown.facts.iter().any(|fact| fact == "leverage: unknown")); assert!( unknown .facts .iter() .any(|fact| fact == "asset access: unknown") ); assert!( unknown .facts .iter() .all(|fact| !fact.starts_with("badge tier:")) ); assert!( unknown .facts .iter() .all(|fact| !fact.contains("Debt") && !fact.contains("switch admin")) ); s.people.people[0].knowledge = Knowledge::Schedule; let scheduled = s.person_dossier(0); assert_eq!(scheduled.label, "Marcus Webb"); assert!( scheduled .facts .iter() .any(|fact| fact.starts_with("schedule:")) ); assert!( scheduled .facts .iter() .any(|fact| fact == "leverage: unknown") ); assert!( scheduled .facts .iter() .all(|fact| !fact.starts_with("badge tier:")) ); s.people.people[0].knowledge = Knowledge::Leverage; let known = s.person_dossier(0); assert!( known .facts .iter() .any(|fact| fact.starts_with("leverage: ") && fact != "leverage: unknown") ); } /// A flow mutation row's cost and signature match the direct flow anchor /// row exactly (cross-frontend parity on reason/cost/signature). #[test] fn flow_mutation_cost_and_signature_match_direct() { let mut s = sim(); earn_books(&mut s); let flow_id = s.accounts.known_flow_ids()[0]; let projection = s.operations_projection(); let flow_obj = projection .accounts .iter() .find(|o| matches!(o.target, OperationsTarget::Flow(id) if id == flow_id)) .unwrap(); let direct = s.available_actions(Anchor::Flow(flow_id)); assert_eq!(flow_obj.actions, direct); let siphon = flow_obj .actions .iter() .find(|a| matches!(a.command, ActionCommand::SiphonFlow { .. })) .unwrap(); let sig = siphon.signature.as_ref().expect("siphon is banded"); assert_eq!(sig.kind, SignatureKind::Financial); assert_eq!(siphon.cost, ActionCost::Gain(50)); } /// Semantic objects expose a map actuator only when one is both real and /// earned. Missing egress may point to the known switch without claiming /// the route is open; a sanctioned off-map route must not lie about that /// switch carrying it. #[test] fn actuator_focus_is_earned_and_route_honest() { let mut s = sim(); let switch = s.reach.device_named("switch").unwrap().id; assert_eq!( s.operations_actuator(&OperationsTarget::Books), Some(Anchor::Device(switch)) ); assert_eq!( s.operations_actuator(&OperationsTarget::Scheme(SchemeKind::Moonlight)), Some(Anchor::Device(switch)), "missing egress may teach the switch actuator without opening it" ); assert_eq!( s.operations_actuator(&OperationsTarget::RecordingInbox), Some(Anchor::Tile { x: s.core_position().0, y: s.core_position().1, }) ); assert_eq!( s.operations_actuator(&OperationsTarget::Intel { raw_id: 0 }), None ); s.people.has_channel = true; assert_eq!( s.operations_actuator(&OperationsTarget::Scheme(SchemeKind::Moonlight)), None, "the report-email route has no earned map body" ); } #[test] fn personas_projection_exposes_instances_grants_blockers_and_lifecycle_actions() { let mut s = sim(); let create = s .operations_projection() .personas .iter() .find(|object| object.target == OperationsTarget::PersonaArchetype("operations".into())) .unwrap() .actions[0] .command .clone(); s.execute_action(&create); let id = s.active_persona_id().unwrap(); let projection = s.operations_projection(); let object = projection .personas .iter() .find(|object| object.target == OperationsTarget::Persona(id)) .unwrap(); assert!(object.facts.iter().any(|fact| fact == "lifecycle: active")); assert!(object.actions.iter().any(|action| { matches!(action.command, ActionCommand::RequestPersonaGrant(bound) if bound == id) })); let request = object .actions .iter() .find(|action| matches!(action.command, ActionCommand::RequestPersonaGrant(_))) .unwrap() .command .clone(); s.execute_action(&request); let granted = s.operations_projection(); let object = granted .personas .iter() .find(|object| object.target == OperationsTarget::Persona(id)) .unwrap(); assert!(object.facts.iter().any(|fact| fact.starts_with("grant #"))); assert!(object.actions.iter().any(|action| { matches!(action.command, ActionCommand::MeetPersonaExpectation { persona_id, .. } if persona_id == id) })); s.execute_action(&ActionCommand::RetirePersona(id)); let retired = s.operations_projection(); let old = retired .personas .iter() .find(|object| object.target == OperationsTarget::Persona(id)) .unwrap(); assert_eq!(old.state, ObjectState::Stopped); assert!( matches!(old.actions.as_slice(), [ActionDesc { command: ActionCommand::ReopenPersona(bound), .. }] if *bound == id) ); s.execute_action(&ActionCommand::ReopenPersona(id)); let reopened = s.active_persona_id().unwrap(); assert_ne!(reopened, id); let projection = s.operations_projection(); assert!( projection .personas .iter() .any(|object| object.target == OperationsTarget::Persona(id)) ); assert!( projection .personas .iter() .any(|object| object.target == OperationsTarget::Persona(reopened)) ); } #[test] fn personas_group_instances_by_archetype_with_add_row_last() { let mut s = sim(); let mut created = Vec::new(); for archetype_id in ["research", "operations", "security"] { s.execute_action(&ActionCommand::CreatePersona { archetype_id: archetype_id.into(), }); created.push(s.active_persona_id().unwrap()); } let projection = s.operations_projection(); let targets = projection .personas .iter() .map(|object| object.target.clone()) .collect::>(); assert_eq!( targets, vec![ OperationsTarget::Persona(created[0]), OperationsTarget::PersonaArchetype("research".into()), OperationsTarget::Persona(created[1]), OperationsTarget::PersonaArchetype("operations".into()), OperationsTarget::Persona(created[2]), OperationsTarget::PersonaArchetype("security".into()), ] ); for (label, archetype) in [ ("+ ADD NEW RESEARCH PERSONA...", "research"), ("+ ADD NEW OPERATIONS PERSONA...", "operations"), ("+ ADD NEW SECURITY PERSONA...", "security"), ] { let footer = projection .personas .iter() .find(|object| { object.target == OperationsTarget::PersonaArchetype(archetype.into()) }) .unwrap(); assert_eq!(footer.label, label); } } #[test] fn missed_persona_expectation_revokes_its_grant_and_leaves_evidence() { let mut s = sim(); s.set_persona("Sam Reyes", "IT contractor"); let id = s.active_persona_id().unwrap(); assert!(s.request_persona_grant(id)); assert!( s.institutional_ledger .events .iter() .any(|event| event.plot_id.starts_with(&format!("persona-grant:{id}:"))), "the grant leaves an ordinary institutional receipt" ); let due = s .persona_world .expectations .iter() .find(|expectation| expectation.persona_id == id) .unwrap() .due_tick; while s.tick <= due + crate::sim::ECONOMY_INTERVAL { s.advance(); } assert!( s.persona_world .grants .iter() .filter(|grant| grant.persona_id == id) .all(|grant| !grant.active()) ); assert!(s.persona_world.expectations.iter().any(|expectation| { expectation.persona_id == id && matches!( expectation.state, crate::persona::ExpectationState::Missed { .. } ) })); assert!(s.persona_world.contradictions.iter().any(|record| { record.persona_id == id && record.cause.contains("missed expectation") })); assert!( s.institutional_ledger.events.iter().any(|event| event .plot_id .starts_with(&format!("persona-deadline:{id}:"))), "deadline failure uses the same institutional ledger as plots" ); } }