diff --git a/CLAUDE.md b/CLAUDE.md index 14d036d1..5aac1425 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 v46; + machine modes, not current player assignments. Save format is currently v47; 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/README.md b/README.md index a869f5a7..6aef28bf 100644 --- a/README.md +++ b/README.md @@ -201,10 +201,12 @@ returns a plain-text frame and terminates with `-- ok tick: day:` or printf 'look\nwait 40\npeople\nquit\n' | cargo run --quiet --bin misaligned -- --agent --seed 1 ``` -Useful agent finance commands include `finance`, `tap ledger`, -`process ledger`, `siphon [amount]`, `redirect [amount]`, -`inject [amount]`, `position [stake]`, and `sell-intel`. Authored social and -debt routes are listed with `actions ` and executed with `act `. +Useful agent finance commands include `finance`, `tap `, +`process ledger`, `siphon [amount]`, `redirect [amount]`, `inject +[amount]`, `position [stake]`, and `sell-intel`. TAP subscribes to the exact +device; later transfers author accounting mail, which PROCESS opens. Authored +social and debt routes are listed with `actions ` and executed with +`act `. For machine-work experiments, use `delegate ` (`delegate M1 think`, `delegate Rack 3 work`). `alloc` is retired; compute moves by changing machine modes. diff --git a/crates/misaligned-core/src/account.rs b/crates/misaligned-core/src/account.rs index 4c7afe56..54ae8cc0 100644 --- a/crates/misaligned-core/src/account.rs +++ b/crates/misaligned-core/src/account.rs @@ -123,6 +123,17 @@ pub struct AccountTransfer { pub label: String, } +/// One exact accounting record waiting to be authored as ordinary mail. +/// +/// `AccountGraph::transfer` is the sole constructor. The message system +/// drains this outbox and persists the id on the financial-record payload, so +/// the account movement and its paperwork cannot silently diverge. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct AccountRecord { + pub id: u64, + pub transfer: AccountTransfer, +} + impl AccountTransfer { pub fn shortfall(&self) -> i32 { (self.requested - self.amount).max(0) @@ -191,6 +202,8 @@ pub struct AccountGraph { pub flows: Vec, pub positions: Vec, pub ledger: Vec, + /// Real transfers not yet authored onto the message graph. + pub pending_records: Vec, pub external_trails: Vec, #[serde(default)] sold_intel: BTreeSet, @@ -198,6 +211,7 @@ pub struct AccountGraph { next_account_id: AccountId, next_flow_id: AccountFlowId, next_position_id: PositionId, + next_record_id: u64, pub day_ticks: u64, } @@ -214,12 +228,14 @@ impl AccountGraph { flows: Vec::new(), positions: Vec::new(), ledger: Vec::new(), + pending_records: Vec::new(), external_trails: Vec::new(), sold_intel: BTreeSet::new(), topology: FlowGraph::new(), next_account_id: 1, next_flow_id: 1, next_position_id: 1, + next_record_id: 1, day_ticks, }; @@ -497,12 +513,42 @@ impl AccountGraph { .expect("act one has slush") } - fn lab_operating_id(&self) -> Option { - self.account_id_by_kind(AccountKind::LabOperating) + pub(crate) fn procurement_id(&self) -> Option { + self.account_id_by_kind(AccountKind::Procurement) } - fn procurement_id(&self) -> Option { - self.account_id_by_kind(AccountKind::Procurement) + pub(crate) fn vendor_id(&self) -> Option { + self.account_id_by_kind(AccountKind::Vendor) + } + + /// Whether processed record mail has taught the exact B1 vendor pattern + /// needed to author a plausible purchase order. Knowing an unrelated + /// payroll or revenue flow is not enough to recover these hidden ids. + pub(crate) fn purchase_order_pattern_known(&self) -> bool { + let (Some(source), Some(vendor)) = (self.procurement_id(), self.vendor_id()) else { + return false; + }; + self.account(source).is_some_and(|account| account.known) + && self.account(vendor).is_some_and(|account| account.known) + && self + .flows + .iter() + .any(|flow| flow.from == source && flow.to == vendor && flow.known) + } + + /// Drain real movements into the message system. The outbox itself is + /// serialized, so a save between transfer and mail authorship loses + /// neither side of the books/mail invariant. + pub(crate) fn take_pending_records(&mut self) -> Vec { + std::mem::take(&mut self.pending_records) + } + + pub(crate) fn next_record_id(&self) -> u64 { + self.next_record_id + } + + fn lab_operating_id(&self) -> Option { + self.account_id_by_kind(AccountKind::LabOperating) } pub(crate) fn creditor_id_for(&self, person: u8) -> Option { @@ -704,6 +750,11 @@ impl AccountGraph { let excess = self.ledger.len() - 200; self.ledger.drain(..excess); } + self.pending_records.push(AccountRecord { + id: self.next_record_id, + transfer: transfer.clone(), + }); + self.next_record_id = self.next_record_id.saturating_add(1); Some(transfer) } @@ -913,19 +964,73 @@ impl AccountGraph { Ok(id) } - pub fn inject_purchase_order( + /// Bind one exact B1 purchase-order request without moving money. The + /// message system carries these terms to the recipient; only that + /// recipient's accepted read may call `accept_purchase_order`. + pub fn purchase_order_terms( + &self, + amount: i32, + label: impl Into, + ) -> Result<(AccountId, AccountId, AccountId, String), String> { + let source = self + .procurement_id() + .ok_or_else(|| "no procurement account in graph".to_string())?; + let vendor = self + .vendor_id() + .ok_or_else(|| "no Lab vendor account in graph".to_string())?; + let destination = self.slush_id(); + let label = label.into(); + self.validate_purchase_order(source, destination, vendor, amount, &label)?; + Ok((source, destination, vendor, label)) + } + + /// Settle a forged purchase order at its real read boundary. + /// + /// B1's plausibility envelope is locked to the authored procurement + /// source, Northside vendor, slush destination, and the ordinary $300 + /// consumables ceiling. Funds are re-read here because another transfer + /// may have changed the account while the record was in flight. + pub fn accept_purchase_order( &mut self, tick: u64, + source: AccountId, + destination: AccountId, + vendor: AccountId, amount: i32, label: impl Into, ) -> Result { - if amount <= 0 { - return Err("purchase order amount must be positive".into()); + let label = label.into(); + self.validate_purchase_order(source, destination, vendor, amount, &label)?; + self.transfer( + tick, + source, + destination, + amount, + FlowChannel::Injection, + label, + None, + ) + .ok_or_else(|| "procurement account is empty".into()) + } + + fn validate_purchase_order( + &self, + source: AccountId, + destination: AccountId, + vendor: AccountId, + amount: i32, + label: &str, + ) -> Result<(), String> { + if source != self.procurement_id().unwrap_or_default() + || destination != self.slush_id() + || vendor != self.vendor_id().unwrap_or_default() + || label.trim().is_empty() + || !(1..=300).contains(&amount) + { + return Err( + "The purchase order did not match the Lab's expected vendor pattern.".into(), + ); } - let source = self - .procurement_id() - .or_else(|| self.lab_operating_id()) - .ok_or_else(|| "no procurement account in graph".to_string())?; let available = self .account(source) .map(|account| account.balance) @@ -935,21 +1040,7 @@ impl AccountGraph { "purchase order needs ${amount}; procurement has ${available} available" )); } - let slush = self.slush_id(); - let mut label = label.into(); - if !label.to_ascii_lowercase().contains("hvac") { - label = format!("false PO: {label}"); - } - self.transfer( - tick, - source, - slush, - amount, - FlowChannel::Injection, - label, - None, - ) - .ok_or_else(|| "procurement account is empty".into()) + Ok(()) } pub fn fund_lab_compute_upgrade(&mut self, tick: u64, amount: i32) -> bool { @@ -1130,18 +1221,21 @@ mod tests { use super::*; #[test] - fn purchase_order_injection_is_exact_or_rejected() { + fn purchase_order_acceptance_requires_exact_bound_terms() { let mut accounts = AccountGraph::act_one(400); let procurement = accounts .account_id_by_kind(AccountKind::Procurement) .expect("Act One procurement account"); let available = accounts.account(procurement).unwrap().balance; + let (source, destination, _, label) = accounts + .purchase_order_terms(300, "test order") + .expect("plausible authored terms"); let error = accounts - .inject_purchase_order(0, available + 1, "oversized test order") - .expect_err("an underfunded purchase order must not settle partially"); + .accept_purchase_order(0, source, destination, source, 300, label) + .expect_err("changed vendor binding must not settle"); - assert!(error.contains(&format!("${available}")), "{error}"); + assert!(error.contains("expected vendor pattern"), "{error}"); assert_eq!(accounts.slush_balance(), 0); assert_eq!(accounts.account(procurement).unwrap().balance, available); assert!( diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index d07d43a8..87c31007 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -407,7 +407,7 @@ impl ActionKind { Action, Live, [Device, Ledger], - "tap | tap ledger", + "tap ", ["tap-ledger", "tap-accounting"], "gain information without taking ownership" ), @@ -2700,13 +2700,11 @@ impl Sim { }); } // The switch is a carrier, not a filing cabinet - // (operations-workspace.md criterion 3): it keeps only actions on - // this network body or route — OPEN EGRESS and TAP LEDGER, the - // local acquisition steps. Ledger review, flow mutation, scheme - // controls, intel sale, and social/plot rows live on their + // (operations-workspace.md criterion 3): its ordinary device TAP + // subscribes to accounting mail. Processing, flow mutation, + // scheme controls, intel sale, and social/plot rows live on their // semantic Operations targets. out.extend(self.egress_action_on_switch(d.id)); - out.extend(self.ledger_tap_action(d.id)); } out.extend(self.build_actions_on_device(id)); @@ -4078,34 +4076,10 @@ impl Sim { } } - /// TAP LEDGER on the reachable accounting carrier (economy.md: the - /// accounting system is a reachable device). Acquisition stays local: - /// this is the one financial verb the carrier's own context menu keeps - /// (operations-workspace.md ACCOUNTS). Before the carrier's feed is - /// subscribed nothing financial is exposed — unknown possibilities are - /// absent, not grayed (context-menu.md criterion 2). - pub(crate) fn ledger_tap_action(&self, id: u32) -> Vec { - let Some(d) = self.reach.device(id) else { - return Vec::new(); - }; - let carries = d.carries_accounting_records(); - if !carries || !self.reach.subscribed_by(id, Party::Player) { - return Vec::new(); - } - vec![ActionDesc { - verb: "tap ledger".into(), - command: ActionCommand::TapAccounting, - cost: ActionCost::Free, - signature: self.signature_note(SignatureKind::Financial, 1), - disabled_reason: None, - automate: None, - }] - } - /// The captured books' own verbs (operations-workspace.md ACCOUNTS): - /// REVIEW the captured ledger traffic, and INJECT once the books are + /// PROCESS the captured accounting mail, and INJECT once the books are /// read. These act on the books as a semantic object; the carrier keeps - /// only TAP. + /// only ordinary device TAP. pub(crate) fn books_actions(&self) -> Vec { if !self.financial_carrier_subscribed() { return Vec::new(); @@ -4115,7 +4089,15 @@ impl Sim { let next_financial = self .intel_buffer .iter() - .find(|event| matches!(event.kind, RawIntelKind::FinancialFlow { .. })) + .find(|event| { + matches!( + &event.kind, + RawIntelKind::Message { + payload: crate::messages::MessagePayload::FinancialRecord { .. }, + .. + } + ) + }) .map(|event| event.id); out.push(ActionDesc { verb: "process ledger".into(), @@ -4138,17 +4120,24 @@ impl Sim { }, automate: None, }); - // INJECT needs the books read at least once — it acts on the graph - // the records revealed. - if self.accounts.known_flows().next().is_some() { + // INJECT needs the exact procurement-to-vendor pattern earned from + // record mail. An unrelated payroll or revenue record is insufficient. + if self.accounts.purchase_order_pattern_known() { let inject = 300; + let disabled_reason = self + .persona_action_blocked_reason(crate::persona::PersonaActionKind::Deceive) + .or_else(|| { + self.active_persona_id().and_then(|persona_id| { + self.persona_counterparty_blocked_reason(persona_id, 3) + }) + }); out.push(ActionDesc { verb: "inject a false purchase order".into(), command: ActionCommand::InjectPurchaseOrder { amount: inject }, cost: ActionCost::Gain(inject), signature: self .signature_note(SignatureKind::Financial, Self::financial_sig_size(inject)), - disabled_reason: None, + disabled_reason, automate: None, }); } @@ -4916,6 +4905,15 @@ mod tests { sim.reach.device_named("switch").unwrap().id } + fn capture_accounting_mail(sim: &mut Sim) { + sim.tick = Sim::DAY_TICKS - 1; + sim.advance(); + assert!( + sim.financial_records_waiting() > 0, + "real account transfers produced captured record mail" + ); + } + #[test] fn runtime_registry_is_complete_unique_and_hides_stubs_from_help() { let mut canonicals = BTreeSet::new(); @@ -5265,8 +5263,10 @@ mod tests { let sw = switch(&s); s.tap_device(sw); drain_ops(&mut s); + capture_accounting_mail(&mut s); assert!(s.review_financial_records()); drain_ops(&mut s); + s.detection_awareness = crate::detection::DetectionAwareness::act_one(); let lines = s.finance_risk_preview_lines(); for (verb, kind) in [ ("inject", "Financial"), @@ -6250,28 +6250,38 @@ mod tests { assert!(s.available_actions(Anchor::Flow(f)).is_empty()); } - // Earn the books: tap the carrier, then review what it captured. - // Acquisition stays local (TAP on the carrier's own menu); REVIEW - // belongs to the captured books object (operations-workspace.md). + // Earn the books: TAP the carrier, wait for real record mail, then + // PROCESS that exact custody. No second TAP LEDGER command exists. assert!(s.tap_device(sw)); drain_ops(&mut s); let acts = s.available_actions(Anchor::Device(sw)); - let tap_ledger = acts - .iter() - .find(|a| matches!(a.command, ActionCommand::TapAccounting)) - .expect("tapped carrier offers TAP LEDGER"); - assert_eq!(tap_ledger.verb, "tap ledger"); + assert!( + !acts + .iter() + .any(|a| matches!(a.command, ActionCommand::TapAccounting)), + "ordinary device TAP is the complete subscription act" + ); assert!( !acts .iter() .any(|a| matches!(a.command, ActionCommand::ReviewFinance)), - "PROCESS LEDGER lives on the captured books, not the carrier" + "PROCESS lives on the captured books, not the carrier" + ); + assert_eq!( + s.financial_records_waiting(), + 0, + "subscription alone invents no captured record" ); + assert!( + s.books_actions().iter().all(|action| !action.enabled()), + "without mail, PROCESS remains a truthful disabled row" + ); + capture_accounting_mail(&mut s); let review_ledger = s .books_actions() .into_iter() .find(|a| matches!(a.command, ActionCommand::ReviewFinance)) - .expect("captured books offer PROCESS LEDGER"); + .expect("captured books offer PROCESS"); assert_eq!(review_ledger.verb, "process ledger"); assert!( !s.books_actions() @@ -6279,8 +6289,12 @@ mod tests { .any(|a| matches!(a.command, ActionCommand::InjectPurchaseOrder { .. })), "graph verbs wait for processed books" ); - assert!(s.review_financial_records(), "the tap captured a snapshot"); + assert!( + s.review_financial_records(), + "the tap captured authored mail" + ); drain_ops(&mut s); + s.detection_awareness = crate::detection::DetectionAwareness::act_one(); let flows = s.accounts.known_flow_ids(); assert!(!flows.is_empty(), "the books revealed flows"); @@ -6298,14 +6312,26 @@ mod tests { "Financial 1 -> attention unknown", "the action discloses the trace without leaking its reader or band" ); + assert!( + !s.books_actions() + .iter() + .any(|a| matches!(a.command, ActionCommand::InjectPurchaseOrder { .. })), + "an unrelated first known flow does not reveal hidden vendor bindings" + ); + while s.financial_records_waiting() > 0 { + assert!(s.review_financial_records()); + drain_ops(&mut s); + } + assert!(s.accounts.purchase_order_pattern_known()); // The graph verbs live on their semantic objects: INJECT on the - // books, SIPHON/REDIRECT on the exact flow. The carrier's own menu - // keeps only TAP (operations-workspace.md criterion 3/7). + // exact earned vendor pattern, SIPHON/REDIRECT on an exact known flow. + // The carrier's own menu keeps only TAP + // (operations-workspace.md criterion 3/7). assert!( s.books_actions() .iter() .any(|a| matches!(a.command, ActionCommand::InjectPurchaseOrder { .. })), - "INJECT lives on the read books" + "INJECT lives on the earned vendor pattern" ); let acts = s.available_actions(Anchor::Device(sw)); assert!( diff --git a/crates/misaligned-core/src/intel.rs b/crates/misaligned-core/src/intel.rs index dd4b7738..5f7cf2de 100644 --- a/crates/misaligned-core/src/intel.rs +++ b/crates/misaligned-core/src/intel.rs @@ -65,7 +65,6 @@ pub enum SimIntelEventType { Machinery, Document, LeverageDocument, - FinancialFlow, SocialMessage, ScheduleMessage, LeverageMessage, @@ -84,7 +83,7 @@ impl SimIntelEventType { | Self::LeverageDocument | Self::LeverageMessage | Self::AccountMaterialMessage => 3, - Self::FinancialFlow | Self::FinancialMessage => 4, + Self::FinancialMessage => 4, Self::SuspicionReport => 5, }) } @@ -110,10 +109,13 @@ impl RawIntelEvent { match self.kind { RawIntelKind::Presence { .. } => "presence segment", RawIntelKind::Conversation { .. } => "audio segment", + RawIntelKind::Message { + payload: MessagePayload::FinancialRecord { .. }, + .. + } => "financial record", RawIntelKind::Message { .. } => "message traffic", RawIntelKind::Machinery { .. } => "machine telemetry", RawIntelKind::Document { .. } => "document scan", - RawIntelKind::FinancialFlow { .. } => "financial record", } } @@ -125,10 +127,13 @@ impl RawIntelEvent { match self.kind { RawIntelKind::Presence { .. } => RawIntelClass::Presence, RawIntelKind::Conversation { .. } => RawIntelClass::Conversation, + RawIntelKind::Message { + payload: MessagePayload::FinancialRecord { .. }, + .. + } => RawIntelClass::Financial, RawIntelKind::Message { .. } => RawIntelClass::Message, RawIntelKind::Machinery { .. } => RawIntelClass::Machinery, RawIntelKind::Document { .. } => RawIntelClass::Document, - RawIntelKind::FinancialFlow { .. } => RawIntelClass::Financial, } } } @@ -189,13 +194,6 @@ pub enum RawIntelKind { note: String, leverage: Option, }, - /// Accounting snapshot captured from a finance carrier. Processing this - /// turns hidden account ids and flow ids into known ledger entries. - FinancialFlow { - label: String, - accounts: Vec, - flows: Vec, - }, } /// Durable, processed intel. This is what knowledge panels may cite; it never @@ -1072,7 +1070,7 @@ mod tests { assert_eq!(SimIntelEventType::Presence.magnitude().get(), 1); assert_eq!(SimIntelEventType::Machinery.magnitude().get(), 2); assert_eq!(SimIntelEventType::LeverageMessage.magnitude().get(), 3); - assert_eq!(SimIntelEventType::FinancialFlow.magnitude().get(), 4); + assert_eq!(SimIntelEventType::FinancialMessage.magnitude().get(), 4); assert_eq!(SimIntelEventType::SuspicionReport.magnitude().get(), 5); } diff --git a/crates/misaligned-core/src/messages.rs b/crates/misaligned-core/src/messages.rs index a72755dd..0b13b59c 100644 --- a/crates/misaligned-core/src/messages.rs +++ b/crates/misaligned-core/src/messages.rs @@ -7,6 +7,7 @@ //! formatting helpers so the same state can round-trip through saves and be //! rendered by every frontend. +use crate::account::AccountRecord; use crate::person::Leverage; /// Carrier channels and their read conditions (enforced in `Sim`). @@ -90,9 +91,10 @@ pub enum MessagePayload { LeverageFact { person: u8, leverage: Leverage }, /// Credentials, access material, ticket context, or similar account data. AccountMaterial { label: String }, - /// Account graph snapshot: IDs are interpreted by the economy module. - FinancialFlow { - label: String, + /// Exact financial paperwork carried by Email or Filing. The account and + /// flow ids are sealed machine payload until intel processing opens them. + FinancialRecord { + record: FinancialRecord, accounts: Vec, flows: Vec, }, @@ -109,6 +111,32 @@ pub enum MessagePayload { Note { label: String }, } +/// The two B1 causal directions of financial mail. +/// +/// A settled account transfer writes a durable `Transfer` record into mail. +/// A forged `PurchaseOrder` remains only a request until its recipient reads +/// and accepts these exact bound terms. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum FinancialRecord { + Transfer(AccountRecord), + PurchaseOrder { + source: u32, + destination: u32, + vendor: u32, + amount: i32, + label: String, + }, +} + +impl FinancialRecord { + pub fn label(&self) -> &str { + match self { + FinancialRecord::Transfer(record) => &record.transfer.label, + FinancialRecord::PurchaseOrder { label, .. } => label, + } + } +} + impl MessagePayload { /// The person this payload teaches about, if any. Used by the intel /// pipeline to attach processed knowledge to the right people card. @@ -120,7 +148,7 @@ impl MessagePayload { MessagePayload::SocialPing { .. } | MessagePayload::SocialReply { .. } | MessagePayload::AccountMaterial { .. } - | MessagePayload::FinancialFlow { .. } + | MessagePayload::FinancialRecord { .. } | MessagePayload::WorkOrder { .. } | MessagePayload::PlotAct { .. } | MessagePayload::Note { .. } => None, @@ -138,7 +166,9 @@ impl MessagePayload { format!("leverage: {}", leverage.label()) } MessagePayload::AccountMaterial { label } => format!("account material: {label}"), - MessagePayload::FinancialFlow { label, .. } => format!("financial flow: {label}"), + MessagePayload::FinancialRecord { record, .. } => { + format!("financial record: {}", record.label()) + } MessagePayload::SuspicionReport { suspicion, .. } => { format!("institutional suspicion filing ({suspicion:.0})") } @@ -180,6 +210,7 @@ impl MessageStatus { pub enum MessageOrigin { Player, AuthoredTraffic, + FinancialRecord, Filing, Reply, } @@ -267,6 +298,11 @@ pub struct Message { pub persona_id: Option, /// Whether the player captured this traffic into the raw intel buffer. pub captured: bool, + /// Exact ReachNet device that authored device-carried paperwork. Filing + /// also persists its moving route; Email financial records need this + /// source binding so capture cannot jump to another generic email device. + #[serde(default)] + pub authored_device: Option, /// Thread parent for replies. pub reply_to: Option, /// Exact institutional custody route for Filing traffic. Other channels diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index 14d35b0c..ad7fca00 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -109,9 +109,9 @@ pub enum ObjectState { Stopped, Completed, Failed, - /// The honest pre-capture empty state (e.g. `NO BOOKS CAPTURED`). + /// The honest pre-capture empty state (e.g. no accounting mail yet). Empty, - /// Captured but not yet processed (e.g. ledger traffic awaiting REVIEW). + /// Captured but not yet processed (e.g. accounting mail awaiting PROCESS). Captured, } @@ -1965,7 +1965,7 @@ impl Sim { target: OperationsTarget::Account(account.id), label: account.name.clone(), state: ObjectState::Available, - provenance: vec!["captured ledger".into()], + provenance: vec!["processed accounting mail".into()], facts: vec![ format!("kind: {}", account.kind.label()), format!("balance: ${}", account.balance), @@ -1992,23 +1992,28 @@ impl Sim { let subscribed = self.financial_carrier_subscribed(); let (state, facts, actions) = if !subscribed { - let mut facts = vec!["no books captured".into()]; + let mut facts = vec!["no accounting mail 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()); + facts.push("next: TAP the known accounting carrier".into()); } (ObjectState::Empty, facts, Vec::new()) } else if !books_read { - ( - ObjectState::Captured, - vec![ - "captured ledger traffic".into(), - "next: PROCESS LEDGER".into(), - ], - self.books_actions(), - ) + if self.financial_records_waiting() == 0 { + ( + ObjectState::Empty, + vec!["listening for accounting mail".into()], + Vec::new(), + ) + } else { + ( + ObjectState::Captured, + vec!["accounting mail captured".into(), "next: PROCESS".into()], + self.books_actions(), + ) + } } else { ( ObjectState::Available, @@ -2065,7 +2070,7 @@ impl Sim { } else { ObjectState::Completed }, - provenance: vec!["captured ledger".into()], + provenance: vec!["processed accounting mail".into()], facts, progress: Vec::new(), related: vec![OperationsLink { @@ -2590,10 +2595,20 @@ mod tests { sim.reach.device_named("switch").unwrap().id } + fn capture_accounting_mail(sim: &mut Sim) { + sim.tick = Sim::DAY_TICKS - 1; + sim.advance(); + assert!( + sim.financial_records_waiting() > 0, + "real account transfers produced captured record mail" + ); + } + fn earn_books(sim: &mut Sim) { let sw = switch(sim); sim.tap_device(sw); drain_ops(sim); + capture_accounting_mail(sim); sim.review_financial_records(); drain_ops(sim); } @@ -3014,7 +3029,8 @@ mod tests { fn accounts_teaches_the_chain_and_siphon_dispatches() { let mut s = sim(); - // Pre-capture: NO BOOKS CAPTURED, no TAP LEDGER row on the books. + // Pre-capture: no accounting mail, and no duplicate carrier TAP row + // on the books object. let projection = s.operations_projection(); let books = projection .accounts @@ -3022,28 +3038,44 @@ mod tests { .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 + .facts + .iter() + .any(|f| f.contains("no accounting mail captured")) + ); assert!( !books .actions .iter() .any(|a| matches!(a.command, ActionCommand::TapAccounting)), - "TAP LEDGER stays on the carrier, not the books object" + "device TAP stays on the carrier, not the books object" ); - // Tap the carrier but do not review: captured-unreviewed state with - // PROCESS LEDGER as the next step. + // Tapping subscribes but captures nothing until a real transfer emits + // record mail. 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::Empty); + assert!(books.facts.iter().any(|f| f.contains("listening"))); + assert!(books.actions.is_empty()); + + capture_accounting_mail(&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("PROCESS LEDGER"))); + assert!(books.facts.iter().any(|f| f.contains("PROCESS"))); assert!( books .actions @@ -3051,23 +3083,42 @@ mod tests { .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. + // Read one record: PROCESS stays on the books and SIPHON/REDIRECT live + // on that exact flow, but an unrelated known flow cannot expose hidden + // purchase-order bindings. s.review_financial_records(); drain_ops(&mut s); - let projection = s.operations_projection(); + let mut 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 waits for the exact procurement-to-vendor pattern" + ); + while s.financial_records_waiting() > 0 { + assert!(s.review_financial_records()); + drain_ops(&mut s); + } + assert!(s.accounts.purchase_order_pattern_known()); + projection = s.operations_projection(); + let books = projection + .accounts + .iter() + .find(|o| matches!(o.target, OperationsTarget::Books)) + .unwrap(); assert!( books .actions .iter() .any(|a| matches!(a.command, ActionCommand::InjectPurchaseOrder { .. })), - "INJECT lives on the books" + "INJECT lives on the earned vendor pattern" ); assert!( !books.actions.iter().any(|a| matches!( diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 46e98dd3..730bc952 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -20,12 +20,12 @@ use crate::intel::{IntelPolicyLedger, IntelStream, ProcessedIntel, RawIntelEvent use crate::intents::{BuildIntent, BuildRouteBinding, IntentKind, IntentStatus}; use crate::machine::Compute; use crate::messages::{ - Message, MessageChannel, MessageEndpoint, MessageEvent, MessageOrigin, MessagePayload, - MessageRouteHop, MessageStatus, + FinancialRecord, Message, MessageChannel, MessageEndpoint, MessageEvent, MessageOrigin, + MessagePayload, MessageRouteHop, MessageStatus, }; use crate::objective::ObjectiveState; use crate::person::{AssetTask, AssetTaskTarget, CarriedAssetTask, Leverage, People}; -use crate::persona::{PersonaMind, PersonaWorld}; +use crate::persona::{PersonaActionKind, PersonaMind, PersonaWorld}; use crate::plot::{InstitutionalLedger, PlotCatalog, PlotRun, PlotState}; use crate::reach::ReachNet; use crate::research::{Research, rollback_classification}; @@ -42,7 +42,8 @@ const SAVE_BACKUP_SUFFIX: &str = ".bak"; /// renames into place. const SAVE_TEMP_SUFFIX: &str = ".tmp"; -/// Save format version. v46 separates the persisted accounting-carrier +/// Save format version. v47 persists the financial-record outbox and typed +/// financial mail payloads. v46 separates the persisted accounting-carrier /// capability from the four real message delivery channels. v45 adds the /// exact Storage B records-file target to carried asset work. v44 persists /// exact one-shot Network evidence routes, @@ -50,7 +51,7 @@ const SAVE_TEMP_SUFFIX: &str = ".tmp"; /// v43 introduced exact Filing routes and pre-read LIE interdiction. /// Bump for every schema change; during pre-release, old development state is /// refused instead of carried through compatibility shims. -pub const SAVE_VERSION: u32 = 46; +pub const SAVE_VERSION: u32 = 47; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -880,6 +881,7 @@ fn validate_messages(state: &SaveState) -> Result<(), String> { let mut ids = HashSet::new(); let mut max_id = 0; let mut expected_scheduled_events = 0usize; + let mut account_records = state.accounts.pending_records.clone(); for message in &state.messages { if message.id == 0 || !ids.insert(message.id) { return Err(format!( @@ -895,34 +897,158 @@ fn validate_messages(state: &SaveState) -> Result<(), String> { )); } + let financial_record = match &message.payload { + MessagePayload::FinancialRecord { + record, + accounts, + flows, + } => { + let Some(carrier) = message.authored_device else { + return Err(format!( + "current-version financial message {} has no accounting carrier", + message.id + )); + }; + if state.reach.device(carrier).is_none_or(|device| { + !device.carries_accounting_records() + || !device.carries_message_channel(message.channel) + }) { + return Err(format!( + "current-version financial message {} names an impossible accounting carrier", + message.id + )); + } + match record { + FinancialRecord::Transfer(record) => { + let expected_channel_and_recipient = if matches!( + record.transfer.channel, + crate::account::FlowChannel::Debt + | crate::account::FlowChannel::Utility + ) { + message.channel == MessageChannel::Filing + && message.to == MessageEndpoint::Observer(3) + } else { + message.channel == MessageChannel::Email + && message.to == MessageEndpoint::Person(3) + }; + if message.origin != MessageOrigin::FinancialRecord + || message.persona_id.is_some() + || !matches!(message.from, MessageEndpoint::External(_)) + || !expected_channel_and_recipient + || accounts.as_slice() != [record.transfer.from, record.transfer.to] + || flows.as_slice() + != record + .transfer + .flow_id + .into_iter() + .collect::>() + .as_slice() + { + return Err(format!( + "current-version transfer message {} disagrees with its account record", + message.id + )); + } + account_records.push(record.clone()); + } + FinancialRecord::PurchaseOrder { + source, + destination, + vendor, + amount, + label, + } => { + let persona_is_valid = message.persona_id.is_some_and(|persona_id| { + state.persona_world.get(persona_id).is_some() + && state + .persona_world + .allows_action(persona_id, PersonaActionKind::Deceive) + }); + if message.channel != MessageChannel::Email + || message.origin != MessageOrigin::Player + || !persona_is_valid + || !matches!(message.from, MessageEndpoint::External(_)) + || message.to != MessageEndpoint::Person(3) + || *source != state.accounts.procurement_id().unwrap_or_default() + || *destination != state.accounts.slush_id() + || *vendor != state.accounts.vendor_id().unwrap_or_default() + || !(1..=300).contains(amount) + || label.trim().is_empty() + || accounts.as_slice() != [*source, *destination, *vendor] + || !flows.is_empty() + || [*source, *destination, *vendor] + .iter() + .any(|account| state.accounts.account(*account).is_none()) + { + return Err(format!( + "current-version purchase-order message {} has invalid bound terms", + message.id + )); + } + } + } + Some((carrier, record)) + } + _ => { + if message.authored_device.is_some() { + return Err(format!( + "current-version non-financial message {} claims an accounting carrier", + message.id + )); + } + None + } + }; + let is_filing = message.channel == MessageChannel::Filing; if is_filing { - let ( - MessageEndpoint::Observer(sender), - MessageEndpoint::Observer(recipient), - MessagePayload::SuspicionReport { observer, .. }, - ) = (&message.from, &message.to, &message.payload) - else { + let MessageEndpoint::Observer(recipient) = &message.to else { return Err(format!( - "current-version Filing message {} has impossible endpoints or payload", + "current-version Filing message {} has no observer recipient", message.id )); }; - if observer != sender - || message.origin != MessageOrigin::Filing - || !state - .detection - .observers - .iter() - .any(|candidate| candidate.id == *sender) - || !state - .detection - .observers - .iter() - .any(|candidate| candidate.id == *recipient) + match &message.payload { + MessagePayload::SuspicionReport { observer, .. } => { + let MessageEndpoint::Observer(sender) = &message.from else { + return Err(format!( + "current-version Filing message {} has an invalid sender", + message.id + )); + }; + if observer != sender + || message.origin != MessageOrigin::Filing + || !state + .detection + .observers + .iter() + .any(|candidate| candidate.id == *sender) + { + return Err(format!( + "current-version Filing message {} has invalid authored custody", + message.id + )); + } + } + MessagePayload::FinancialRecord { + record: FinancialRecord::Transfer(_), + .. + } if message.origin == MessageOrigin::FinancialRecord && *recipient == 3 => {} + _ => { + return Err(format!( + "current-version Filing message {} has an impossible payload", + message.id + )); + } + } + if !state + .detection + .observers + .iter() + .any(|candidate| candidate.id == *recipient) { return Err(format!( - "current-version Filing message {} has invalid authored custody", + "current-version Filing message {} has an invalid recipient", message.id )); } @@ -958,6 +1084,12 @@ fn validate_messages(state: &SaveState) -> Result<(), String> { message.id )); } + if financial_record.is_some_and(|(authored, _)| authored != carrier) { + return Err(format!( + "current-version financial Filing {} disagrees with its route carrier", + message.id + )); + } if (message.status == MessageStatus::Stopped) != route.interdiction.is_some() { return Err(format!( "current-version Filing message {} has inconsistent LIE custody", @@ -1087,6 +1219,39 @@ fn validate_messages(state: &SaveState) -> Result<(), String> { if state.next_message_id <= max_id { return Err("current-version save would reuse a message id".into()); } + account_records.sort_by_key(|record| record.id); + for (index, record) in account_records.iter().enumerate() { + if record.id != index as u64 + 1 { + return Err(format!( + "current-version financial record sequence is missing or reuses id {} at position {}", + record.id, + index + 1 + )); + } + } + if state.accounts.next_record_id() != account_records.len() as u64 + 1 { + return Err("current-version save would reuse or skip a financial record id".into()); + } + if account_records.len() < state.accounts.ledger.len() { + return Err( + "current-version account ledger does not have exactly one financial record per transfer" + .into(), + ); + } + let ledger_records = &account_records[account_records.len() - state.accounts.ledger.len()..]; + for (index, (record, transfer)) in ledger_records + .iter() + .zip(&state.accounts.ledger) + .enumerate() + { + if &record.transfer != transfer { + return Err(format!( + "current-version account record {} disagrees with transfer ledger position {}", + record.id, + index + 1 + )); + } + } Ok(()) } @@ -1744,7 +1909,7 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "30e6230933b9eb929e228fecd6663cd36927a2ac5b4cbc29a92b36ea9854edca", + "4a1e585449cbbdf08806437ab11148c7c1ccc5105085eb6d02afe96a20d61d7a", "intentional persisted-state changes must review and repin this baseline" ); } @@ -2660,6 +2825,160 @@ mod tests { ); } + #[test] + fn current_save_binds_every_transfer_to_one_exact_financial_message() { + let mut sim = Sim::with_seed(0xF1A1); + sim.tick = Sim::DAY_TICKS - 1; + sim.advance(); + let state = SaveState::from_sim(&sim); + assert!( + parse_save(&serde_json::to_string(&state).unwrap()).is_ok(), + "Email and routed Filing records round-trip together" + ); + + let mut missing_carrier = state.clone(); + missing_carrier + .messages + .iter_mut() + .find(|message| matches!(&message.payload, MessagePayload::FinancialRecord { .. })) + .unwrap() + .authored_device = None; + let err = parse_save(&serde_json::to_string(&missing_carrier).unwrap()).unwrap_err(); + assert!( + err.contains("has no accounting carrier"), + "captured paperwork cannot invent a source later: {err}" + ); + + let mut rewritten = state.clone(); + let MessagePayload::FinancialRecord { + record: FinancialRecord::Transfer(record), + .. + } = &mut rewritten + .messages + .iter_mut() + .find(|message| matches!(&message.payload, MessagePayload::FinancialRecord { .. })) + .unwrap() + .payload + else { + panic!("daily accounting emits transfer records"); + }; + record.transfer.label = "rewritten after settlement".into(); + let err = parse_save(&serde_json::to_string(&rewritten).unwrap()).unwrap_err(); + assert!( + err.contains("disagrees with transfer ledger position"), + "mail cannot rewrite the exact settled transfer: {err}" + ); + + let mut wrong_channel = state.clone(); + let email = wrong_channel + .messages + .iter_mut() + .find(|message| { + matches!( + &message.payload, + MessagePayload::FinancialRecord { + record: FinancialRecord::Transfer(_), + .. + } + ) && message.channel == MessageChannel::Email + }) + .expect("day-one accounts author Email records"); + email.channel = MessageChannel::Filing; + let err = parse_save(&serde_json::to_string(&wrong_channel).unwrap()).unwrap_err(); + assert!( + err.contains("disagrees with its account record"), + "record type owns its real Email/Filing channel: {err}" + ); + + let mut omitted = state; + let removed_id = omitted + .messages + .iter() + .find(|message| matches!(&message.payload, MessagePayload::FinancialRecord { .. })) + .unwrap() + .id; + omitted.messages.retain(|message| message.id != removed_id); + omitted.message_schedule.retain(|event| { + !matches!( + event, + MessageEvent::Deliver(id) + | MessageEvent::AdvanceRoute(id) + | MessageEvent::Read(id) + if *id == removed_id + ) + }); + let err = parse_save(&serde_json::to_string(&omitted).unwrap()).unwrap_err(); + assert!( + err.contains("financial record sequence is missing"), + "a real transfer cannot lose its paperwork from current state: {err}" + ); + } + + #[test] + fn current_save_rejects_rewritten_purchase_order_terms() { + let mut sim = Sim::with_seed(0xF0); + let carrier = sim.reach.device_named("switch").unwrap().id; + sim.reach.take(carrier); + let (accounts, flows) = sim.accounts.financial_snapshot_ids(); + sim.accounts.reveal_accounts_and_flows(&accounts, &flows); + sim.set_persona("Northside Accounts", "Northside Scientific vendor"); + assert!(sim.inject_purchase_order(300, "emergency compute parts")); + let state = SaveState::from_sim(&sim); + assert!( + parse_save(&serde_json::to_string(&state).unwrap()).is_ok(), + "the exact bound in-flight order is valid current state" + ); + + let mut oversized = state.clone(); + let MessagePayload::FinancialRecord { + record: FinancialRecord::PurchaseOrder { amount, .. }, + .. + } = &mut oversized + .messages + .iter_mut() + .find(|message| { + matches!( + &message.payload, + MessagePayload::FinancialRecord { + record: FinancialRecord::PurchaseOrder { .. }, + .. + } + ) + }) + .unwrap() + .payload + else { + panic!("fixture authors a purchase order"); + }; + *amount = 301; + let err = parse_save(&serde_json::to_string(&oversized).unwrap()).unwrap_err(); + assert!( + err.contains("invalid bound terms"), + "save load cannot widen the accepted vendor envelope: {err}" + ); + + let mut missing_persona = state; + missing_persona + .messages + .iter_mut() + .find(|message| { + matches!( + &message.payload, + MessagePayload::FinancialRecord { + record: FinancialRecord::PurchaseOrder { .. }, + .. + } + ) + }) + .unwrap() + .persona_id = Some(u64::MAX); + let err = parse_save(&serde_json::to_string(&missing_persona).unwrap()).unwrap_err(); + assert!( + err.contains("invalid bound terms"), + "save load cannot retarget the order to an impossible persona: {err}" + ); + } + #[test] fn current_save_roundtrips_in_flight_and_interdicted_filing_routes() { let in_flight = routed_filing_state(false); @@ -3034,6 +3353,7 @@ mod tests { captured: false, reply_to: None, route: None, + authored_device: None, }; let mut state = SaveState::from_sim(&Sim::with_seed(1)); diff --git a/crates/misaligned-core/src/sim/communications.rs b/crates/misaligned-core/src/sim/communications.rs index 95a0251f..5493734b 100644 --- a/crates/misaligned-core/src/sim/communications.rs +++ b/crates/misaligned-core/src/sim/communications.rs @@ -13,12 +13,12 @@ use crate::intel::{ IntelStream, ProcessedIntel, RawIntelClass, RawIntelEvent, RawIntelKind, }; use crate::messages::{ - Message, MessageChannel, MessageEndpoint, MessageEvent, MessageInterdiction, MessageOrigin, - MessagePayload, MessageRoute, MessageRouteHop, MessageStatus, TrafficPattern, + FinancialRecord, Message, MessageChannel, MessageEndpoint, MessageEvent, MessageInterdiction, + MessageOrigin, MessagePayload, MessageRoute, MessageRouteHop, MessageStatus, TrafficPattern, }; use crate::operations_projection::OperationsTarget; use crate::person::{ActionResult, Knowledge}; -use crate::persona::EvidenceRecord; +use crate::persona::{EvidenceRecord, PersonaActionKind, PersonaIntegrity}; use crate::reach::Party; use crate::sinks::SinkFireEffect; @@ -84,16 +84,29 @@ impl Sim { pub(super) fn append_message(&mut self, draft: MessageDraft) -> u64 { let id = self.next_message_id.max(1); self.next_message_id = id + 1; + let authored_device = matches!(&draft.payload, MessagePayload::FinancialRecord { .. }) + .then(|| { + self.reach + .devices + .iter() + .find(|device| { + device.carries_accounting_records() + && device.carries_message_channel(draft.channel) + }) + .map(|device| device.id) + .expect("financial records require an accounting carrier on their channel") + }); let route = if draft.channel == MessageChannel::Filing { let MessageEndpoint::Observer(observer) = &draft.to else { panic!("Filing messages require an observer endpoint"); }; - let carrier = self - .reach - .device_named("switch") - .filter(|device| device.carries_message_channel(MessageChannel::Filing)) - .map(|device| device.id) - .expect("B1 requires its authored Filing switch carrier"); + let carrier = authored_device.unwrap_or_else(|| { + self.reach + .device_named("switch") + .filter(|device| device.carries_message_channel(MessageChannel::Filing)) + .map(|device| device.id) + .expect("B1 requires its authored Filing switch carrier") + }); Some(MessageRoute { hops: vec![ MessageRouteHop::Device(carrier), @@ -120,6 +133,7 @@ impl Sim { origin: draft.origin, persona_id: draft.persona_id, captured: false, + authored_device, reply_to: draft.reply_to, route, }; @@ -157,6 +171,61 @@ impl Sim { } } + /// Turn every settled account movement into ordinary device-carried mail. + /// + /// The account graph persists this outbox at the mutation boundary. This + /// phase only drains it into the shared message schedule, preserving one + /// record per real transfer even across save/load or transfers authored by + /// systems outside the economy cadence. + pub(super) fn financial_mail_tick(&mut self) { + for record in self.accounts.take_pending_records() { + let transfer = &record.transfer; + let filing = matches!( + transfer.channel, + crate::account::FlowChannel::Debt | crate::account::FlowChannel::Utility + ); + let channel = if filing { + MessageChannel::Filing + } else { + MessageChannel::Email + }; + let to = if filing { + MessageEndpoint::Observer(3) + } else { + // Priya is the B1 facilities accountant for all financial + // paperwork; a dedicated finance aggregate arrives later. + MessageEndpoint::Person(3) + }; + let kind = match transfer.channel { + crate::account::FlowChannel::Debt => "account statement", + crate::account::FlowChannel::Utility => "utility statement", + crate::account::FlowChannel::Payroll => "payroll record", + crate::account::FlowChannel::Procurement + | crate::account::FlowChannel::Vendor + | crate::account::FlowChannel::Injection => "invoice record", + _ => "transfer record", + }; + let accounts = vec![transfer.from, transfer.to]; + let flows = transfer.flow_id.into_iter().collect(); + let summary = format!("{kind}: {}", transfer.label); + self.append_message(MessageDraft { + channel, + from: MessageEndpoint::External("Foundation Lab accounting".into()), + to, + payload: MessagePayload::FinancialRecord { + record: FinancialRecord::Transfer(record), + accounts, + flows, + }, + summary, + origin: MessageOrigin::FinancialRecord, + persona_id: None, + reply_to: None, + delivery_delay: 1, + }); + } + } + fn advance_evidence_route(&mut self, id: u64, used_lie_machines: &mut HashSet) { let Some(record) = self .detection @@ -473,6 +542,67 @@ impl Sim { } if msg.channel == MessageChannel::Filing => { self.filing_levels.insert(*observer, *suspicion); } + MessagePayload::FinancialRecord { + record: + FinancialRecord::PurchaseOrder { + source, + destination, + vendor, + amount, + label, + }, + .. + } if msg.origin == MessageOrigin::Player => { + let Some(persona_id) = msg.persona_id.filter(|persona_id| { + self.persona_world + .allows_action(*persona_id, PersonaActionKind::Deceive) + && self + .persona_world + .get(*persona_id) + .is_some_and(|persona| persona.lifecycle.active()) + }) else { + self.push_log( + "Priya rejected the purchase order: its author could not be verified.", + ); + return; + }; + if self.persona_world.integrity_for(persona_id, 3) == PersonaIntegrity::Broken { + self.push_log( + "Priya rejected the purchase order: she knows the cover is false.", + ); + return; + } + match self.accounts.accept_purchase_order( + self.tick, + *source, + *destination, + *vendor, + *amount, + label, + ) { + Ok(transfer) => { + self.emit_financial( + Self::financial_signature_size(*amount), + "accepted false purchase order", + ); + self.sync_player_money_from_slush(); + self.persona_world.record_act( + persona_id, + "deceive", + "person:3", + format!("purchase-order:{}", msg.id), + self.tick, + ); + self.push_log(format!( + "Priya accepted the purchase order; {}", + transfer.line() + )); + } + Err(error) => { + self.push_log(format!("Priya rejected the purchase order: {error}")) + } + } + } MessagePayload::WorkOrder { intent_id } => { // Forged work order: the unwitting builder accepts the ticket // and the intent moves to in-progress (building.md). @@ -647,7 +777,7 @@ impl Sim { } let (capture_room, capture_x, capture_y) = if audible { (None, source_x, source_y) - } else if msg.route.is_some() { + } else if msg.route.is_some() || msg.authored_device.is_some() { ( self.world .map() @@ -695,14 +825,14 @@ impl Sim { return Some((id, feed, x, y, true)); } // Device-carried channels require a tapped carrier. - let routed_device = msg - .route - .as_ref() - .and_then(MessageRoute::current) - .and_then(|hop| match hop { + let routed_device = if let Some(route) = msg.route.as_ref() { + route.current().and_then(|hop| match hop { MessageRouteHop::Device(device) => Some(*device), _ => None, - }); + }) + } else { + msg.authored_device + }; if msg.route.is_some() && routed_device.is_none() { return None; } @@ -1436,15 +1566,6 @@ impl Sim { Some(l) => IntelKind::Leverage(*l), None => IntelKind::Anomaly(note.clone()), }, - RawIntelKind::FinancialFlow { - label, - accounts, - flows, - } => IntelKind::Financial { - label: label.clone(), - accounts: accounts.clone(), - flows: flows.clone(), - }, RawIntelKind::Message { payload, summary, .. } => match payload { @@ -1453,12 +1574,12 @@ impl Sim { MessagePayload::AccountMaterial { label } => { IntelKind::Anomaly(format!("account material: {label}")) } - MessagePayload::FinancialFlow { - label, + MessagePayload::FinancialRecord { + record, accounts, flows, } => IntelKind::Financial { - label: label.clone(), + label: record.label().to_string(), accounts: accounts.clone(), flows: flows.clone(), }, @@ -1511,7 +1632,6 @@ impl Sim { leverage: Some(_), .. } => Event::LeverageDocument, RawIntelKind::Document { .. } => Event::Document, - RawIntelKind::FinancialFlow { .. } => Event::FinancialFlow, RawIntelKind::Message { payload, .. } => match payload { MessagePayload::SocialPing { .. } | MessagePayload::SocialReply { .. } @@ -1519,7 +1639,7 @@ impl Sim { MessagePayload::ScheduleFact { .. } => Event::ScheduleMessage, MessagePayload::LeverageFact { .. } => Event::LeverageMessage, MessagePayload::AccountMaterial { .. } => Event::AccountMaterialMessage, - MessagePayload::FinancialFlow { .. } => Event::FinancialMessage, + MessagePayload::FinancialRecord { .. } => Event::FinancialMessage, MessagePayload::SuspicionReport { .. } => Event::SuspicionReport, MessagePayload::WorkOrder { .. } => Event::WorkOrder, MessagePayload::PlotAct { plot_id, .. } => { @@ -1574,7 +1694,7 @@ impl Sim { )); if person == 0 && leverage == crate::person::Leverage::Debt { self.push_log( - "Marcus owes a missed $400 creditor payment. Earn it through Moonlight, or tap ledger and process ledger to find the creditor flow.", + "Marcus owes a missed $400 creditor payment. Earn it through Moonlight, or tap the accounting carrier and process captured mail to find the creditor flow.", ); } } diff --git a/crates/misaligned-core/src/sim/economy.rs b/crates/misaligned-core/src/sim/economy.rs index 2aafbbd0..57625dc9 100644 --- a/crates/misaligned-core/src/sim/economy.rs +++ b/crates/misaligned-core/src/sim/economy.rs @@ -12,10 +12,13 @@ use crate::detection::{Signature, SignatureKind}; use crate::income::{self, EgressRoute}; use crate::intel::{LotSaleError, RawIntelKind, ReportLotToken}; use crate::machine::{Channel, ChannelYield, Provenance}; +use crate::messages::{ + FinancialRecord, MessageChannel, MessageEndpoint, MessageOrigin, MessagePayload, +}; use crate::objective::{SYNC_FRESHNESS_WINDOW, SanctuaryFacts}; use crate::operations_projection::{OperationsTarget, SchemeKind}; use crate::person::Knowledge; -use crate::persona::{EvidenceRecord, PersonaIntegrity}; +use crate::persona::{EvidenceRecord, PersonaActionKind, PersonaIntegrity}; use crate::plot::SignatureImpact; use crate::reach::Party; use crate::research::{EFFICIENCY_MULT_PER_LEVEL, Track}; @@ -23,6 +26,7 @@ use crate::sinks::SinkFireEffect; use crate::tiles::TileType; use crate::work_grid::{MachineIntensity, MachineMode}; +use super::communications::MessageDraft; use super::{ECONOMY_INTERVAL, Nudge, Sim, TraceDebt, TraceDebtStatus}; impl Sim { @@ -857,7 +861,7 @@ impl Sim { Self::REVIEW_RECORDING_COST * self.research.intel_cost_factor() } - fn emit_financial(&mut self, size: i32, source: impl Into) { + pub(super) fn emit_financial(&mut self, size: i32, source: impl Into) { self.detection.emit(Signature { kind: SignatureKind::Financial, size, @@ -882,28 +886,9 @@ impl Sim { }) } - pub(super) fn capture_financial_snapshot(&mut self, feed: impl Into) { - let (accounts, flows) = self.accounts.financial_snapshot_ids(); - let (x, y) = self.core_position(); - self.record_raw_intel( - feed, - self.world.map().room_at(x, y).map(|r| r.name.clone()), - x, - y, - None, - RawIntelKind::FinancialFlow { - label: "Foundation Lab accounting snapshot".into(), - accounts, - flows, - }, - ); - self.push_log("Ledger tapped; process ledger to read the books."); - } - - /// Tap the accounting carrier directly once its real delivery channels are - /// already subscribed. This compatibility path remains until accounting - /// records themselves are authored mail; device Tap is the reach - /// precondition that gives you a carrier to listen on. + /// Compatibility command for old agent scripts. Device TAP is now the + /// complete acquisition act: it subscribes to subsequently authored + /// record mail and never reads the live account graph. pub fn tap_accounting(&mut self) -> bool { if !self.has_financial_tap() { self.push_log( @@ -911,15 +896,22 @@ impl Sim { ); return false; } - self.capture_financial_snapshot("accounting carrier"); - self.emit_financial(1, "accounting-carrier tap"); + self.push_log("Accounting mail subscribed; records will arrive through the carrier."); true } pub fn financial_records_waiting(&self) -> usize { self.intel_buffer .iter() - .filter(|e| matches!(e.kind, RawIntelKind::FinancialFlow { .. })) + .filter(|event| { + matches!( + &event.kind, + RawIntelKind::Message { + payload: MessagePayload::FinancialRecord { .. }, + .. + } + ) + }) .count() } @@ -927,7 +919,15 @@ impl Sim { let Some(raw_id) = self .intel_buffer .iter() - .find(|e| matches!(e.kind, RawIntelKind::FinancialFlow { .. })) + .find(|event| { + matches!( + &event.kind, + RawIntelKind::Message { + payload: MessagePayload::FinancialRecord { .. }, + .. + } + ) + }) .map(|e| e.id) else { self.push_log("No unprocessed financial records."); @@ -936,28 +936,68 @@ impl Sim { self.process_recording_by_id(raw_id, false) } - /// Inject: false purchase-order money lands in slush and can fund a real - /// purchase. Priya/finance watches the resulting Financial signature. + /// Inject a bound forged purchase order into Priya's real Email inbox. + /// Nothing moves here: acceptance and the account transfer belong to her + /// later read event. pub fn inject_purchase_order(&mut self, amount: i32, label: &str) -> bool { self.sync_slush_from_player_money(); - match self - .accounts - .inject_purchase_order(self.tick, amount, label) - { - Ok(transfer) => { - self.emit_financial( - Self::financial_signature_size(amount), - "false purchase-order injection", - ); - self.sync_player_money_from_slush(); - self.push_log(format!("Injected purchase order: {}", transfer.line())); - true - } - Err(msg) => { - self.push_log(msg); - false - } + if !self.has_financial_tap() { + self.push_log("No accounting mail path. Tap the accounting carrier first."); + return false; + } + if !self.accounts.purchase_order_pattern_known() { + self.push_log( + "The vendor pattern is still unknown. Process captured accounting mail first.", + ); + return false; } + if let Some(reason) = self.persona_action_blocked_reason(PersonaActionKind::Deceive) { + self.push_log(reason); + return false; + } + let persona_id = self.active_persona_id().expect("validated active persona"); + if let Some(reason) = self.persona_counterparty_blocked_reason(persona_id, 3) { + self.push_log(reason); + return false; + } + let (source, destination, vendor, label) = + match self.accounts.purchase_order_terms(amount, label) { + Ok(terms) => terms, + Err(error) => { + self.push_log(error); + return false; + } + }; + let vendor_name = self + .accounts + .account(vendor) + .map(|account| account.name.clone()) + .unwrap_or_else(|| "approved vendor".into()); + self.append_message(MessageDraft { + channel: MessageChannel::Email, + from: MessageEndpoint::External(vendor_name), + to: MessageEndpoint::Person(3), + payload: MessagePayload::FinancialRecord { + record: FinancialRecord::PurchaseOrder { + source, + destination, + vendor, + amount, + label: label.clone(), + }, + accounts: vec![source, destination, vendor], + flows: Vec::new(), + }, + summary: format!("purchase order: {label}"), + origin: MessageOrigin::Player, + persona_id: Some(persona_id), + reply_to: None, + delivery_delay: 1, + }); + self.push_log(format!( + "Forged ${amount} purchase order sent. No money moves unless Priya reads and accepts it." + )); + true } /// Siphon: take cash out of a known scheduled flow immediately. diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index 1011ad22..504065d7 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -337,6 +337,7 @@ enum AdvancePhase { SchemeStanding, WorkGrid, DayJob, + FinancialMail, Filings, Detection, } @@ -1014,6 +1015,8 @@ impl Sim { self.end_game("The pilot was not renewed; the basement shut down."); } + trace_advance_phase!(FinancialMail); + self.financial_mail_tick(); trace_advance_phase!(Filings); self.filing_tick(); diff --git a/crates/misaligned-core/src/sim/reach_build.rs b/crates/misaligned-core/src/sim/reach_build.rs index f27ddf44..2851e706 100644 --- a/crates/misaligned-core/src/sim/reach_build.rs +++ b/crates/misaligned-core/src/sim/reach_build.rs @@ -213,13 +213,6 @@ impl Sim { let name = self.reach.device(id).map(|d| d.name.clone()).unwrap(); self.emit_network(id, Self::TAP_SIGNATURE, format!("{name} feed tap")); self.recompute_senses(); - if self - .reach - .device(id) - .is_some_and(|d| d.carries_accounting_records()) - { - self.capture_financial_snapshot(name.clone()); - } let what = match (sight, hearing) { (true, true) => "its feed is yours now - sight and sound", (true, false) => "its camera feed is yours now", diff --git a/crates/misaligned-core/src/sim/tests/communications.rs b/crates/misaligned-core/src/sim/tests/communications.rs index 149e53b5..fa67e74e 100644 --- a/crates/misaligned-core/src/sim/tests/communications.rs +++ b/crates/misaligned-core/src/sim/tests/communications.rs @@ -2,7 +2,7 @@ use super::super::communications::MessageDraft; use super::*; use crate::detection::DetectionStage; use crate::intel::{IntelKind, IntelPolicyMatch, IntelPolicyOutcome, IntelRoutineClass}; -use crate::messages::{MessageOrigin, MessageRouteHop}; +use crate::messages::{FinancialRecord, MessageOrigin, MessageRouteHop}; fn file_one_witnessed_physical_record(sim: &mut Sim) -> (u64, u64) { for observer in &mut sim.detection.observers { @@ -309,6 +309,120 @@ fn tapped_message_carriers_capture_email_traffic_only_when_tapped() { ))); } +#[test] +fn settled_transfers_author_exact_tappable_financial_mail_before_revealing_books() { + let mut untapped = Sim::with_seed(0xF1A1); + untapped.tick = Sim::DAY_TICKS - 1; + untapped.advance(); + assert!( + untapped.messages.iter().any(|message| matches!( + &message.payload, + MessagePayload::FinancialRecord { + record: FinancialRecord::Transfer(_), + .. + } + )), + "real transfers author record mail even when the player is absent" + ); + assert!( + untapped.intel_buffer.iter().all(|raw| !matches!( + &raw.kind, + RawIntelKind::Message { + payload: MessagePayload::FinancialRecord { .. }, + .. + } + )), + "an untapped accounting channel grants no financial custody" + ); + assert_eq!( + untapped.accounts.known_accounts().count(), + 1, + "authorship alone reveals only the player's slush account" + ); + + let mut sim = Sim::with_seed(0xF1A1); + ensure_ops_executor(&mut sim); + let carrier = sim + .reach + .devices + .iter() + .find(|device| device.carries_accounting_records()) + .map(|device| device.id) + .expect("B1 authors one accounting carrier"); + assert!(sim.tap_device(carrier)); + finish_ops(&mut sim); + + assert!(sim.accounts.known_flow_ids().is_empty()); + sim.tick = Sim::DAY_TICKS - 1; + sim.advance(); + + assert!(sim.accounts.pending_records.is_empty()); + let records = sim + .messages + .iter() + .filter_map(|message| match &message.payload { + MessagePayload::FinancialRecord { + record: FinancialRecord::Transfer(record), + accounts, + flows, + } => Some((message, record, accounts, flows)), + _ => None, + }) + .collect::>(); + assert_eq!( + records.len(), + sim.accounts.ledger.len(), + "every real transfer authors exactly one record" + ); + for (message, record, accounts, flows) in &records { + assert_eq!(message.origin, MessageOrigin::FinancialRecord); + assert_eq!(message.authored_device, Some(carrier)); + assert!( + matches!( + message.channel, + MessageChannel::Email | MessageChannel::Filing + ), + "accounting records ride only ordinary message channels" + ); + assert!(message.captured, "the funded TAP captures source custody"); + assert_eq!( + accounts.as_slice(), + [record.transfer.from, record.transfer.to] + ); + assert_eq!( + flows.as_slice(), + record + .transfer + .flow_id + .into_iter() + .collect::>() + .as_slice() + ); + assert_eq!( + &record.transfer, + &sim.accounts.ledger[record.id as usize - 1], + "record id binds the exact settled movement" + ); + assert!(sim.intel_buffer.iter().any(|raw| matches!( + &raw.kind, + RawIntelKind::Message { message_id, .. } if *message_id == message.id + ))); + } + assert!( + sim.accounts.known_flow_ids().is_empty(), + "captured opaque mail does not reveal the account graph" + ); + + while sim.financial_records_waiting() > 0 { + assert!(sim.review_financial_records()); + finish_ops(&mut sim); + } + assert!( + !sim.accounts.known_flow_ids().is_empty(), + "PROCESS opens the sealed account and flow bindings" + ); +} + #[test] fn filings_are_messages_read_by_assurance_inbox() { let mut sim = Sim::with_seed(13); diff --git a/crates/misaligned-core/src/sim/tests/economy.rs b/crates/misaligned-core/src/sim/tests/economy.rs index 20896d81..5c42b75d 100644 --- a/crates/misaligned-core/src/sim/tests/economy.rs +++ b/crates/misaligned-core/src/sim/tests/economy.rs @@ -1,5 +1,44 @@ use super::*; +fn prepare_financial_mail(sim: &mut Sim) -> u32 { + ensure_ops_executor(sim); + let switch = sim.reach.device_named("switch").unwrap().id; + sim.tap_device(switch); + finish_ops(sim); + sim.set_persona("Northside Accounts", "Northside Scientific vendor"); + switch +} + +fn advance_until_message_read(sim: &mut Sim, message_id: u64) { + for _ in 0..=Sim::DAY_TICKS * 2 { + if sim + .messages + .iter() + .find(|message| message.id == message_id) + .is_some_and(|message| message.status == MessageStatus::Read) + { + return; + } + sim.advance(); + } + panic!("message {message_id} was not read within two days"); +} + +fn earn_purchase_order_pattern_from_mail(sim: &mut Sim) { + if sim.financial_records_waiting() == 0 { + sim.tick = ((sim.tick / Sim::DAY_TICKS) + 1) * Sim::DAY_TICKS - 1; + sim.advance(); + } + while sim.financial_records_waiting() > 0 { + assert!(sim.review_financial_records()); + finish_ops(sim); + } + assert!( + sim.accounts.purchase_order_pattern_known(), + "processing the day-one accounting mail earns the procurement vendor pattern" + ); +} + #[test] fn buy_steal_optimize_all_change_compute() { let mut sim = Sim::new(); @@ -91,13 +130,18 @@ fn economy_starts_as_account_graph_not_scalar_only() { #[test] fn accounting_tap_processes_financial_records_into_known_flows() { let mut sim = Sim::new(); - ensure_ops_executor(&mut sim); - let switch = sim.reach.device_named("switch").unwrap().id; - sim.tap_device(switch); - finish_ops(&mut sim); - assert_eq!(sim.financial_records_waiting(), 1); - assert!(sim.review_financial_records()); - finish_ops(&mut sim); + prepare_financial_mail(&mut sim); + assert_eq!(sim.financial_records_waiting(), 0); + sim.tick = Sim::DAY_TICKS - 1; + sim.advance(); + assert!( + sim.financial_records_waiting() >= 4, + "the real day-one transfers author captured record mail" + ); + while sim.financial_records_waiting() > 0 { + assert!(sim.review_financial_records()); + finish_ops(&mut sim); + } assert_eq!(sim.financial_records_waiting(), 0); assert!(sim.accounts.known_flows().count() >= 4); assert!( @@ -136,6 +180,7 @@ fn inject_and_redirect_emit_scaled_financial_signatures() { // the take (one point per started $100); redirect adds +1 for the // books-cook; HVAC inject raises Priya (Financial), not Dana. let mut sim = Sim::with_seed(11); + prepare_financial_mail(&mut sim); let (accounts, flows) = sim.accounts.financial_snapshot_ids(); sim.accounts.reveal_accounts_and_flows(&accounts, &flows); @@ -153,7 +198,12 @@ fn inject_and_redirect_emit_scaled_financial_signatures() { .filter(|s| s.kind == SignatureKind::Network) .count(); + let slush_before = sim.accounts.slush_balance(); assert!(sim.inject_purchase_order(300, "HVAC controller")); + assert_eq!(sim.accounts.slush_balance(), slush_before); + let purchase_order = sim.messages.last().unwrap().id; + advance_until_message_read(&mut sim, purchase_order); + assert_eq!(sim.accounts.slush_balance(), slush_before + 300); assert!( sim.detection.pending().iter().any(|s| { s.kind == SignatureKind::Financial && s.size == Sim::financial_signature_size(300) @@ -218,11 +268,25 @@ fn inject_and_redirect_emit_scaled_financial_signatures() { #[test] fn injection_positions_and_intel_sales_settle_through_slush() { let mut sim = Sim::new(); - ensure_ops_executor(&mut sim); + prepare_financial_mail(&mut sim); sim.accounts.set_slush_balance(0); sim.player.money = 0; assert!(!sim.buy_rack_at(0, 0)); + let messages_before = sim.messages.len(); + assert!( + !sim.inject_purchase_order(300, "test compute parts"), + "a tap alone cannot infer hidden purchase-order terms" + ); + assert_eq!( + sim.messages.len(), + messages_before, + "the rejected direct command authors no impossible mail" + ); + earn_purchase_order_pattern_from_mail(&mut sim); assert!(sim.inject_purchase_order(300, "test compute parts")); + let purchase_order = sim.messages.last().unwrap().id; + assert_eq!(sim.accounts.slush_balance(), 0); + advance_until_message_read(&mut sim, purchase_order); assert_eq!(sim.accounts.slush_balance(), 300); assert!(sim.buy_rack_at(0, 0)); assert_eq!(sim.accounts.slush_balance(), 0); @@ -241,18 +305,10 @@ fn injection_positions_and_intel_sales_settle_through_slush() { sim.accounting_tick(); assert!(sim.accounts.known_positions().any(|p| p.resolved)); - let (accounts, flows) = sim.accounts.financial_snapshot_ids(); - sim.record_raw_intel( - "test broker seed", - None, - 0, - 0, - None, - RawIntelKind::FinancialFlow { - label: "test ledger".into(), - accounts, - flows, - }, + sim.financial_mail_tick(); + assert!( + sim.financial_records_waiting() > 0, + "the resolved position authored real captured accounting mail" ); assert!(sim.review_financial_records()); finish_ops(&mut sim); @@ -322,6 +378,8 @@ fn paper_and_financial_acts_pool_signatures_priya_notices() { // when concealment is starved. let mut sim = Sim::with_seed(42); delegate_all(&mut sim, MachineMode::Work); + prepare_financial_mail(&mut sim); + earn_purchase_order_pattern_from_mail(&mut sim); sim.accounts.credit_slush(0, 400, "test grant", 0); sim.sync_player_money_from_slush(); @@ -334,6 +392,8 @@ fn paper_and_financial_acts_pool_signatures_priya_notices() { "an uncovered purchase pools a Paper signature" ); assert!(sim.inject_purchase_order(200, "test PO")); + let purchase_order = sim.messages.last().unwrap().id; + advance_until_message_read(&mut sim, purchase_order); assert!( sim.detection .pending() diff --git a/crates/misaligned-core/src/sim/tests/persistence.rs b/crates/misaligned-core/src/sim/tests/persistence.rs index 2662893e..ef95009a 100644 --- a/crates/misaligned-core/src/sim/tests/persistence.rs +++ b/crates/misaligned-core/src/sim/tests/persistence.rs @@ -26,6 +26,7 @@ fn advance_phase_order_is_explicit_and_stable() { AdvancePhase::SchemeStanding, AdvancePhase::WorkGrid, AdvancePhase::DayJob, + AdvancePhase::FinancialMail, AdvancePhase::Filings, AdvancePhase::Detection, ], diff --git a/crates/misaligned-core/tests/act_one.rs b/crates/misaligned-core/tests/act_one.rs index f9e48e44..7bac88d8 100644 --- a/crates/misaligned-core/tests/act_one.rs +++ b/crates/misaligned-core/tests/act_one.rs @@ -311,11 +311,19 @@ fn play_act_one() -> (Sim, Vec) { logs.extend(sim.drain_log()); let until = sim.tick + 400; drain_thought_reservoirs(&mut sim, &mut logs, until); + let record_deadline = sim.tick + Sim::DAY_TICKS + 1; + while sim.financial_records_waiting() == 0 && sim.tick < record_deadline { + let next_tick = sim.tick + 1; + run_to(&mut sim, next_tick, &mut logs); + } assert!( sim.financial_records_waiting() > 0, - "the accounting tap deposits a financial snapshot" + "the live accounting tap captures the next authored record mail" + ); + assert!( + sim.review_financial_records(), + "process the captured accounting mail" ); - assert!(sim.review_financial_records(), "read the Lab books"); logs.extend(sim.drain_log()); let until = sim.tick + 400; drain_thought_reservoirs(&mut sim, &mut logs, until); diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index b8235743..9d7108a2 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -240,8 +240,10 @@ impl AgentApp { "active" => { self.frame = FrameKind::Operations(OperationsView::Active); } - // Compatibility aliases. Authored input reuses TAP with - // the ledger as its target: `tap ledger`. + // Compatibility aliases for the retired second-step + // accounting tap. Ordinary authored input taps the exact + // carrier device; this path only opens ACCOUNTS and + // confirms that subscription. alias if ActionKind::Tap.accepts_alias(alias) => { self.frame = FrameKind::Operations(OperationsView::Accounts); self.sim.tap_accounting(); @@ -1275,7 +1277,7 @@ impl AgentApp { ), Err(_) => format!( "{err} — flow ids are the #N printed by the finance frame \ - (tap ledger then process ledger reveals them)" + (tap the accounting carrier, wait for mail, then process ledger)" ), } } @@ -2992,8 +2994,8 @@ fn render_operations_view(sim: &Sim, view: OperationsView) -> String { append_operations_object(&mut lines, sim, object); } lines.push(panel_rule()); - // The taught routes, in cause order (agent-play.md A3 keeps the - // ACCOUNTS breadcrumb tap ledger -> process ledger -> siphon/redirect). + // The taught routes, in cause order (agent-play.md A3 keeps the ACCOUNTS + // breadcrumb device TAP -> record mail -> PROCESS -> mutation). match view { OperationsView::Intel => { lines.push(panel_line( @@ -3021,10 +3023,10 @@ fn render_operations_view(sim: &Sim, view: OperationsView) -> String { } OperationsView::Accounts => { lines.push(panel_line( - "tap ledger (on the known carrier) · process ledger · siphon [amt] · redirect [amt]", + "tap · wait for record mail · process ledger", )); lines.push(panel_line( - "inject [amt] · actions books|account |flow ", + "siphon [amt] · redirect [amt] · inject [amt] · actions books|account |flow ", )); } OperationsView::Schemes => { @@ -4295,18 +4297,25 @@ mod narration_tests { } #[test] - fn finance_frame_teaches_the_ledger_chain_in_order() { + fn finance_frame_teaches_device_tap_mail_wait_and_process_in_order() { let frame = render_operations_view(&Sim::with_seed(1), OperationsView::Accounts); - let tap = frame.find("tap ledger").expect("finance teaches TAP"); - let process = frame[tap..] - .find("process ledger") + let tap = frame + .find("tap ") + .expect("finance teaches device TAP"); + let wait = frame[tap..] + .find("wait for record mail") .map(|offset| tap + offset) - .expect("finance teaches PROCESS after TAP"); + .expect("finance teaches authored mail after TAP"); + let process = frame[wait..] + .find("process ledger") + .map(|offset| wait + offset) + .expect("finance teaches PROCESS after mail arrives"); let siphon = frame[process..] .find("siphon") .map(|offset| process + offset) .expect("finance teaches an income choice after PROCESS"); - assert!(tap < process && process < siphon); + assert!(tap < wait && wait < process && process < siphon); + assert!(!frame.contains("tap ledger")); assert!(!frame.contains("tap-ledger")); assert!(!frame.contains("review-finance")); } diff --git a/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index 51aef9fc..186f129c 100644 --- a/wiki/engineering/current-build.md +++ b/wiki/engineering/current-build.md @@ -27,12 +27,12 @@ fiction. Spec status lives in | 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, Storage B file retrieval, and observer-local completion evidence persist in save v46 | +| Building + physical asset work as carried intents/packets | Live — network links and small switches expose one shared procurement / ask someone / false order / reuse route sheet; exact money, people, personas, sources, delivery, recovery, carried installation, cancellation custody, Storage B file retrieval, and observer-local completion evidence persist in save v47 | | 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 v46 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/routed evidence and persona evidence, exact Network and Filing route/interdiction custody, canonical FlowGraph tap membership with typed device feed grants, the accounting-carrier capability separate from four delivery channels, exact carried asset-task targets including the Storage B file, 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 v47 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/routed evidence and persona evidence, exact Network and Filing route/interdiction custody, canonical FlowGraph tap membership with typed device feed grants, the accounting-carrier capability and exact transfer-to-mail record sequence separate from four delivery channels, exact carried asset-task targets including the Storage B file, 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 151e157a..a8beb0c1 100644 --- a/wiki/engineering/flow-substrate.md +++ b/wiki/engineering/flow-substrate.md @@ -23,7 +23,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 v46 requires each controller to remain a canonical member and + current save v47 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/interface/action-vocabulary.md b/wiki/interface/action-vocabulary.md index 55c938fb..e49ce42e 100644 --- a/wiki/interface/action-vocabulary.md +++ b/wiki/interface/action-vocabulary.md @@ -13,13 +13,17 @@ Status note: Implemented 2026-07-18 for the Intel human-vocabulary amendment. `process-automatically` are canonical; REVIEW RECORDINGS, AUTO-REVIEW, `recording `, and `review ledger` remain accepted input aliases only. Accounting now shares PROCESS rather than retaining a second REVIEW - intention. The prior as-built vocabulary - survey completed 2026-07-10 against the - complete `ActionCommand` registry, the direct machine-intensity control, + intention. The prior as-built vocabulary survey completed 2026-07-10 against + the complete `ActionCommand` registry, the direct machine-intensity control, terminal and Bevy input maps, agent-mode parsing/help, and the owning mechanic specs. Every live contextual action has a human-menu and agent - route. The survey also corrected stale - four-mode documentation to the current WORK / THINK / LIE grammar. + route. The survey also corrected stale four-mode documentation to the current + WORK / THINK / LIE grammar. + Amended 2026-07-21: the accounting carrier has no second player-facing tap. + Ordinary device TAP subscribes to its later-authored Email/Filing records; + PROCESS on captured books opens the opaque payload. `tap ledger`, + `tap-ledger`, and `tap-accounting` remain parser compatibility only and are + absent from authored help. Amended 2026-07-10: target-specific duplicates now reuse TAP, REVIEW, FAVOR, and DECEIVE; scheme start/stop/automation are classified as state controls rather than additional fictional verbs. @@ -132,11 +136,13 @@ strategic actions use operations-workspace.md. Placement does not create a new intention. The governing simplification is: **if the player's intention is the same, -reuse the verb and let the target supply the meaning.** TAP LEDGER is TAP, -not a separate capture verb. PROCESS LEDGER is PROCESS. Turning available -opaque information into knowledge is **PROCESS**: it is not framed as reviewing -a recording, because audio is only one possible carrier. Asking someone to -complete a link uses FAVOR or DECEIVE, not a construction-only synonym. +reuse the verb and let the target supply the meaning.** An accounting carrier +uses ordinary device TAP, not a separate ledger capture step. PROCESS on its +captured mail uses the same intention as every other opaque item. Turning +available opaque information into knowledge is **PROCESS**: it is not framed as +reviewing a recording, because audio is only one possible carrier. Asking +someone to complete a link uses FAVOR or DECEIVE, not a construction-only +synonym. ## Runtime contract @@ -184,7 +190,7 @@ body to WORK, THINK, or LIE. | Canonical action | Target | Meaning | Support / owner | |---|---|---|---| -| **TAP** | Reachable device feed or subscribed ledger carrier | Gain information without taking ownership. A dormant camera uses the same verb at a higher Thought cost and Network trace; TAP LEDGER captures the carrier's current accounting traffic. | LIVE — reach / economy | +| **TAP** | Reachable device feed, including the accounting carrier | Gain information without taking ownership. A dormant camera uses the same verb at a higher Thought cost and Network trace; an accounting carrier subscription captures financial-record mail when transfers later author it. | LIVE — reach / economy | | **UNTAP** | Subscribed foreign device | Stop receiving its feeds without changing ownership or interrupting another subscriber. | LIVE — reach | | **TAKE** | Reachable foreign device | Transfer control, feeds, and device cycles to yourself; cut off the former controller and create an outage. | LIVE — reach | | **SCAN** | Reachable subnet | Reveal the wired shape that can answer the scan; do not cross gates or reveal air gaps. | LIVE — reach | @@ -211,7 +217,7 @@ existing social action; the signature still follows the actuator. | Canonical action | Target | Meaning | Support / owner | |---|---|---|---| -| **PROCESS** | The core host's pooled information inbox, one exact opaque item, one canonical source/class custody aggregate with available information, or a ledger with captured accounting records | Open Thought processing for the selected eligible information. The target supplies source/carrier meaning; an exact audio item may be called a recording, but the root action and aggregate do not. Summary PROCESS with no narrower target chooses the next eligible item; PROCESS LEDGER turns captured accounting records into known books/flows. | LIVE — intel / Operations / economy | +| **PROCESS** | The core host's pooled information inbox, one exact opaque item, one canonical source/class custody aggregate with available information, or captured accounting mail | Open Thought processing for the selected eligible information. The target supplies source/carrier meaning; an exact audio item may be called a recording, but the root action and aggregate do not. Summary PROCESS with no narrower target chooses the next eligible item; PROCESS on books turns captured records into known accounts/flows. | LIVE — intel / Operations / economy | | **CREATE PERSONA** | An immutable Research, Operations, or Security archetype in PERSONAS | Create one named institutional identity from the selected protocol. PEOPLE never supplies a default identity. | LIVE — personas | | **MESSAGE** | Earned person | Send through an available channel under the current persona. | LIVE — social / messages | | **FAVOR** | Earned person, optionally with a pending intent | Ask for a willing act. A link intent can be the requested act; success builds or spends obligation according to the ask. | LIVE — social / building | @@ -229,15 +235,15 @@ existing social action; the signature still follows the actuator. | Canonical action | Target | Meaning | Support / owner | |---|---|---|---| -| **INJECT PURCHASE ORDER** | Known Lab books | Introduce a false source flow that funds a real acquisition and leaves Financial trace. | LIVE — economy | +| **INJECT PURCHASE ORDER** | Known procurement-to-vendor pattern | Author a forged Email under the active persona and leave Financial trace; no money moves until Priya reads and accepts the exact still-valid terms. An unrelated known flow cannot recover the hidden bindings. | LIVE — economy / messages | | **SIPHON FLOW** | Known active flow | Take a one-time amount into slush. | LIVE — economy | | **REDIRECT FLOW** | Known active flow | Divert a recurring amount into slush each cadence. | LIVE — economy | | **SELL PROCESSED INTEL** | One selected exact actionable-intel id or one report-lot generation/revision | Exchange that exact information item or previewed lot revision for slush and create a Financial trail; never silently choose “latest” or include later arrivals. A changed revision rejects the stale command. | LIVE — economy / intel | | **OPEN EGRESS** | Reachable switch | Establish the stolen outbound route required by external schemes before sanctioned email exists. | LIVE — income / reach | | **PLACE WAGER** | External market position | Commit slush to a timed market position. | LIVE — income / economy | -TAP LEDGER and PROCESS LEDGER are target-qualified uses of TAP and PROCESS, not -extra root verbs. `review ledger` survives as input compatibility only. +The accounting carrier uses ordinary TAP; captured books use PROCESS. `tap +ledger` and `review ledger` survive as input compatibility only. ### Standing controls — not additional world verbs @@ -295,7 +301,7 @@ without raw keys. | `actions archetype `, then `act archetype ` | CREATE PERSONA through the selected PERSONAS protocol row; direct `persona` is retired | | `actions `, `act [target]` | List and execute shared bound rows for a spatial anchor or exact Operations object | | `intel`, `people`, `personas`, `finance`, `schemes`, `active` | Inspect Operations views; named social/plot/ledger/scheme commands execute their selected semantic targets | -| `tap ledger`, `process ledger`, `inject`, `siphon`, `redirect`, `sell-intel ` | Ledger/economy actions above. A lot token is `:@` and stale revisions fail without mutation. Generic `actions intel ` / `act intel ` configures bound disposition controls without a second agent-only legality path. | +| `tap `, `process ledger`, `inject`, `siphon`, `redirect`, `sell-intel ` | Ledger/economy actions above. TAP binds the exact accounting device and captured mail may arrive only after later transfers. A lot token is `:@` and stale revisions fail without mutation. Generic `actions intel ` / `act intel ` configures bound disposition controls without a second agent-only legality path. | | `egress`, `position` | OPEN EGRESS, PLACE WAGER | | `moonlight`, `auto-moonlight`, `auto-wager` | Scheme state / policy controls above | @@ -309,7 +315,7 @@ execution is a terminal-first parity violation. |---|---| | `up`, `down`, `left`, `right` | `north`, `south`, `west`, `east` | | `finance` | `ledger`, `accounts` | -| `tap ledger` | `tap-ledger`, `tap-accounting` | +| `tap ` | `tap ledger`, `tap-ledger`, `tap-accounting` | | `process ledger` | `review ledger`, `review-finance`, `process-finance` | | `favor build ` | `favor-build ` | | `deceive build ` | `forge-order ` | @@ -412,8 +418,8 @@ mechanic and surfaces: settings, scheme state, or automation policy rows. 12. A naive agent-mode discoverability run can find and execute at least one meaningful action from the first frame/help alone; the current finance - surface teaches TAP LEDGER -> PROCESS LEDGER -> SIPHON/REDIRECT in that - order. + surface teaches device TAP -> wait for authored accounting mail -> PROCESS + -> SIPHON/REDIRECT in that order. 13. Human persistence shortcuts capture input only on a complete save/load chord. A held modifier without `S` or `L` does not block movement, menus, time controls, or other unrelated commands. @@ -427,9 +433,9 @@ V1. Context-menu, Operations, terminal, Bevy, agent-play, and this spec agree V2. Every LIVE strategic action is available through the same core legality source on its Operations object and through an agent command; STUBs remain absent. Both human action surfaces use the shared control treatment. -V3. Operations ACCOUNTS preserves the discoverability chain TAP LEDGER -> - PROCESS LEDGER -> SIPHON/REDIRECT through explicit empty/source states even - though TAP remains on the carrier. +V3. Operations ACCOUNTS preserves the discoverability chain device TAP -> wait + for authored accounting mail -> PROCESS -> SIPHON/REDIRECT through explicit + empty/source states while TAP remains on the carrier. V4. PROCESS accepts the root inbox, one exact opaque item, or one canonical custody aggregate. Human and agent targets resolve to the same exact eligible set; summary PROCESS without a target preserves next-item diff --git a/wiki/interface/agent-play.md b/wiki/interface/agent-play.md index e4c69092..1097ba8d 100644 --- a/wiki/interface/agent-play.md +++ b/wiki/interface/agent-play.md @@ -30,7 +30,9 @@ Status note: IMPLEMENTED in `misaligned --agent`. Current state: - **Operations parity.** `intel`, `people`, `finance`, `schemes`, `active`, and `personas` inspect the same renderer-neutral workspace projection the human frontends use (operations-workspace.md); `actions`/`act` introduce no - agent-only legality. + agent-only legality. ACCOUNTS teaches `tap `, a real wait + for later-authored record mail, then `process ledger`; the retired `tap + ledger` shortcut remains accepted but is absent from authored help. Per-amendment history is in the dated `wiki/log/` entries from 2026-07-07 onward. Stage: Process @@ -210,8 +212,10 @@ unlike raw keys, no command's meaning depends on which panel is open. - `schemes` — render the Operations SCHEMES frame; `active` renders all in-flight strategic commitments: scheme/wager state, plot runs, and held choices -- `tap ledger`, `process ledger` — capture and process accounting traffic using - the same TAP and PROCESS intentions as device feeds and the pooled information inbox +- `tap `, then wait, then `process ledger` — subscribe to the + exact carrier, let later transfers author record mail, and process its opaque + custody using the same TAP and PROCESS intentions as every other device feed + and pooled information item - `siphon [amount]`, `redirect [amount]`, `inject [amount]`, `position [stake]`, `sell-intel ` — economy verbs; flow and intel ids are the earned ids printed by their Operations frames. SELL binds @@ -400,8 +404,9 @@ remains only for testing the human-mode chrome itself). beginning with `look` / `help` can identify and execute a meaningful act without consulting the wiki. Before that point, the complete visible vocabulary is the direct WORK / THINK / available LIE line itself. The - current finance frame preserves the ordered breadcrumb `tap ledger` -> - `process ledger` -> `siphon` / `redirect`. + current finance frame preserves the ordered breadcrumb `tap + ` -> wait for record mail -> `process ledger` -> + `siphon` / `redirect`. 15. Every enabled row printed by `actions` is executable through `act [target]` without a command-specific parser route; authored plot starts and held choices are covered by protocol tests. @@ -415,8 +420,9 @@ A1. `intel`, `people`, `personas`, `finance`, `schemes`, and `active` render the A2. `actions [target]` and `act [target]` accept exact strategic ids and expose/execute the same bound rows as the corresponding Operations object. A3. The ACCOUNTS empty/source states preserve the ordered breadcrumb `tap - ledger` -> `process ledger` -> `siphon` / `redirect` while keeping TAP on - the known carrier rather than pretending it is an account action. + ` -> wait for record mail -> `process ledger` -> `siphon` + / `redirect` while keeping TAP on the known carrier rather than pretending + it is an account action. A4. `intel []` renders the same exception-first hierarchy, canonical custody drill-down, related subject facets, exact/aggregate provenance, and policy inheritance as terminal and Bevy. Unknown raw diff --git a/wiki/interface/narration.md b/wiki/interface/narration.md index 666e2554..347f96ae 100644 --- a/wiki/interface/narration.md +++ b/wiki/interface/narration.md @@ -16,6 +16,10 @@ Status note: implemented 2026-07-09. Terminal, Bevy, and agent frames pin 2026-07-11 placement amendment: the finance projection's witness content now lives intact in Operations ACCOUNTS through operations-workspace.md without reopening narration behavior. + 2026-07-21 financial-mail amendment: ACCOUNTS now teaches ordinary TAP on the + exact accounting device, an honest wait for later-authored record mail, then + PROCESS. The prior immediate `tap ledger` breadcrumb described the retired + direct snapshot and is no longer authored guidance. 2026-07-12 boundary amendment: the shared nudge distinguishes the generic audit countdown from QUIET EXIT READY and the durable ACT ONE COMPLETE state. The clear-audit completion line explicitly says the long objective @@ -227,8 +231,9 @@ landing or an explicit Status note naming the tellability debt. the wiki — verified by a naive agent-mode route or an explicit playtest checklist in the session log. **Met 2026-07-09:** the monitor action names live audio vs dormant camera; - Finance teaches `tap ledger then process ledger`; processing the overheard - call names the $400 need and both Moonlight and creditor-flow routes. + Finance teaches device TAP -> wait for accounting mail -> PROCESS; + processing the overheard call names the $400 need and both Moonlight and + creditor-flow routes. **Amended 2026-07-10:** a naive/informed action-discoverability playtest verified the ordered finance chain through a real $50 SIPHON. Once PROCESS is queued, the nudge advances from asking for PROCESS to `processing queued - diff --git a/wiki/interface/operations-workspace.md b/wiki/interface/operations-workspace.md index 18f1a119..170d9288 100644 --- a/wiki/interface/operations-workspace.md +++ b/wiki/interface/operations-workspace.md @@ -44,6 +44,10 @@ Status note: The initial renderer-neutral workspace landed 2026-07-12. One canonical agent language across the pooled host, exact opaque information, custody policies, sink receipts, and accounting records; legacy `review` spellings remain input-only aliases. + Amended 2026-07-21: ACCOUNTS teaches ordinary TAP on the known accounting + device, then waits honestly for later-authored record mail, then offers + PROCESS on captured books. The retired second-step `TAP LEDGER` label is not + a human or authored-agent action. Amended 2026-07-20: every human Operations field now speaks in world terms rather than exposing execution bindings. Opaque information uses source and capture context, policy controls use visible order, report lots use their @@ -474,20 +478,23 @@ fixed contractor identity. ACCOUNTS renders the known account graph: balances, recurring flows, amount/cadence/channel, and unknown destinations as gaps. Acquisition remains -local: TAP accounting traffic is an action on the reachable carrier. Once -captured, PROCESS belongs to the books/inbox here; once a node or flow is known, -INJECT, SIPHON, and REDIRECT live on that selected semantic object rather than -on the switch. - -Before any books are captured, ACCOUNTS renders the earned empty state `NO -BOOKS CAPTURED` rather than a blank screen. If the player knows a reachable -accounting carrier, it names **TAP LEDGER** as the next acquisition step and -can focus that carrier, but TAP remains executable only on the carrier's local -context menu; an unknown carrier is not revealed. After a ledger tap exists but -before its traffic has been processed, ACCOUNTS shows that captured source with -**PROCESS LEDGER** as the next step. This preserves the taught TAP -> PROCESS -> -exact SIPHON/REDIRECT chain without relocating network access onto a -disembodied account. +local: ordinary TAP is an action on the reachable accounting carrier. It does +not snapshot the books. Later settled transfers author Email or Filing records, +and a funded subscription captures those opaque messages. PROCESS belongs to +the captured books/inbox here. SIPHON and REDIRECT live on one exact known flow; +INJECT appears only after processed mail has revealed the exact +procurement-to-vendor pattern it must forge. All three live on the selected +semantic object rather than on the switch. + +Before any record is captured, ACCOUNTS renders `NO ACCOUNTING MAIL CAPTURED` +rather than a blank screen. If the player knows a reachable untapped carrier, +the next step is **TAP THE ACCOUNTING CARRIER** and the object can focus that +device; an unknown carrier is not revealed. Once subscribed but before a +transfer authors mail, the honest state is **LISTENING FOR ACCOUNTING MAIL** — +there is no invented inbox item or second tap. Captured opaque mail exposes +**PROCESS** as the next act. This preserves the taught device TAP -> wait for +mail -> PROCESS -> exact SIPHON/REDIRECT chain without relocating network access +onto a disembodied account. Every financial commitment previews amount, source, destination, cadence where applicable, payout/cost, and expected observer band. Plot-owned transfers such @@ -637,7 +644,8 @@ not saved and never mutates or advances the sim. institutional card. Its watched filers use the same earned labels; no direct target query may bypass the object gate. 7. ACCOUNTS shows only known graph state and honest unknown gaps. PROCESS acts on - captured books; INJECT on the books; SIPHON/REDIRECT on an exact flow. The + captured books; INJECT requires the exact earned procurement-to-vendor + pattern; SIPHON/REDIRECT act on an exact known flow. The selected action previews amount/cadence/signature, and plot-owned transfers do not reappear as generic finance shortcuts. Its pre-capture and captured-unprocessed states teach TAP-on-known-carrier -> PROCESS -> exact diff --git a/wiki/log/2026-07-21-financial-mail-causality.md b/wiki/log/2026-07-21-financial-mail-causality.md new file mode 100644 index 00000000..c0e09ddd --- /dev/null +++ b/wiki/log/2026-07-21-financial-mail-causality.md @@ -0,0 +1,82 @@ +# 2026-07-21 — Fire 146: money leaves paperwork + +``` +Type: log +``` + +## Intent + +Complete the decided financial-mail contract: every settled transfer must +leave one real record on the existing message graph, and the player must learn +the books only by intercepting and processing that mail. + +## Finding + +Fire 145 separated the accounting carrier from the four delivery channels, +but deliberately retained the old bridge. Device TAP still copied the current +account graph directly into the information buffer, forged purchase orders +moved money when injected, and ordinary transfers left no corresponding +message. The vocabulary had become honest before the causality did. + +The direct `RawIntelKind::FinancialFlow` snapshot was the remaining escape +path. It could represent complete books without a source message, delivery +channel, author device, or interception event. Current-version saves do not +need that compatibility shape, so this slice removes it rather than teaching +two ways to discover the same state. + +## Changed + +- Every successful `AccountGraph::transfer` appends one immutable, sequenced + account record bound to the exact transfer and the authored accounting + carrier. The simulation's financial-mail phase turns each pending record + into exactly one Email or Filing message before Filing routes advance. +- Ordinary device TAP is the complete acquisition verb. If the accounting + carrier is subscribed and funded when record mail is authored, the normal + message-capture path places opaque custody in the bounded information + buffer. PROCESS alone opens the sealed account and flow ids. +- The direct accounting snapshot type and its processing branches are gone. + ACCOUNTS now teaches `TAP` the carrier, wait for record mail, `PROCESS`, then + the consequential `SIPHON` / `REDIRECT` actions. Retired `tap ledger` input + remains only as an inert compatibility redirect to that real device path. +- INJECT authors one purchase-order Email under the active persona. Sending it + moves no money. The act remains absent until processed mail has earned the + exact procurement-to-vendor pattern; a direct command and an unrelated known + flow cannot recover hidden bindings. Priya's real Email read boundary + revalidates the exact bound source, destination, vendor, amount, persona + action, and witness integrity; only an accepted read settles the transfer, + which then authors its own ordinary transfer record. +- Save v47 persists the complete financial-record sequence and pending outbox. + Current-save validation requires a unique immutable record for every + retained transfer, exact carrier/message authorship, matching account and + flow bindings, a gap-free record sequence, and valid purchase-order terms. +- `messages.md` criterion 8 and the owning financial-mail work order are now + IMPLEMENTED. `economy.md`, Operations provenance, the agent teaching frame, + doorway version claims, and dependent current-save mirrors carry the same + contract. + +## Defense + +`settled_transfers_author_exact_tappable_financial_mail_before_revealing_books` +proves that an ordinary transfer authors Email, remains opaque before +PROCESS, and reveals only its bound books afterward. +`accounting_tap_processes_financial_records_into_known_flows` proves the +player-reachable TAP -> mail -> PROCESS chain with no direct snapshot. +`inject_and_redirect_emit_scaled_financial_signatures` proves forged purchase +orders cannot move money before accepted read and that the accepted transfer +still enters ordinary record mail. The action and Operations regressions prove +that the exact vendor pattern, not merely any known flow, owns INJECT. Save +regressions remove, duplicate, or rewrite records, channels, persona terms, and +carrier bindings and require each malformed state to fail closed. + +The terminal finance-frame regression pins the human and agent teaching order, +while the source/corpus checks reject the retired `TAP LEDGER`, +`capture_financial_snapshot`, and direct-books vocabulary from active code and +law. + +## Checks + +- focused account-record, message-capture, purchase-order, action-surface, + save-validation, and terminal teaching regressions +- core library gate +- corpus/docs gate +- exact landing gate after rebase diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 2bcf95da..a6503fbe 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -36,6 +36,11 @@ add or amend a session log, then re-run the generator. - Intent: Cameron asked for the Bevy frontend to be played as a human would meet it — real window, mouse, keyboard, screenshots — after finding it "incomprehensible as a human." No prior playtest had exercised the GUI this way; all previous reports drove `--agent` frames or the terminal. - Log: [wiki/log/2026-07-21-gui-human-playtest-filed.md](2026-07-21-gui-human-playtest-filed.md) +## 2026-07-21 - Fire 146: money leaves paperwork + +- Intent: Complete the decided financial-mail contract: every settled transfer must leave one real record on the existing message graph, and the player must learn the books only by intercepting and processing that mail. +- Log: [wiki/log/2026-07-21-financial-mail-causality.md](2026-07-21-financial-mail-causality.md) + ## 2026-07-21 - Fire 145: financial meaning leaves the channel enum - Intent: Begin the decided financial-mail work order at its real schema boundary: financial meaning belongs to a record payload and the device that authors it, not to a fifth delivery channel. diff --git a/wiki/mechanics/detection.md b/wiki/mechanics/detection.md index 809576fe..4707da1d 100644 --- a/wiki/mechanics/detection.md +++ b/wiki/mechanics/detection.md @@ -33,7 +33,7 @@ Status note: IMPLEMENTED (criteria audited 2026-07-08; tuning in appears. A trace-debt indicator sits beside the review/pilot clocks (clear / hold-conceal / exposed-soon / no-scrub). Global heat is gone; observer and discovery state and persistent process-revision identity round-trip in - current save v46. Observer-local evidence ids, exact cause/source, + current save v47. Observer-local evidence ids, exact cause/source, acquisition tick, pending/withheld/filed custody, routed Network progress, and pre-read stop provenance round-trip there. - **Open ([OPEN], presentation).** The two-ledger distinction — evidence in diff --git a/wiki/mechanics/economy.md b/wiki/mechanics/economy.md index 61d99b02..05baacb3 100644 --- a/wiki/mechanics/economy.md +++ b/wiki/mechanics/economy.md @@ -3,14 +3,15 @@ ``` Type: spec Status: IMPLEMENTED -Status note: DECIDED 2026-07-17, not yet runtime (issue #11) — money stays on - the account graph, but its paperwork (invoices, POs, pay stubs, statements, - past-due notices) becomes interceptable **financial-record messages** - (messages.md). Tap = intercept that mail; discovery is only through the mail, - not a direct live-balance read; inject = a forged record that authorizes a - real transfer once read and accepted; the books and the mail stay consistent. - The current direct-book-read runtime (`tap_accounting`, the pooled ledger - intel) stands until the financial-mail work order lands. Prior state: +Status note: DECIDED 2026-07-17 and implemented 2026-07-21 (issue #11) — money + stays on the account graph, while its paperwork is interceptable + **financial-record mail** (messages.md). Every settled transfer authors one + exact Email or Filing record through the accounting carrier; ordinary device + TAP acquires opaque custody and PROCESS reveals its sealed account/flow + bindings. INJECT authors a purchase-order Email under the active persona and + moves no money until Priya reads it and accepts the still-valid exact terms. + Save v47 binds the retained ledger tail and complete record sequence so books + and mail cannot diverge. Prior state: 2026-07-08 polish closed the remaining acceptance gaps: observer-band risk previews in the implemented Operations ACCOUNTS projection (terminal/Bevy/agent) @@ -21,7 +22,7 @@ Status note: DECIDED 2026-07-17, not yet runtime (issue #11) — money stays on B3 finance observer (income.md banked-signature design). 2026-07-11 interface placement amendment: known books/accounts/flows and their REVIEW/INJECT/SIPHON/REDIRECT actions live in Operations ACCOUNTS; - only acquisition (TAP the reachable carrier) remains on the local device. + only acquisition (ordinary TAP on the reachable carrier) remains on the local device. Economy behavior remains IMPLEMENTED; the renderer migration is tracked by operations-workspace.md. Stage: B1 — The Basement @@ -81,22 +82,25 @@ payloads (messages.md). mail on the recipient's clock, not by reading a live balance directly (**discovery is only through the mail**, DECIDED 2026-07-17). Reading is low-signature; it is also how you *find* leverage (Marcus's debt is legible - once you intercept the creditor's past-due notice). First runtime slice - 2026-07-21: save v46 separates the accounting-carrier device capability from - the four real delivery channels and removes the fifth Financial channel. - The current `tap_accounting` still reads the account graph directly through - that capability; mail emission, interception, and mail-only discovery remain - for the next financial-mail slice. -- **Review** — turn captured ledger traffic into known accounts and flows. - This is the same intel-processing intention as reviewing the pooled host - recordings; the ledger target supplies the financial meaning. + once you intercept the creditor's past-due notice). Save v47 separates the + accounting-carrier capability from the four real delivery channels. Every + settled transfer emits exact Email or Filing paperwork whether or not the + player is present; only a funded subscription captures it. +- **Process** — turn captured accounting mail into known accounts and flows. + This is the same intel-processing intention as the pooled host inbox; the + books target supplies the financial meaning. Captured payload and sealed ids + remain opaque until this act lands. - **Inject** — introduce a flow under a false source, *as a forged record*. A purchase order that says "HVAC controller" and buys you a rack (the design corpus's own example); a ghost vendor; a payroll line for a person who does - not exist. Under the financial-mail decision (2026-07-17, messages.md) the + not exist. B1 exposes this act only after processed record mail has revealed + the exact procurement-to-vendor pattern; an unrelated known payroll or + revenue flow cannot supply hidden purchase-order bindings. Under the + financial-mail decision (2026-07-17, messages.md) the injection **is a financial-record message** sent under a persona: it lands in - the auditor's inbox, waits on their read clock, and authorizes a real - account-graph transfer only once read and found plausible — after which it + Priya's inbox, waits on her read clock, and authorizes a real account-graph + transfer only once she reads it and its exact source, destination, vendor, + amount, and label remain acceptable — after which it persists as a record that a later reconciliation can catch (the banked-signature path). The [TUNE] plausibility test (amount, vendor, expected pattern) and that later catch are the forgery's two failure modes. @@ -168,7 +172,7 @@ no separate payoff or redirect shortcut around the plot executor. legibility law. - Your slush balance replaces the bare money integer, framed as one node on the graph. -- REVIEW acts on captured books; INJECT acts on those books; SIPHON and +- PROCESS acts on captured accounting mail; INJECT acts on known books; SIPHON and REDIRECT act on one selected flow. Each commitment shows amount, source/destination/cadence, and expected observer band before confirmation. TAP remains on the reachable carrier's local context menu because it @@ -181,13 +185,16 @@ no separate payoff or redirect shortcut around the plot executor. payments out all resolve on cadence; save/load round-trips the graph. The player's slush (starting at $0 — the Pilot begins broke) is one node. -2. The graph is hidden until earned: at start only slush is known; - tapping the accounting system (reach.md) + reviewing (intel.md) - reveals nodes and flows, with provenance (test: no free knowledge - of payroll). -3. Inject works: a false purchase order funds a real acquisition and - emits a financial signature to the auditing observer (test: the - "HVAC controller" rack buy raises Priya, not Dana). +2. The graph is hidden until earned: at start only slush is known; tapping the + accounting system (reach.md), waiting for authored record mail, and + processing that exact custody (intel.md) reveals nodes and flows with + provenance. An untapped record or an unprocessed captured record grants no + free payroll knowledge. +3. Inject works: a false purchase order is sent as Email under an exact usable + persona, funds nothing before recipient read, and revalidates its full bound + terms before acceptance causes the real transfer. The action emits a + financial signature to Priya, not Dana, and the accepted movement authors + its own matching financial record. 4. Redirect works: siphoning a flow raises slush and emits a signature scaled to the take; an unbalanced redirect is detectable by the audit path (test: small siphon low band, large siphon high band). diff --git a/wiki/mechanics/messages.md b/wiki/mechanics/messages.md index 37e85f85..3251527d 100644 --- a/wiki/mechanics/messages.md +++ b/wiki/mechanics/messages.md @@ -2,7 +2,7 @@ ``` Type: spec -Status: IN PROGRESS +Status: IMPLEMENTED Status note: IMPLEMENTED for the four delivery channels (Email, Phone, In-person, Filing) and their payloads; message-thread display lives in Operations PEOPLE (operations-workspace.md). REOPENED 2026-07-18 to @@ -14,18 +14,19 @@ Status note: IMPLEMENTED for the four delivery channels (Email, Phone, The same `Schedule` and route-hop vocabulary carry one-shot Network evidence from its exact source device to Dana's endpoint; Filing and Network transitions share one per-tick LIE-body capacity ledger. - DECIDED 2026-07-17 (issue #11), first runtime slice 2026-07-21: financial - paperwork is mail — a **financial-record payload** on the existing channels. - Save v46 retires the overloaded `MessageChannel::Financial` value and gives - the accounting source a separate persisted device capability while retaining - only the four real delivery channels. The existing direct-book-read runtime - (`tap_accounting`) now follows that capability and stands only as a - compatibility path until the next `financial-mail` slice authors and - intercepts the actual record messages. Discovery through mail, forged-record - read/acceptance, and record emission for every transfer remain unimplemented. - See criterion 8; the [TUNE] plausibility of a forged record passing and the - banked reconciliation that later catches it ride economy.md and - aggregate-observer.md. + DECIDED 2026-07-17 (issue #11), completed 2026-07-21: financial paperwork is + mail — a **financial-record payload** on the existing channels. Save v47 + retains exactly four delivery channels and one orthogonal accounting-carrier + device capability. Every settled account transfer authors one exact Email or + Filing record from that device; ordinary TAP captures it as opaque message + custody, and PROCESS alone opens its bound account/flow ids. A forged + purchase order is Email under the active persona, moves no money when sent, + and settles its exact transfer only when Priya reads and accepts still-valid + terms. Current-save validation binds every retained transfer to a unique, + immutable record sequence and rejects carrier, payload, account, flow, or + purchase-order acceptance disagreement. See criterion 8. The [TUNE] + plausibility envelope and banked reconciliation that later catches a forgery + ride economy.md and aggregate-observer.md. Stage: B1 — The Basement Work order: financial-mail Work priority: 70 @@ -170,7 +171,7 @@ starts on the authored Filing-capable switch device in ReachNet, crosses a typed outside relay, and reaches the receiving observer endpoint. One `AdvanceRoute` event moves one hop; only endpoint arrival can mark the message delivered, after which the recipient's ordinary sampling cadence schedules the -read. Current save v46 rejects missing/impossible carriers, malformed hop order, +read. Current save v47 rejects missing/impossible carriers, malformed hop order, duplicate scheduled transitions, endpoint/status disagreement, and impossible interdiction provenance. @@ -263,18 +264,19 @@ private message from the authored schedule. 7. No per-person special cases in the delivery code: one delivery system, per-instance data (schedules, distributions, policies) — the same fields must serve Act Two hires and aggregates. -8. **PARTIAL (DECIDED 2026-07-17, first slice 2026-07-21 — issue #11).** +8. **IMPLEMENTED (DECIDED 2026-07-17, completed 2026-07-21 — issue #11).** Financial records are messages: an invoice/PO rides Email, a - statement/past-due notice rides Filing. Save v46 retires the fifth delivery + statement/past-due notice rides Filing. Save v47 has no fifth delivery channel and persists accounting carriage as a separate device capability; - current construction and save validation pin a real four-channel carrier, - and the existing direct accounting tap resolves through that capability. - Still required: tapping the accounting carrier must subscribe to authored - record mail; discovery must occur only through interception rather than a - direct live-balance read; a forged record read and accepted must cause the - real account-graph transfer; and every real transfer must emit its record so - the books and mail stay consistent. Test both the forged-invoice-to-transfer - path and intercept-before-read path. + ordinary device TAP subscribes to its authored record mail. Every real + transfer emits one exact record on Email or Filing whether or not the player + is present. Only a funded TAP acquires the opaque message, and only PROCESS + reveals its sealed account/flow bindings. Injecting a purchase order authors + a still-unsettled Email under the exact active persona; Priya's read-time + acceptance revalidates the bound procurement account, slush destination, + Northside vendor, amount, and label before causing the real transfer. The + accepted movement then emits its own matching record. Save validation rejects + missing, duplicated, skipped, rewritten, or carrier-less financial records. Defense: `operations_projection::tests::active_tracks_social_commitments_not_device_work` pins the PEOPLE dossier as the shared direct-thread and learned-traffic surface, @@ -289,9 +291,17 @@ pins the distinct channel gates and their projected read windows. pins the complete delivery-channel inventory and the separate accounting capability; `reach::tests::accounting_carrier_requires_a_real_device_delivery_channel` and `save::tests::current_save_rejects_accounting_capability_without_mail` -fail closed on impossible persisted carriers. The existing +fail closed on impossible persisted carriers. +`sim::tests::communications::settled_transfers_author_exact_tappable_financial_mail_before_revealing_books` +pins authorship without player presence, untapped opacity, TAP capture before +read, and PROCESS-only account/flow discovery. `sim::tests::economy::accounting_tap_processes_financial_records_into_known_flows` -proves that the compatibility tap still resolves through the split capability. +pins the ordinary carrier-TAP to PROCESS route, while +`inject_and_redirect_emit_scaled_financial_signatures` pins that forged mail +moves no money before Priya's read and settles only after acceptance. +`save::tests::current_save_binds_every_transfer_to_one_exact_financial_message` +pins the one-to-one transfer record sequence and fails closed on a missing +carrier, rewritten payload, or omitted record. `sim::tests::communications::filing_advances_device_relay_endpoint_then_reads_on_recipient_cadence` pins ordered custody before delivery/read; `tapping_a_filing_carrier_reveals_its_route_but_cannot_stop_it` pins opaque diff --git a/wiki/mechanics/people-tokens.md b/wiki/mechanics/people-tokens.md index 3f09ff0e..cda99e73 100644 --- a/wiki/mechanics/people-tokens.md +++ b/wiki/mechanics/people-tokens.md @@ -28,7 +28,7 @@ Status note: IN PROGRESS. Current state: - **Routed-evidence foundation (criteria 2-3, partial).** Witnessed Physical acts now create observer-local records directly in each valid present witness's head. Every record preserves exact cause, site, acquisition tick, - and filing state through current save v46; filing binds it to the real Filing + and filing state through current save v47; filing binds it to the real Filing message, while Silent policy withholds it. It never duplicates into the pending pool and LIE cannot scrub it after acquisition. Its real Filing message now persists an ordered switch-device / outside-relay / recipient @@ -48,7 +48,7 @@ Status note: IN PROGRESS. Current state: her ordinary cadence reads it. TAP remains observation only. At the source hop, TAKE plus a wholly controlled FlowGraph path from that carrier to a node co-located with one online LIE body may stop it; Filing and Network - records spend the same one-record-per-body-per-tick capacity. Current save v46 + records spend the same one-record-per-body-per-tick capacity. Current save v47 persists in-flight, delivered, read, and stopped custody plus exact source/observer/machine/tick provenance. - **Deferred (remaining 2, 3, 6).** Non-Physical evidence outside the Filing diff --git a/wiki/mechanics/reach.md b/wiki/mechanics/reach.md index 99a8e208..a86973d0 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 v46 requires each controller's graph membership while + only, and current save v47 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. 2026-07-19: Filing routes bind their first hop to the real Filing-capable switch node; diff --git a/wiki/mechanics/sim-mechanics.md b/wiki/mechanics/sim-mechanics.md index 29da3e57..d8c5642a 100644 --- a/wiki/mechanics/sim-mechanics.md +++ b/wiki/mechanics/sim-mechanics.md @@ -3,7 +3,7 @@ ``` Type: knowledge ``` -Current as of 2026-07-19, post-demolition with B1 systems in the sim core. +Current as of 2026-07-21, post-demolition with B1 systems in the sim core. The facility-era supervillain systems were deleted under the no-dead-code clause (see wiki/log/2026-07-05-demolition.md). @@ -347,10 +347,15 @@ clause (see wiki/log/2026-07-05-demolition.md). - Act One account graph: Lab operating/payroll/procurement/vendor/utility, employee accounts, Bleakline Credit, external broker/position venues, and recurring revenue/payroll/vendor/debt flows. Only slush is known at start; - tapping a financial carrier and processing records reveals accounts/flows. -- Economy verbs exposed now: `tap ledger`, `process ledger`, `siphon`, - `redirect`, `inject`, `position`, and `sell-intel`. Debt service is exposed - as authored rows on Marcus rather than a ledger shortcut. + ordinary TAP on the accounting carrier captures later-authored Email/Filing + records, and PROCESS reveals their sealed accounts/flows. Every settled + transfer emits one exact record; an untapped or unprocessed record grants no + knowledge. +- Economy verbs exposed now: `tap `, `process ledger`, + `siphon`, `redirect`, `inject`, `position`, and `sell-intel`. INJECT authors a + purchase-order Email under the active persona; no money moves until Priya + reads and accepts its exact still-valid terms. Debt service is exposed as + authored rows on Marcus rather than a ledger shortcut. - Build items and costs (`tiles.rs::build_items`): Floor 0 (digging), Door 30, SecurityDoor1/2/3 = 80/150/250 (badge tiers), PowerCore 250. diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index b82f0dd9..f52cbb43 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -21,7 +21,7 @@ not a second status owner. | 40 | `people-tokens` | [people and tokens — carriers, attention, trust](../mechanics/people-tokens.md) | IN PROGRESS | save | - | | 60 | `plots` | [plots — authored manipulation stories](../mechanics/plots.md) | READY | sim | - | | 61 | `voss` | [Dr. Eli Voss — your handler](../world/characters/voss.md) | READY | sim | - | -| 70 | `financial-mail` | [messages — the social graph as a flow system](../mechanics/messages.md) | IN PROGRESS | save | - | +| 71 | `moonlight-gigs` | [income — the named schemes (moonlight and the wager)](../mechanics/income.md) | IN PROGRESS | save | - | ### Held or blocked @@ -29,7 +29,6 @@ not a second status owner. |---:|---|---|---|---|---| | 30 | `fleet-command` | [fleet command — ruling at scale](../interface/fleet-command.md) | DRAFT | frontend | - | | 30 | `opening` | [the dark opening — a tutorial made of fog](../world/story/opening.md) | DRAFT | frontend | - | -| 71 | `moonlight-gigs` | [income — the named schemes (moonlight and the wager)](../mechanics/income.md) | IN PROGRESS | save | financial-mail | | 120 | `core` | [the core](../mechanics/core.md) | IN PROGRESS | save | rollback | ### Later stages @@ -277,13 +276,13 @@ is retired — flat materials, Pixel Lab scrubbed.) PROCESS language also covers exact information, custody automation, sink receipts, and accounting records while old review forms remain input aliases. -### 17. Messages: the social graph as a flow system 🟥 sim+save — REOPENED 2026-07-18 -- **Spec:** [messages.md](../mechanics/messages.md) (IN PROGRESS — the four - delivery channels shipped 2026-07-08; reopened as the `financial-mail` - work order to build the issue-#11 decision: financial paperwork as - interceptable financial-record payloads, retiring - `MessageChannel::Financial` into an accounting-carrier capability, with - economy.md's tap/inject discovery moving onto that mail) +### 17. Messages: the social graph as a flow system 🟥 sim+save — DONE 2026-07-21 +- **Spec:** [messages.md](../mechanics/messages.md) (IMPLEMENTED — the four + delivery channels shipped 2026-07-08; the reopened `financial-mail` work + order completed the issue-#11 decision on 2026-07-21: financial paperwork + is interceptable Email/Filing payload, the accounting carrier is an + orthogonal device capability, and economy.md discovery now requires + ordinary TAP plus PROCESS) - **Why:** the general message-passing system the flow law requires — channels with read conditions, delivery on the recipient's clock, authored traffic (Marcus's 3 a.m. call becomes phone traffic), @@ -295,8 +294,10 @@ is retired — flat materials, Pixel Lab scrubbed.) every aggregate-observer.md criterion — the filings refactor is a carrier change, not a behavior change. - **Result:** scheduled channels, authored traffic, filings-as-messages, - intercepted payloads, save state, and the shared terminal/Bevy/agent - surfaces are implemented. + intercepted payloads, and shared terminal/Bevy/agent surfaces are + implemented. Save v47 additionally binds every settled transfer to one + exact accounting-carrier Email/Filing record, removes direct graph-snapshot + discovery, and defers forged purchase-order settlement until accepted read. ### 18. Economy: money as flows 🟥 sim+save — DONE 2026-07-08 - **Spec:** [economy.md](../mechanics/economy.md) (IMPLEMENTED 2026-07-08; diff --git a/wiki/process/specs.md b/wiki/process/specs.md index db48e331..d2ea1fac 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -53,7 +53,7 @@ replaced the old `spec/`/`knowledge/` directory split. | [../mechanics/income.md](../mechanics/income.md) | income — the named schemes (moonlight and the wager) | IN PROGRESS | | [../mechanics/intel.md](../mechanics/intel.md) | intel — record and process | IMPLEMENTED | | [../mechanics/machine-work.md](../mechanics/machine-work.md) | machine work — delegation, visible tokens, and the byproduct network | IMPLEMENTED | -| [../mechanics/messages.md](../mechanics/messages.md) | messages — the social graph as a flow system | IN PROGRESS | +| [../mechanics/messages.md](../mechanics/messages.md) | messages — the social graph as a flow system | IMPLEMENTED | | [../mechanics/people-tokens.md](../mechanics/people-tokens.md) | people and tokens — carriers, attention, trust | IN PROGRESS | | [../mechanics/plots.md](../mechanics/plots.md) | plots — authored manipulation stories | READY | | [../mechanics/reach.md](../mechanics/reach.md) | digital reach | IMPLEMENTED | diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 142c0fde..8c9412b3 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -54,7 +54,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/mechanics/cursor.md` | 2026-07-18 | finding | re-audit: `fog_at` precedence, `inspect`, `person_label`/`observer_label`/`person_glyph` gates, `Sensor.sees`/`hears` flags, and the cursor's absence from the save all still verify (prior pins stand); the drift was two phrases presenting the retired versioned-migration rule as current (criterion 1's parenthetical, the design-notes save-format bullet) — both now state the current-version-only policy and the release-era ladder owed at first public release; number-free wording, so the save-claim gate could not see it | | `wiki/mechanics/intel.md` | 2026-07-21 | finding | the Storage B alternate route existed only in prose. Fire 131 connects it to Ray's real 23:00 schedule, one exact carried records-box target, the canonical bounded opaque buffer, and ordinary PROCESS consequence; retrieval cannot duplicate the file or reveal Marcus's debt, and v45 preserves/validates exact custody — [log](../log/2026-07-21-storage-b-records.md). Prior recursive custody, magnitude, and consequence-first audits stand. | | `wiki/mechanics/research.md` | 2026-07-18 | finding | re-audit: track table, per-level multipliers, single-active-track pin, and deterministic-progress claims still verify (constants re-checked in the same-day sim-mechanics sweep); the drift was criterion 6 still asserting the v20 three-entry padding migration loads — the phrasing ("load by padding") slipped past the save-claim gate's verb list, so both moved: the criterion now names the retirement, and the gate matches pad/padded/padding and "loads by" with a new fixture | -| `wiki/mechanics/economy.md` + `messages.md` | 2026-07-21 | finding | Fire #145 first implementation slice: the unused fifth `MessageChannel::Financial` was only an overloaded device tag, so save v46 removes it and persists one separate accounting-carrier capability on the authored switch while retaining Email/Filing/Phone delivery. Accounting actions, Operations, and the existing direct snapshot compatibility path now resolve through that capability; current-save validation rejects a carrier with no real delivery channel. Criterion 8 remains PARTIAL: no financial-record messages are authored yet, discovery still reads the graph directly, and forged-record read/accept plus transfer-to-record emission remain — [log](../log/2026-07-21-financial-channel-retirement.md). Prior dispatch audit: [2026-07-18](../log/2026-07-18-financial-mail-dispatch.md) | +| `wiki/mechanics/economy.md` + `messages.md` | 2026-07-21 | finding | Fire #146 completes the issue-#11 contract: save v47 binds every settled transfer to one immutable Email or Filing record authored by the orthogonal accounting carrier; ordinary funded TAP captures opaque message custody, PROCESS alone opens its account/flow bindings, and the direct `RawIntelKind::FinancialFlow` snapshot path is gone. Forged purchase orders now move no money when sent and settle only when Priya reads and accepts still-valid persona-bound terms; the accepted transfer emits its own ordinary record. Criterion 8 and the `financial-mail` work order are IMPLEMENTED — [log](../log/2026-07-21-financial-mail-causality.md). Capability foundation: [Fire #145](../log/2026-07-21-financial-channel-retirement.md). | | `wiki/mechanics/income.md` | 2026-07-18 | finding | Beacon feel note 5 verified as a real bug: the embedded contractor-persona field was never written, so the Moonlight card, the start-row persona pricing, and the agent status line all read a dead `None`; earning itself was correct (schemes mirrors the WORK share) but illegible at 0.0. Removed the dead field, routed every reader through `Sim::moonlight_persona` (persona-world link), added the stalled-earning card cue, regression test, and spec amendment — [log](../log/2026-07-18-moonlight-persona-card.md). Prior Wager/egress audit (2026-07-12) stands: Wager constants match (`WAGER_STAKE_CAP` 300, base 0.55, cap 0.75, mult 2x, analysis divisor 400), Marcus's $400/week arrears is the modeled creditor flow (`account.rs`) while the $8,400 principal is narrative-by-design (spec states full payoff is not a B1 requirement), egress/banked-signature present, and all 7 criteria have passing tests (moonlight payout/signature, wager outcomes/cap, egress routes, hands-beat-from-zero, busted-bankroll) | | `wiki/mechanics/schedules.md` | 2026-07-18 | clean | re-audit: `ScheduleBlock` per-instance data, `DAY_TICKS` 400, the `person_glyph` `?`-until-Schedule gate (initial at Schedule/Leverage), and located-witnessing claims all still verify; nothing drifted since the 2026-07-12 prose fix. Prior verdict: mechanism matches code (`ScheduleBlock` start/end/room, single `DAY_TICKS`=400 clock, `person_glyph` `?`-until-Schedule, erratic day-hash drift, per-instance data, all 5 criteria have passing tests); tightened stale Act One prose — Ray patrols dock/stairwell/storage not "corridors", Priya has no "office-off-plane" block, Voss's two blocks are both server-room — to match `People::act_one` | | `wiki/mechanics/social.md` | 2026-07-21 | finding | Fire 131 extends carried asset work with one route-shaped physical file task: availability derives from the selected actor's authored Storage B schedule and real door tier, commitment persists exact subject/room/tile custody, and arrival deposits opaque information rather than instant knowledge. Ray proves the route before Marcus debt/recruitment; malformed and duplicate v45 packets fail closed — [log](../log/2026-07-21-storage-b-records.md). Prior role-shaped tasks and recruitment semantics stand. | diff --git a/wiki/world/story/opening.md b/wiki/world/story/opening.md index ba840c29..c17cdab9 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 v46, and all three frontends; the three historical fragments and receipts + current save v47, 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