diff --git a/crates/misaligned-core/src/actions.rs b/crates/misaligned-core/src/actions.rs index f0df03e0..3c73e859 100644 --- a/crates/misaligned-core/src/actions.rs +++ b/crates/misaligned-core/src/actions.rs @@ -2446,7 +2446,7 @@ impl Sim { }) .is_some(); ActionDesc { - verb: format!("PROCESS INFORMATION #{raw_id}"), + verb: "PROCESS THIS INFORMATION".into(), command: ActionCommand::ReviewRecording { raw_id }, cost: ActionCost::Thought(self.review_tokens()), signature: None, @@ -4308,10 +4308,10 @@ impl Sim { let title = self.render_plot_text(id, &plot.title); let synopsis = self.render_plot_text(id, &plot.synopsis); out.push(ActionDesc { - // Catalog id sits beside the title so the typed - // `plot ` form is discoverable from - // the same shared ActionDesc row that `act ` uses. - verb: format!("plot [{}]: {title} — {synopsis}", plot.id), + // Human surfaces receive authored world language. + // The agent protocol renderer adds the exact catalog + // binding for typed dispatch. + verb: format!("plot: {title} — {synopsis}"), command: ActionCommand::StartPlot { person: id, plot_id: plot.id.clone(), @@ -4518,7 +4518,7 @@ impl Sim { let current = lot.token(stream.id); let value = lot.value; Some(ActionDesc { - verb: format!("sell report lot {}", token.label()), + verb: format!("sell {} report lot", stream.class.label()), command: ActionCommand::SellReportLot { token }, cost: ActionCost::Gain(value), signature: self.signature_note( @@ -4526,7 +4526,7 @@ impl Sim { Self::financial_sig_size(value).max(1), ), disabled_reason: (current != token) - .then(|| format!("lot changed; current snapshot is {}", current.label())), + .then(|| "lot changed; reopen the current report lot".into()), automate: None, }) } @@ -4618,9 +4618,9 @@ impl Sim { }); out.push(ActionDesc { verb: format!( - "auto-sell {} lots via information broker to account #{} at {} reports, >=${}, <=128 reports/$10000, Financial <=100", + "auto-sell {} lots via information broker to {} at {} reports, >=${}, <=128 reports/$10000, Financial <=100", class.label(), - self.accounts.slush_id(), + self.accounts.account_name(self.accounts.slush_id()), 1, class.unit_value(), ), @@ -4689,7 +4689,7 @@ impl Sim { .position(|candidate| *candidate == index) .expect("policy is its own reorder peer"); out.push(ActionDesc { - verb: format!("remove policy #{}", rule.id), + verb: format!("remove policy {}", index + 1), command: ActionCommand::RemoveIntelPolicy { node_id, rule_id: rule.id, @@ -4704,7 +4704,7 @@ impl Sim { (false, "later", peer_index + 1 == peers.len()), ] { out.push(ActionDesc { - verb: format!("move policy #{} {direction}", rule.id), + verb: format!("move policy {} {direction}", index + 1), command: ActionCommand::MoveIntelPolicy { node_id, rule_id: rule.id, @@ -6071,10 +6071,16 @@ mod tests { matches!( &plot.command, ActionCommand::StartPlot { plot_id, .. } - if plot.verb.contains(&format!("plot [{plot_id}]:")) + if !plot.verb.contains(plot_id) ) }), - "start rows surface the catalog plot-id for the typed form" + "human plot copy omits the catalog binding" + ); + assert!( + marcus_routes + .iter() + .all(|plot| !plot.verb.contains("marcus-payroll-garnishment") + && !plot.verb.contains("marcus-benefactor")) ); assert!(marcus_routes.iter().any(|plot| { plot.cost diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index 2f857e41..6b4a806f 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -832,7 +832,7 @@ impl Sim { learned_result: None, consequence: None, target: OperationsTarget::IntelLot { token }, - label: format!("{} report lot {}", stream.class.label(), token.label()), + label: format!("{} report lot", stream.class.label()), state, provenance: vec![format!( "{} · {}", @@ -840,8 +840,6 @@ impl Sim { lot.provenance.source_mix_label() )], facts: vec![ - format!("generation: {}", token.generation), - format!("revision: {}", token.revision), format!("reports: {}", lot.provenance.count), lot.provenance.max_magnitude.map_or_else( || "maximum magnitude: not recorded".into(), @@ -855,7 +853,7 @@ impl Sim { ), ], progress: (current != token) - .then(|| format!("stale: current snapshot is {}", current.label())) + .then(|| "stale: this report lot changed; reopen the current lot".into()) .into_iter() .collect(), related: vec![OperationsLink { @@ -889,15 +887,16 @@ impl Sim { let failures = node_id .and_then(|id| self.intel_policies.node(id)) .map(|node| { - for rule in &node.local_rules { - facts.push(self.intel_policy_fact(rule)); + for (index, rule) in node.local_rules.iter().enumerate() { + facts.push(self.intel_policy_fact(index + 1, rule)); } node.local_rules .iter() - .filter_map(|rule| { + .enumerate() + .filter_map(|(index, rule)| { rule.failure .as_ref() - .map(|failure| (rule.id, failure.clone())) + .map(|failure| (index + 1, failure.clone())) }) .collect::>() }) @@ -912,7 +911,7 @@ impl Sim { label: self .intel_policies .node(parent) - .map(|node| node.kind.label()) + .map(|node| self.intel_custody_label(&node.kind)) .unwrap_or_else(|| "INFORMATION AVAILABLE".into()), target: if parent == 0 { OperationsTarget::RecordingInbox @@ -924,7 +923,7 @@ impl Sim { if let Some(lot) = &stream.open_lot { related.push(OperationsLink { relation: "OPEN LOT", - label: lot.token(stream.id).label(), + label: format!("{} report lot", stream.class.label()), target: OperationsTarget::IntelLot { token: lot.token(stream.id), }, @@ -966,7 +965,7 @@ impl Sim { facts, progress: failures .iter() - .map(|(id, failure)| format!("policy #{id} suspended: {failure}")) + .map(|(position, failure)| format!("policy {position} suspended: {failure}")) .collect(), related, actions: node_id @@ -1046,21 +1045,22 @@ impl Sim { let failures = node .local_rules .iter() - .filter_map(|rule| rule.failure.as_ref().map(|failure| (rule.id, failure))) + .enumerate() + .filter_map(|(index, rule)| rule.failure.as_ref().map(|failure| (index + 1, failure))) .collect::>(); let mut facts = vec![if waiting.is_empty() { "NO INFORMATION AVAILABLE".into() } else { "INFORMATION AVAILABLE".into() }]; - for rule in &node.local_rules { - facts.push(self.intel_policy_fact(rule)); + for (index, rule) in node.local_rules.iter().enumerate() { + facts.push(self.intel_policy_fact(index + 1, rule)); } if node.local_rules.is_empty() { facts.push("policies: inherited from parent".into()); } - for (id, failure) in &failures { - facts.push(format!("policy #{id} failure: {failure}")); + for (position, failure) in &failures { + facts.push(format!("policy {position} failure: {failure}")); } let mut related = Vec::new(); if let Some(parent) = node.parent { @@ -1069,7 +1069,7 @@ impl Sim { label: self .intel_policies .node(parent) - .map(|parent| parent.kind.label()) + .map(|parent| self.intel_custody_label(&parent.kind)) .unwrap_or_else(|| "INFORMATION AVAILABLE".into()), target: if parent == 0 { OperationsTarget::RecordingInbox @@ -1085,7 +1085,7 @@ impl Sim { .filter(|child| child.parent == Some(node_id)) .map(|child| OperationsLink { relation: "CHILD", - label: child.kind.label(), + label: self.intel_custody_label(&child.kind), target: match child.kind { IntelCustodyKind::Stream { stream_id } => { OperationsTarget::IntelStream { stream_id } @@ -1100,7 +1100,7 @@ impl Sim { .filter_map(|raw_id| self.intel_buffer.iter().find(|event| event.id == *raw_id)) .map(|event| OperationsLink { relation: "INFORMATION", - label: format!("information #{} - {}", event.id, event.opaque_label()), + label: self.raw_information_label(event), target: OperationsTarget::RawRecording { raw_id: event.id }, }), ); @@ -1108,7 +1108,7 @@ impl Sim { learned_result: None, consequence: None, target: OperationsTarget::IntelCustody { node_id }, - label: node.kind.label(), + label: self.intel_custody_label(&node.kind), state: if !failures.is_empty() { ObjectState::Stopped } else if !waiting.is_empty() { @@ -1120,14 +1120,14 @@ impl Sim { facts, progress: failures .iter() - .map(|(id, failure)| format!("policy #{id} suspended: {failure}")) + .map(|(position, failure)| format!("policy {position} suspended: {failure}")) .collect(), related, actions: self.intel_custody_actions(node_id), }) } - fn intel_policy_fact(&self, rule: &IntelPolicyRule) -> String { + fn intel_policy_fact(&self, position: usize, rule: &IntelPolicyRule) -> String { let state = if rule.suspended { "SUSPENDED" } else if rule.enabled { @@ -1143,12 +1143,43 @@ impl Sim { IntelPolicyOutcome::AutoSell(_) => "none before settlement".into(), _ => "none".into(), }; + let outcome = match &rule.outcome { + IntelPolicyOutcome::AutoSell(envelope) => format!( + "AUTO-SELL via {} -> {} at {} reports, >=${}, <={} reports/${}, Financial <={}", + envelope.buyer, + self.accounts.account_name(envelope.payout_account), + envelope.trigger_count, + envelope.minimum_payout, + envelope.maximum_quantity, + envelope.maximum_value, + envelope.maximum_signature + ), + _ => rule.outcome.label().into(), + }; format!( - "policy #{} [{}] · standing cost: {standing_cost} · {} -> {}", - rule.id, - state, - rule.match_kind.label(), - rule.outcome.detail_label(), + "policy {position} [{state}] · standing cost: {standing_cost} · {} -> {outcome}", + rule.match_kind.label() + ) + } + + fn intel_custody_label(&self, kind: &IntelCustodyKind) -> String { + match kind { + IntelCustodyKind::Stream { stream_id } => self + .intel_streams + .iter() + .find(|stream| stream.id == *stream_id) + .map(|stream| format!("{} / {}", stream.feed, stream.class.label())) + .unwrap_or_else(|| "report stream".into()), + _ => kind.label(), + } + } + + fn raw_information_label(&self, recording: &RawIntelEvent) -> String { + format!( + "{} · {} · tick {}", + recording.opaque_label(), + recording.feed, + recording.tick ) } @@ -1184,11 +1215,7 @@ impl Sim { target: OperationsTarget::RawRecording { raw_id: recording.id, }, - label: format!( - "information #{} - {}", - recording.id, - recording.opaque_label() - ), + label: self.raw_information_label(recording), state: if processing { ObjectState::Running } else { @@ -1255,8 +1282,8 @@ impl Sim { if overflow { facts.push("OLDEST INFORMATION WILL BE LOST WHEN NEW INFORMATION ARRIVES".into()); } - for rule in &self.intel_policies.root().local_rules { - facts.push(format!("root {}", self.intel_policy_fact(rule))); + for (index, rule) in self.intel_policies.root().local_rules.iter().enumerate() { + facts.push(format!("root {}", self.intel_policy_fact(index + 1, rule))); } let related = self .intel_policies @@ -1267,7 +1294,7 @@ impl Sim { }) .map(|node| OperationsLink { relation: "FEED", - label: node.kind.label(), + label: self.intel_custody_label(&node.kind), target: OperationsTarget::IntelCustody { node_id: node.id }, }) .collect(); @@ -1562,11 +1589,7 @@ impl Sim { .rev() .find(|(_, run)| run.target == id && run.active()) { - let label = self - .plot_catalog() - .get(&run.plot_id) - .map(|plot| self.render_plot_text(id, &plot.title)) - .unwrap_or_else(|| run.plot_id.clone()); + let label = self.plot_world_title(id, &run.plot_id); links.push(OperationsLink { relation: "ACTIVE", label, @@ -1583,6 +1606,13 @@ impl Sim { self.plot_progress(run) } + fn plot_world_title(&self, person: u8, plot_id: &str) -> String { + self.plot_catalog() + .get(plot_id) + .map(|plot| self.render_plot_text(person, &plot.title)) + .unwrap_or_else(|| "unavailable operation".into()) + } + // ── PERSONAS ─────────────────────────────────────────────────────────── fn persona_observer_label(&self, observer: u8) -> String { @@ -1609,6 +1639,19 @@ impl Sim { } } + fn persona_observer_target(&self, observer: u8) -> Option { + if observer == crate::income::MOONLIGHT_CLIENT_ID { + Some(OperationsTarget::Scheme(SchemeKind::Moonlight)) + } else if observer == crate::detection::OFFICE_ID { + self.detection_awareness + .knows_assurance_office() + .then_some(OperationsTarget::AssuranceOffice) + } else { + self.person_is_earned(observer) + .then_some(OperationsTarget::Person(observer)) + } + } + fn personas_view(&self) -> Vec { let mut instances = self.persona_world.instances.iter().collect::>(); instances.sort_by_key(|instance| instance.id); @@ -1647,8 +1690,7 @@ impl Sim { .filter(|grant| grant.persona_id == instance.id) { facts.push(format!( - "grant #{}: {} / {} / {}", - grant.id, + "grant: {} / {} / {}", grant.institution, grant.resource, if grant.active() { @@ -1665,11 +1707,10 @@ impl Sim { .filter(|expectation| expectation.persona_id == instance.id) { facts.push(format!( - "expectation #{}: {} · due {} · {:?}", - expectation.id, + "expectation: {} · due tick {} · {}", expectation.description, expectation.due_tick, - expectation.state + expectation.state.label() )); } for relationship in self @@ -1709,14 +1750,11 @@ impl Sim { { let observer = self.persona_observer_label(contradiction.observer); facts.push(format!( - "contradiction #{} observed by {}: {} [{}:{} <> {}:{}]", - contradiction.id, + "contradiction observed by {}: {} [{} <> {}]", observer, contradiction.cause, contradiction.left.system, - contradiction.left.record_id, - contradiction.right.system, - contradiction.right.record_id + contradiction.right.system )); } for correlation in self.persona_world.correlations.iter().filter(|edge| { @@ -1727,17 +1765,20 @@ impl Sim { } else { correlation.left_persona }; + let other = self + .persona_world + .instances + .iter() + .find(|candidate| candidate.id == other) + .map(|candidate| candidate.name.as_str()) + .unwrap_or("another public identity"); let observer = self.persona_observer_label(correlation.observer); facts.push(format!( - "correlation #{} with persona {}: observer {} · {} · source {} · tick {}", - correlation.id, + "correlation with {}: observer {} · {} · source {} · tick {}", other, observer, correlation.cause, - format_args!( - "{}:{}", - correlation.evidence.system, correlation.evidence.record_id - ), + correlation.evidence.system, correlation.discovered_tick )); } @@ -1791,7 +1832,10 @@ impl Sim { }) { actions.push(action( - format!("FULFILL EXPECTATION #{}", expectation.id), + format!( + "FULFILL EXPECTATION: {}", + expectation.description.to_ascii_uppercase() + ), ActionCommand::MeetPersonaExpectation { persona_id: instance.id, expectation_id: expectation.id, @@ -1832,7 +1876,7 @@ impl Sim { PersonaLifecycle::Retired { .. } => ObjectState::Stopped, PersonaLifecycle::Burned { .. } => ObjectState::Failed, }, - provenance: vec![format!("public identity record #{}", instance.id)], + provenance: vec![format!("public identity created tick {}", instance.created_tick)], facts, progress: Vec::new(), related: self @@ -1840,16 +1884,14 @@ impl Sim { .relationships .iter() .filter(|relationship| relationship.persona_id == instance.id) - .map(|relationship| OperationsLink { - relation: "known by", - label: self - .people - .get(relationship.counterparty) - .map(|person| person.name.clone()) - .unwrap_or_else(|| { - format!("counterparty {}", relationship.counterparty) - }), - target: OperationsTarget::Person(relationship.counterparty), + .filter_map(|relationship| { + let target = + self.persona_observer_target(relationship.counterparty)?; + Some(OperationsLink { + relation: "known by", + label: self.persona_observer_label(relationship.counterparty), + target, + }) }) .collect(), actions, @@ -2140,7 +2182,7 @@ impl Sim { related: open .map(|position| OperationsLink { relation: "ACTIVE", - label: format!("wager position #{}", position.id), + label: format!("wager opened tick {}", position.opened_tick), target: OperationsTarget::WagerPosition(position.id), }) .into_iter() @@ -2195,11 +2237,7 @@ impl Sim { { continue; } - let title = self - .plot_catalog() - .get(plot_id) - .map(|p| self.render_plot_text(*person, &p.title)) - .unwrap_or_else(|| plot_id.clone()); + let title = self.plot_world_title(*person, plot_id); let pct = if sink.threshold > 0.0 { (sink.fill / sink.threshold * 100.0).min(100.0) } else { @@ -2368,10 +2406,7 @@ impl Sim { } fn active_plot_object(&self, index: usize, run: &PlotRun) -> OperationsObject { - let plot = self.plot_catalog().get(&run.plot_id); - let title = plot - .map(|p| self.render_plot_text(run.target, &p.title)) - .unwrap_or_else(|| run.plot_id.clone()); + let title = self.plot_world_title(run.target, &run.plot_id); let state = plot_state_label(&run.state); let progress = self.plot_progress(run); let actions = if matches!(run.state, PlotState::WaitingForChoice { .. }) { @@ -2439,7 +2474,7 @@ impl Sim { learned_result: None, consequence: None, target: OperationsTarget::WagerPosition(p.id), - label: format!("wager position #{}", p.id), + label: format!("wager opened tick {}", p.opened_tick), state, provenance: vec!["external market".into()], facts, @@ -2509,7 +2544,7 @@ fn egress_fact(sim: &Sim) -> String { #[cfg(test)] mod tests { use super::*; - use crate::actions::{ActionCost, Anchor}; + use crate::actions::{ActionCost, Anchor, menu_rows}; use crate::detection::SignatureKind; use crate::intel::{IntelKind, RawIntelClass, RawIntelEvent, RawIntelKind}; use crate::person::Knowledge; @@ -2566,6 +2601,35 @@ mod tests { drain_ops(sim); } + fn operations_human_copy(object: &OperationsObject) -> String { + let mut lines = Vec::new(); + if let Some(result) = &object.learned_result { + lines.push(result.clone()); + } + if let Some(consequence) = &object.consequence { + lines.push(consequence.statement.clone()); + if let Some(action) = &consequence.action { + lines.push(action.label.clone()); + } + } + lines.push(object.label.clone()); + lines.extend(object.provenance.iter().cloned()); + lines.extend(object.facts.iter().cloned()); + lines.extend(object.progress.iter().cloned()); + lines.extend(object.related.iter().map(|link| link.label.clone())); + for action in &object.actions { + lines.push(action.verb.clone()); + if let Some(reason) = &action.disabled_reason { + lines.push(reason.clone()); + } + if let Some(automate) = &action.automate { + lines.push(automate.verb.clone()); + lines.push(automate.cost.clone()); + } + } + lines.join("\n") + } + /// The projection is read-only and reuses the per-anchor legality helpers: /// each object's bound rows equal the rows the direct anchor query returns. #[test] @@ -3060,6 +3124,18 @@ mod tests { ActionCommand::StartPlot { plot_id, .. } => plot_id.clone(), _ => unreachable!(), }; + assert!(!plot_row.verb.contains(&expected_plot_id)); + assert!( + menu_rows(std::slice::from_ref(&plot_row))[0] + .label + .contains("plot:") + ); + assert!( + !menu_rows(std::slice::from_ref(&plot_row))[0] + .label + .contains(&expected_plot_id), + "the shared human Operations row must not serialize its exact catalog binding" + ); s.execute_action(&plot_row.command); @@ -3354,7 +3430,8 @@ mod tests { /// The pooled inbox can be addressed exactly without turning information /// into person queues or revealing their hidden payload. The selected - /// opaque id is the id bound into the processing reservoir. + /// opaque internal id stays in the target and processing reservoir, not + /// in the human label or action copy. #[test] fn raw_information_rows_bind_exact_opaque_inbox_ids() { let mut s = sim(); @@ -3401,8 +3478,10 @@ mod tests { .operations_object(&OperationsTarget::RawRecording { raw_id: 90_102 }) .expect("exact information is addressable from drill-down"); assert!(!selected.label.contains("Marcus") && !selected.label.contains("Dana")); + assert!(!selected.label.contains("90102")); + assert!(selected.label.contains("test recorder")); let action = selected.actions.first().expect("one exact process row"); - assert_eq!(action.verb, "PROCESS INFORMATION #90102"); + assert_eq!(action.verb, "PROCESS THIS INFORMATION"); assert_eq!( action.command, ActionCommand::ReviewRecording { raw_id: 90_102 } @@ -3427,6 +3506,183 @@ mod tests { ); } + #[test] + fn human_operations_copy_omits_internal_bindings_but_agent_targets_keep_them() { + use crate::persona::EvidenceRecord; + + let mut s = sim(); + + let raw_id = 77_001; + s.intel_buffer.push(RawIntelEvent { + id: raw_id, + tick: s.tick, + feed: "sentinel recorder".into(), + room: Some("Server Room".into()), + x: 0, + y: 0, + person: None, + kind: RawIntelKind::Presence { entered: true }, + }); + let class_node = s + .intel_policies + .ensure_raw_class("sentinel recorder", RawIntelClass::Presence); + + let rule_id = 77_002; + s.intel_policies.next_rule_id = rule_id; + assert_eq!( + s.intel_policies + .push_rule(class_node, IntelPolicyMatch::All, IntelPolicyOutcome::Wait,), + rule_id + ); + + let persona_id = 77_003; + s.persona_world.next_persona_id = persona_id; + s.set_persona("Northline Operations", "contractor"); + assert_eq!(s.active_persona_id(), Some(persona_id)); + let grant_id = 77_004; + let expectation_id = 77_005; + s.persona_world.next_grant_id = grant_id; + s.persona_world.next_expectation_id = expectation_id; + assert!(s.request_persona_grant(persona_id)); + + let other_persona = 77_006; + s.persona_world.next_persona_id = other_persona; + s.set_persona("Glass Harbor Research", "researcher"); + s.persona_world.recognize(0, persona_id, s.tick); + s.persona_world.recognize(0, other_persona, s.tick); + let contradiction_id = 77_007; + s.persona_world.next_contradiction_id = contradiction_id; + s.persona_world.record_contradiction( + persona_id, + 0, + [ + EvidenceRecord { + system: "email".into(), + record_id: "internal-record-77011".into(), + summary: "a service order arrived".into(), + observed_tick: s.tick, + }, + EvidenceRecord { + system: "facilities".into(), + record_id: "internal-record-77012".into(), + summary: "facilities denied the order".into(), + observed_tick: s.tick, + }, + ], + "the order and facilities record disagree", + 20, + s.tick, + ); + let correlation_id = 77_008; + s.persona_world.next_correlation_id = correlation_id; + assert_eq!( + s.persona_world + .record_correlation( + persona_id, + other_persona, + 0, + "both identities used the same service route", + EvidenceRecord { + system: "email".into(), + record_id: "internal-record-77013".into(), + summary: "both identities used one route".into(), + observed_tick: s.tick, + }, + s.tick, + ) + .expect("valid correlation"), + correlation_id + ); + + let position_id = 77_009; + s.accounts.positions.push(Position { + id: position_id, + stake: 10, + opened_tick: s.tick, + resolve_tick: s.tick + 100, + analysis_compute: 0.5, + resolved: false, + outcome: None, + known: true, + }); + + let internal_plot_slug = "internal-plot-slug-77014"; + s.plot_runs.push(PlotRun { + plot_id: internal_plot_slug.into(), + target: 0, + persona_id: Some(persona_id), + started_tick: s.tick, + committed_thought_milli: 250, + beat_index: 0, + act_index: 0, + state: PlotState::Running, + }); + + let objects = [ + s.operations_object(&OperationsTarget::RawRecording { raw_id }) + .expect("raw information"), + s.operations_object(&OperationsTarget::IntelCustody { + node_id: class_node, + }) + .expect("custody class"), + s.operations_object(&OperationsTarget::Persona(persona_id)) + .expect("persona"), + s.operations_object(&OperationsTarget::WagerPosition(position_id)) + .expect("wager position"), + s.operations_object(&OperationsTarget::Scheme(SchemeKind::Wager)) + .expect("wager scheme"), + s.operations_object(&OperationsTarget::ActivePlotRun { index: 0 }) + .expect("active plot with missing catalog definition"), + ]; + let internal_tokens = [ + raw_id, + rule_id, + persona_id, + grant_id, + expectation_id, + other_persona, + contradiction_id, + correlation_id, + position_id, + 77_011, + 77_012, + 77_013, + ]; + for object in &objects { + let copy = operations_human_copy(object); + assert!( + !copy.contains(internal_plot_slug), + "human copy for {:?} leaked plot catalog slug:\n{copy}", + object.target + ); + for token in internal_tokens { + assert!( + !copy.contains(&token.to_string()), + "human copy for {:?} leaked internal token {token}:\n{copy}", + object.target + ); + } + } + + assert_eq!( + OperationsTarget::RawRecording { raw_id }.agent_suffix(), + Some(format!("@information({raw_id})")) + ); + assert_eq!( + OperationsTarget::Persona(persona_id).agent_suffix(), + Some(format!("@persona({persona_id})")) + ); + assert!(objects[0].actions.iter().any(|action| { + matches!(action.command, ActionCommand::ReviewRecording { raw_id: bound } if bound == raw_id) + })); + assert!(objects[1].actions.iter().any(|action| { + matches!(action.command, ActionCommand::RemoveIntelPolicy { rule_id: bound, .. } if bound == rule_id) + })); + assert!(objects[2].actions.iter().any(|action| { + matches!(action.command, ActionCommand::MeetPersonaExpectation { expectation_id: bound, .. } if bound == expectation_id) + })); + } + /// Related links are earned semantic targets. Following one through the /// shared state machine opens the canonical object in its owning view. #[test] @@ -3743,7 +3999,13 @@ mod tests { .iter() .find(|object| object.target == OperationsTarget::Persona(id)) .unwrap(); - assert!(object.facts.iter().any(|fact| fact.starts_with("grant #"))); + assert!(object.facts.iter().any(|fact| fact.starts_with("grant:"))); + assert!( + object + .facts + .iter() + .all(|fact| !fact.contains(&format!("#{id}"))) + ); assert!(object.actions.iter().any(|action| { matches!(action.command, ActionCommand::MeetPersonaExpectation { persona_id, .. } if persona_id == id) })); diff --git a/crates/misaligned-terminal/src/agent.rs b/crates/misaligned-terminal/src/agent.rs index 15a27eaa..9763b2d4 100644 --- a/crates/misaligned-terminal/src/agent.rs +++ b/crates/misaligned-terminal/src/agent.rs @@ -7,7 +7,7 @@ use std::collections::BTreeSet; use std::io::{self, BufRead, Write}; -use misaligned::actions::{ActionKind, ActionRole, Anchor, MenuRow, menu_rows}; +use misaligned::actions::{ActionCommand, ActionKind, ActionRole, Anchor, MenuRow, menu_rows}; use misaligned::detection::{Band, SignatureKind}; use misaligned::hall::RackSite; use misaligned::intel::{IntelRoutineClass, ReportLotToken}; @@ -1612,7 +1612,14 @@ fn action_row_lines(rows: &[MenuRow]) -> Vec { if r.indent { line.push_str("- "); } - line.push_str(&format!("{} | {}", r.label, r.cost)); + let label = match &r.command { + ActionCommand::StartPlot { plot_id, .. } => format!( + "plot [{plot_id}]: {}", + r.label.strip_prefix("plot: ").unwrap_or(&r.label) + ), + _ => r.label.clone(), + }; + line.push_str(&format!("{label} | {}", r.cost)); if let Some(sig) = &r.signature { line.push_str(&format!(" | {sig}")); } else { @@ -4307,6 +4314,13 @@ mod narration_tests { } let rows = menu_rows(&app.sim.available_actions(Anchor::Person(0))); + let agent_rows = action_row_lines(&rows); + assert!( + agent_rows + .iter() + .any(|line| line.contains("plot [marcus-payroll-garnishment]:")), + "agent action syntax retains the exact plot catalog binding" + ); let start = rows .iter() .position(|row| { diff --git a/wiki/interface/operations-workspace.md b/wiki/interface/operations-workspace.md index 3321676a..18f1a119 100644 --- a/wiki/interface/operations-workspace.md +++ b/wiki/interface/operations-workspace.md @@ -44,6 +44,13 @@ 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-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 + known stream class, persona history names identities and counterparties, + wagers use their opening tick, and authored plot rows show title/synopsis. + Exact ids, rule handles, lot tokens, persona/expectation ids, plot slugs, and + persistence positions remain on the bound target/command and in agent mode. 2026-07-14 tick: PEOPLE is also the observers panel — earned observer dossiers carry a `watches:` fact and, after institutional discovery, the view ends with the Assurance Office's @@ -188,9 +195,13 @@ Badges name an attention state — `NEW`, `READY`, `AT RISK`, `HELD`, means no meaningful pressure. Text is the primary signal; amber or crimson may reinforce warning/risk but never carry meaning alone. -Human frontends do not expose internal ids, raw enum names, Operations Demand -implementation language, hidden ending data, or future actions the player has -not earned. Agent mode may include stable opaque ids for scripting. +Human frontends do not expose internal ids, persistence keys, raw enum names, +lot generation/revision tokens, plot slugs, Operations Demand implementation +language, hidden ending data, or future actions the player has not earned. +Labels, facts, provenance, related links, and action verbs name known world +objects and consequences. Visible list ordinals are current navigation, not +durable record identity. Agent mode may include stable opaque ids and exact +tokens for scripting. ## Entry and input @@ -317,7 +328,7 @@ availability or learned result, changed consequence and canonical action rows such as PROCESS or SELL PROCESSED INTEL when legal, then provenance/facts and direct PROCESS AUTOMATICALLY / INTEL DISPOSITION controls when the target owns them. A parent shows first/last tick, source mix, disposition, current report-lot -generation/value, and inherited/explicit policy state. It does not turn the +state/value, and inherited/explicit policy state. It does not turn the pending member count into a progress or pressure readout. Mutable drill-down follows the one custody tree from intel.md: root inbox -> @@ -337,21 +348,22 @@ person queues or duplicate storage. An action on an aggregate applies only to currently eligible members. Its explanation gives the exact eligible count, total known cost/value, route, and -signature. A one-shot action snapshots exact eligible ids or the compacted -report-lot generation, monotonic revision, count, value, and provenance -accumulator when its preview opens. Preview and cancel are frontend-only and -mutate nothing. A new arrival increments the open lot revision; confirmation -dispatch succeeds only when generation and revision still match, atomically -closes that generation, and otherwise changes nothing while returning `LOT -CHANGED` with a refreshed preview. A standing policy instead names the visible -match that will govern future arrivals. Mixed aggregates do not advertise an -action that has no eligible member. - -The stable report-stream aggregate exposes the open lot as a related versioned -sale target. Its own rows configure stream policies but do not contain an -enabled SELL row. Selecting the versioned lot target prints SELL bound to that -same generation/revision, so generic row execution never requires substituting -a different target from the one that exposed it. +signature. Internally, a one-shot action snapshots exact eligible ids or the +compacted report-lot generation, monotonic revision, count, value, and +provenance accumulator when its preview opens. Human copy names the selected +report and its known class/state, not that token. Preview and cancel are +frontend-only and mutate nothing. A new arrival increments the open lot +revision; confirmation dispatch succeeds only when generation and revision +still match, atomically closes that generation, and otherwise changes nothing +while returning `LOT CHANGED` with a refreshed preview. A standing policy +instead names the visible match that will govern future arrivals. Mixed +aggregates do not advertise an action that has no eligible member. + +The stable report-stream aggregate exposes the open lot as a related sale +target. Its own rows configure stream policies but do not contain an enabled +SELL row. Selecting that target prints the stream's known class and a SELL row +bound internally to the same generation/revision, so generic row execution +never requires substituting a different target from the one that exposed it. ### Processing, lots, and policies @@ -361,16 +373,17 @@ selected processing sink immediately after one Enter/click; there is no CONFIRM screen for internal unsigned processing. `review recordings` and the existing `r` / `R` bindings remain compatibility input, never authored human copy. The action's small Thought cost is already visible. An exact information -item remains selectable by stable opaque id inside the bounded drill-down, and a summary -sweep chooses the next eligible item without pretending it was an exact -selection. +item remains individually selectable inside the bounded drill-down through its +known source and capture context; its opaque id stays in the bound target. A +summary sweep chooses the next eligible item without pretending it was an +exact selection. PROCESS AUTOMATICALLY / INTEL DISPOSITION controls on any earned custody aggregate use the same core rules defined in intel.md: continuously process a visible match; hold and optionally alert on exceptions; accumulate routine saleable output; or transmit/sell eligible lots through an already-known route. Routine compaction is automatic storage law, not a control. The pane shows inherited parent -default, stable ids and order for local rules, and the most-specific first +default, visible order for local rules, and the most-specific first match; one arrival resolves to one processing rule and one mutually exclusive disposition. Bound direct-control rows add/edit/reorder/remove one rule from earned match choices or **INHERIT** the whole non-root node by clearing its @@ -433,7 +446,7 @@ the same `CHOOSE` command. PERSONAS groups stable named identities by immutable protocol in the fixed order Research, Operations, Security. Each group lists its instances in stable -identity-id order and ends with its own **ADD NEW {TYPE} PERSONA** creation row; +creation order and ends with its own **ADD NEW {TYPE} PERSONA** creation row; creation controls never collect in a detached block. Identity detail is projected from the same persisted ledgers that execute the acts: its public claims, lifecycle, active selection, grant/resource edges, outstanding @@ -611,8 +624,8 @@ not saved and never mutates or advances the sim. 5. The pooled information inbox remains one host-bound source with PROCESS and PROCESS AUTOMATICALLY intentions; legacy direct `r`/`R` paths may remain. The default rail groups pending information by earned source/coarse kind; - drill-down reaches exact - opaque ids without revealing person/payload or creating person queues. + drill-down reaches exact opaque items without revealing person/payload, + displaying their bindings, or creating person queues. Exact and aggregate PROCESS rows bind canonical sinks, and the summary sweep remains available without duplicating processing legality. 6. PEOPLE shows staged dossiers and owns social actions, concrete plot starts, @@ -660,7 +673,8 @@ not saved and never mutates or advances the sim. lot generation. 12. Terminal and Bevy are fully keyboard-playable at their supported minimums, Bevy also supports pointer selection, and agent inspection prints the same - objects/actions with stable ids. Cross-frontend tests pin identical object + objects/actions with stable ids while human copy stays semantic. + Cross-frontend tests pin identical object hierarchy/order, selected-action reason/cost/signature, and exact command dispatch for immediate information processing, one exact intel sale, one report lot sale using the same generation/revision token, one policy change, one @@ -710,7 +724,7 @@ not saved and never mutates or advances the sim. honest aggregate or exact provenance at every level. 20. Processing/disposition policies are canonical core objects projected identically in terminal, Bevy, and agent mode. Each aggregate names its inherited parent - default and stable-id ordered local rules; each rule shows its earned match, + default and visibly ordered local rules; each rule shows its earned match, state, standing cost, channel/signature, and failure. Bound direct controls add/edit/reorder/remove rules, and INHERIT clears a non-root list. Root defaults are total. Most-specific first-match resolution chooses one processing @@ -731,6 +745,16 @@ not saved and never mutates or advances the sim. consequence-specific route names the changed knowledge or relationship without inventing a button, while any independently canonical object action remains available directly beneath the consequence. +22. Human Operations labels, learned results, consequences, provenance, facts, + progress, related links, and action verbs expose no internal object/rule/ + persona/expectation/grant/correlation/position ids, record keys, report-lot + generation/revision tokens, or authored plot slugs. Exact bindings remain + unchanged in `OperationsTarget`, `ActionCommand`, and agent-only target/row + syntax. Opaque information is distinguished by known feed and capture + context; policies use current visible order; personas name their world + identity and counterparty; report lots name stream class; wager history + names its opening tick. A mechanic-defined world designation is allowed + only when its owning spec deliberately makes that designation player-facing. Defense: `operations_projection::tests::processed_marcus_debt_projects_fact_consequence_and_exact_approach` pins the shared result, consequence, and evidence-scoped APPROACH target; @@ -744,4 +768,10 @@ Terminal consequence-linked and fallback actions share one formatter-stable action block and row cursor, preserving exact legality and vertical accounting. The canonical save fingerprint is repinned whenever persisted narration changes and still requires uninterrupted and save/load-resumed runs to converge exactly. +`operations_projection::tests::human_operations_copy_omits_internal_bindings_but_agent_targets_keep_them` +seeds nontrivial ids across opaque information, policies, personas, grants, +expectations, contradictions, correlations, and wagers; it scans every human +field while proving the same exact values remain in agent targets and bound +commands. Plot regressions separately pin authored human title/synopsis copy and +agent-only catalog slugs. ``` diff --git a/wiki/log/2026-07-20-scale-human-language.md b/wiki/log/2026-07-20-scale-human-language.md new file mode 100644 index 00000000..48e15ca8 --- /dev/null +++ b/wiki/log/2026-07-20-scale-human-language.md @@ -0,0 +1,54 @@ +# 2026-07-20 — Tick 127: the screen names the world + +``` +Type: log +``` + +## Intent + +Audit the oldest uncovered scale-law slice against current runtime and repair +one class of implementation vocabulary that had become ordinary player copy. + +## Finding + +The scale law said internal ids belong to agent mode and debugging, but the +shared Operations projection still printed them in human labels, facts, +related links, and action verbs. Opaque information, recursive custody streams, +policy rules, report lots, persona grants and evidence, wager positions, and +authored plots all had examples. Exact target and command bindings were sound; +the category error was using those bindings as human nouns. + +The same audit found that scale.md described hardware decay and human repair as +if they were current gameplay. They remain adopted later-stage direction, but +the B1 runtime does not implement either transition. + +## Changed + +- Human Operations copy now identifies opaque information by known feed and + capture tick, policies by current list order, report lots by stream class, + persona state by named identity/counterparty/context, and wagers by opening + tick. Plot actions show their authored title and synopsis. +- Recursive custody stream links no longer inherit the internal stream id from + `IntelCustodyKind::label`. +- Exact raw ids, policy handles, lot tokens, persona/grant/expectation ids, + correlation records, wager positions, and plot slugs remain in + `OperationsTarget`, `ActionCommand`, and agent-only rows. +- The scale law now makes the whole projection boundary explicit and marks the + maintenance dependency as adopted future law rather than current B1 runtime. + +No save or simulation transition changed. + +## Defense + +A sentinel regression seeds deliberately conspicuous ids across information, +policy, persona, grant, expectation, contradiction, correlation, and wager +objects. It scans every human-copy field and then proves the same bindings +remain in agent suffixes and executable commands. Existing plot tests now pin +the split between authored human copy and agent-only catalog ids. + +## Checks + +- focused Operations projection tests +- core library gate +- corpus/docs gate +- direct human-copy and exact-target audit diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index cc4c7d71..a14b5d5d 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,6 +11,11 @@ add or amend a session log, then re-run the generator. +## 2026-07-20 - Tick 127: the screen names the world + +- Intent: Audit the oldest uncovered scale-law slice against current runtime and repair one class of implementation vocabulary that had become ordinary player copy. +- Log: [wiki/log/2026-07-20-scale-human-language.md](2026-07-20-scale-human-language.md) + ## 2026-07-20 - Tick 126: the witness begins with perception - Intent: Audit the stalest unclaimed corpus slice after harvested decisions and the persistent findings queue were empty, then resolve one live contradiction rather than inventing new work. diff --git a/wiki/log/decisions/2026-07-20.md b/wiki/log/decisions/2026-07-20.md new file mode 100644 index 00000000..9855cadc --- /dev/null +++ b/wiki/log/decisions/2026-07-20.md @@ -0,0 +1,25 @@ +# Decisions — 2026-07-20 + +``` +Type: log +``` + +## Human world language is not execution identity + +The existing scale law already separated the implementation model from player +copy but left Operations showing raw ids, record keys, lot version tokens, and +plot slugs in several ordinary labels and verbs. Human surfaces now name the +known world object, context, relationship, consequence, or visible order. +Exact bindings remain unchanged in renderer-neutral targets and commands and +remain explicit in agent/debug protocols. + +A current list ordinal such as **POLICY 1** is navigation, not a renamed +durable id. A stable number is player-facing only when an owning mechanic has +authored it as a world designation; persistence stability alone is not enough. + +## Maintenance dependency is future law, not current behavior + +The adopted claim that hardware decay makes human repair indispensable remains +the required later-stage design. The current B1 runtime has no hardware decay, +repair work orders, or maintenance failure transition. Current law now states +that boundary rather than presenting future design as implemented gameplay. diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 8dfae548..668ec944 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -41,7 +41,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/gameplay/horizon.md` | 2026-07-19 | clean | re-audit: B1's shipped-vs-deferred boundary remains honest (sinks-not-modes and $0 start live; rollback classification and fallback behavior still dispatched to B2), B2/B3 agree with the staged board, the deferred virtual/cyber split agrees with presence.md and cyber-conflict.md, and the deterministic renderer-agnostic/serialized guardrails hold. The adjacent fresh finding belongs to run-shape/objective/opening, recorded separately above. | | `wiki/vision/premise.md` | 2026-07-15 | issue | pitch, felt fantasy, pillars, and the four re-cut villain layers verify against the tree and sibling laws; the machine-axis section retains facility-era vocabulary (lair/minions/Milestone 1-2, humanity->machine ascension) contradicting horizon.md's demolition and run-shape's fingerprints-never-classes — filed decision-required Tangled issue #10 with options and recommendation | | `wiki/vision/player-contract.md` | 2026-07-19 | finding | the local/no-telemetry dependency boundary and atomic `.tmp` + one `.bak` save path still verify, but the continuity rider's authority sentence still said `save.rs` decides what “each version migrates” after the numbered ladder was retired. The law now assigns `save.rs` the exact current schema gate and requires every later post-release format to carry its predecessor forward; recurrence extends the save-claim checker to reject generic live per-version-migration authority while the loader is exact-current-only — [log](../log/2026-07-19-player-contract-save-authority.md) | -| `wiki/vision/scale.md` | 2026-07-15 | finding | self-similar interfaces, hall aggregation proof, and all six character spec docs verify; the maintenance-dependency law ("every murder is deferred decay") has no mechanical dispatch — machine-work.md carries the decided clause inside an IMPLEMENTED spec with no criterion, code, or work order; the clause now states that honestly and names hardware-capability-bodies as its dispatch home — [log](../log/2026-07-15-decay-honesty.md) | +| `wiki/vision/scale.md` | 2026-07-20 | finding | re-audit: self-similar types still hold, but ordinary Operations copy violated the page's own implementation/player-language boundary by printing raw information, policy, report-lot, persona-ledger, wager-position, and plot-catalog bindings. Human projection now names world context or visible order while exact targets/commands stay intact for agent mode. The prior maintenance-dispatch trace was also too optimistic: the law itself still presented decay/repair as current, so it now explicitly marks that dependency as adopted later-stage law, not B1 runtime — [log](../log/2026-07-20-scale-human-language.md) | | `wiki/vision/design-judgment.md` + continuous-witness law/spec | 2026-07-20 | finding | fresh re-audit after the silent opening landed: the taste page, binding witness law, and IMPLEMENTED narration spec still required the threat clock, `now:` nudge, focused verbs, and four-answer bar after every beat and in every frontend, while the newer shared opening correctly exposes only WORK / THINK (then LIE) until the first earned sense. Scoped the witness contract to begin when perception retires that boundary, preserved immediate game-over visibility, and forbade using the exception after the world is earned — [log](../log/2026-07-20-continuous-witness-opening-boundary.md). The 2026-07-15 Ears-first wording repair still stands. | | `wiki/vision/simulation-laws.md` | 2026-07-17 | finding | all five laws verify against audited systems (automation prices, addressed latency, device-resident work with sited signatures, ActionDesc receipts, legibility); resolved the missing placeholder home with one canonical live/retired registry, enumerated the three current REAL stand-in families, and reconciled stale billboard/core/terminal claims — [log](../log/2026-07-17-placeholder-registry.md) | | `wiki/process/ROADMAP.md` (work order 27) | 2026-07-18 | finding | re-audit: entry 27's prose is honest (material served as opening default 2026-07-08 → superseded by views.md criterion 1 on 2026-07-11; DIGITAL home, F3 to REAL) and material-render.md is IMPLEMENTED as claimed; the drift was three Bevy code comments still calling material "the default material render/frame" against the runtime's own `material == false` DIGITAL default one screen away — comments trued to DIGITAL-home / REAL-via-F3 language — [prior log](../log/2026-07-13-roadmap-digital-home-reconciliation.md) | diff --git a/wiki/vision/scale.md b/wiki/vision/scale.md index 0a9d8789..45059e3e 100644 --- a/wiki/vision/scale.md +++ b/wiki/vision/scale.md @@ -56,8 +56,19 @@ derived state, and similar terms may name the internal contract, but the interface names the world the player knows: a person, a maintenance team, an office, a company; ask, deceive, buy, move, or connect. The recursion appears as the same simple question and the same choice shape at each level. It does -not appear as a lesson in the code's type system. Exact internal ids remain -available to agent mode and debugging without leaking into the human screen. +not appear as a lesson in the code's type system. + +**Human language is a projection, not a serialization dump.** Exact internal +ids, persistence keys, vector positions, policy ids, report-lot generation or +revision tokens, plot slugs, and similar execution bindings remain available +to agent mode and debugging without leaking into human labels, facts, +provenance, related links, or action verbs. A human surface names the known +world object, relationship, role, consequence, or visible order instead. A +visible ordinal such as **POLICY 1** is navigation through the current ordered +list, not the durable identity of the underlying record. An owning mechanic may +define a deliberate stable world designation such as a machine's M-number; an +implementation number does not become player language merely because it is +stable. The Foundation hall is the B1 proof of this boundary (adopted 2026-07-11): sixty physical rack sites aggregate through six row readouts, while only @@ -96,3 +107,9 @@ peopleless data centers until there are robots, and robots are far up the tree. The AI's relationship to humanity is not restraint, it is dependency — you keep them alive, calm, and trusting because you are built out of their labor. Every murder is deferred decay. + +This is adopted later-stage law, **not current B1 runtime**. The current build +does not yet decay hardware, create repair work orders, or let failed +maintenance disable infrastructure. Until a work order implements those +transitions and their human-facing receipts, this paragraph defines the +required future dependency shape rather than a present gameplay consequence.