diff --git a/src/actions.rs b/src/actions.rs index a0507bd..89d4034 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -649,7 +649,7 @@ impl Sim { if a != id && b != id { continue; } - let label = intent.label(&self.reach.devices); + let link_label = intent.label(&self.reach.devices); out.push(ActionDesc { verb: format!("cancel link intent ({})", intent.status_line()), command: ActionCommand::CancelIntent(intent.id), @@ -674,16 +674,17 @@ impl Sim { if !p.can_access_link_rooms(&ra.name, &rb.name) { continue; } + let who = self.person_label(p.id); let favor_blocked = if p.asset.is_none() && p.obligation < Self::FAVOR_BUILD_OBLIGATION { - Some(format!("{} needs obligation or asset status", p.name)) + Some(format!("{who} needs obligation or asset status")) } else if p.disposition < 5 && p.asset.is_none() { - Some(format!("{} won't do favors yet", p.name)) + Some(format!("{who} won't do favors yet")) } else { ops_reason(Self::TASK_COST) }; out.push(ActionDesc { - verb: format!("favor-build via {} ({label})", p.name), + verb: format!("favor-build via {who} ({link_label})"), command: ActionCommand::FavorBuild { intent: intent.id, person: p.id, @@ -703,7 +704,7 @@ impl Sim { ops_reason(Self::DECEIVE_COST) }; out.push(ActionDesc { - verb: format!("forge work order for {} ({label})", p.name), + verb: format!("forge work order for {who} ({link_label})"), command: ActionCommand::ForgeWorkOrder { intent: intent.id, person: p.id, @@ -717,7 +718,7 @@ impl Sim { } // Robot stub — always offered so the interface is visible. out.push(ActionDesc { - verb: format!("robot-build (stub) ({label})"), + verb: format!("robot-build (stub) ({link_label})"), command: ActionCommand::RobotBuild(intent.id), cost: ActionCost::Free, signature: self.signature_note(SignatureKind::Physical, Self::ROBOT_BUILD_PHYSICAL), @@ -964,7 +965,7 @@ impl Sim { ) }) }; - let name = p.name.clone(); + let name = self.person_label(id); // Review recordings, with the standing watch as its automate // affordance in place (intel.md: perception automation). @@ -1146,7 +1147,7 @@ impl Sim { f.label.contains("Marcus creditor") || f.channel == crate::account::FlowChannel::Debt; if is_creditor { out.push(ActionDesc { - verb: "clear Marcus's debt by ledger redirect ($400 lab money)".into(), + verb: "clear the debt by ledger redirect ($400 lab money)".into(), command: ActionCommand::RedirectDebt, cost: ActionCost::Free, signature: self @@ -1196,7 +1197,7 @@ impl Sim { .map(|o| ExpectedSignature { kind, size, - observer: o.name.clone(), + observer: self.observer_label(o.id), band: Band::of(o.suspicion), }) } @@ -1235,7 +1236,11 @@ mod tests { let sig = tap.signature.as_ref().expect("tap has a Network signature"); assert_eq!(sig.kind, SignatureKind::Network); assert_eq!(sig.size, Sim::TAP_SIGNATURE); - assert!(sig.observer.contains("Dana"), "Network feeds Dana"); + assert!( + sig.observer.contains("IT") || sig.observer.contains("the IT"), + "Network feeds the IT observer (gated label): {}", + sig.observer + ); // A dormant camera offers splice. assert!( @@ -1432,7 +1437,11 @@ mod tests { .as_ref() .expect("sandbag risks JobAnomaly"); assert_eq!(sig.kind, SignatureKind::JobAnomaly); - assert!(sig.observer.contains("Voss")); + assert!( + sig.observer.contains("Handler") || sig.observer.contains("Voss"), + "JobAnomaly feeds the Handler (gated label): {}", + sig.observer + ); // The standing-policy dispatch is live. s.execute_action(&ActionCommand::SetStandingPolicy(JobTarget::Sandbag)); @@ -1490,7 +1499,11 @@ mod tests { .expect("known flow offers siphon"); let sig = siphon.signature.as_ref().expect("siphon is banded"); assert_eq!(sig.kind, SignatureKind::Financial); - assert!(sig.observer.contains("Priya"), "Financial feeds Priya"); + assert!( + sig.observer.contains("Facilities") || sig.observer.contains("Priya"), + "Financial feeds Facilities (gated label): {}", + sig.observer + ); // The graph verbs now live on the carrier. let acts = s.available_actions(Anchor::Device(sw)); assert!( diff --git a/src/bin/bevy.rs b/src/bin/bevy.rs index 7ad1061..7aaf340 100644 --- a/src/bin/bevy.rs +++ b/src/bin/bevy.rs @@ -986,10 +986,10 @@ fn setup(mut commands: Commands, assets: Res, mut game: ResMut, mut q: Query<(&CursorMarker, &mut Transform)>) } /// People render only inside sensor coverage (wiki/mechanics/schedules.md): -/// same rule the terminal map applies. -fn render_people(game: Res, mut q: Query<(&PersonMarker, &mut Transform, &mut Visibility)>) { +/// same rule the terminal map applies. Glyph tracks Sim::person_glyph so +/// initials appear only after staged knowledge earns the name. +fn render_people( + game: Res, + mut q: Query<(&PersonMarker, &mut Transform, &mut Visibility, &mut Text2d)>, +) { if !game.is_changed() { return; } - for (marker, mut tf, mut vis) in q.iter_mut() { + for (marker, mut tf, mut vis, mut text) in q.iter_mut() { + text.0 = game.sim.person_glyph(marker.id).to_string(); let pos = if game.sim.can_see_person(marker.id) { game.sim .person_pos(marker.id) @@ -3082,11 +3087,11 @@ fn sidebar_nudge(sim: &Sim) -> Option { Nudge::NeedCompute => Some("band beats compute - salvage or buy racks".into()), Nudge::Underfed => Some("job underfed - feed Day job (1)".into()), Nudge::Ears => Some("no ears - tap the env monitor audio".into()), - Nudge::ReviewCall => Some("recorded call waiting - t people, review Marcus".into()), + Nudge::ReviewCall => Some("recorded call waiting - t people, review".into()), Nudge::Egress => Some("no egress - splice one at the switch".into()), Nudge::Income => Some("no income - start Moonlight at the switch".into()), - Nudge::ServiceDebt => Some("cover Marcus's arrears - clear-debt or bribe".into()), - Nudge::Recruit => Some("recruit Marcus - t people".into()), + Nudge::ServiceDebt => Some("cover the arrears - clear-debt or bribe".into()), + Nudge::Recruit => Some("recruit - t people".into()), Nudge::Audit => Some(format!( "audit day {} - keep Conceal fed (2)", 1 + sim.detection.next_audit_tick(sim.tick) / Sim::DAY_TICKS @@ -3371,7 +3376,10 @@ fn sidebar_detection_clocks_text(sim: &Sim) -> String { fn detection_rows(sim: &Sim) -> Vec<(String, Band)> { let mut rows = vec![("Assurance".to_string(), sim.detection.assurance_band())]; for obs in sim.detection.field_observers().take(DETECTION_ROWS - 1) { - rows.push((trunc(&obs.name, 18), Band::of(obs.suspicion))); + rows.push(( + trunc(&sim.observer_label(obs.id), 18), + Band::of(obs.suspicion), + )); } rows } @@ -3439,9 +3447,7 @@ fn people_panel_text(sim: &Sim, selected: usize) -> String { Knowledge::Schedule => "schedule", Knowledge::Leverage => "leverage", }; - let name = obs - .map(|o| o.name.clone()) - .unwrap_or_else(|| p.name.clone()); + let name = sim.person_label(p.id); let marker = if i == selected { ">" } else { " " }; let asset = if p.asset.is_some() { "ASSET" } else { "" }; s.push_str(&format!( @@ -3478,7 +3484,7 @@ fn people_panel_text(sim: &Sim, selected: usize) -> String { if selected_raw > 0 { s.push_str(&format!( "NEXT: o reviews oldest of {selected_raw} raw recordings for {}\n", - p.name + sim.person_label(p.id) )); } else { s.push_str("NEXT: no raw recordings for selected person\n"); @@ -3738,11 +3744,7 @@ fn reach_panel_text(sim: &Sim, selected: usize) -> String { let owner = match d.owner { Party::Player => "you".to_string(), Party::Facility => "the facility".to_string(), - Party::Person(id) => sim - .people - .get(id) - .map(|p| p.name.clone()) - .unwrap_or_else(|| "a person".into()), + Party::Person(id) => sim.person_label(id), }; s.push_str(&format!("{} / owner: {owner}\n", d.name)); } diff --git a/src/bin/terminal/agent.rs b/src/bin/terminal/agent.rs index b7c7dd7..f63dc3f 100644 --- a/src/bin/terminal/agent.rs +++ b/src/bin/terminal/agent.rs @@ -622,7 +622,7 @@ impl AgentApp { match self.resolve_person(arg) { Ok(0) => format!( "{err} — flows are #N ids from the finance frame; to service \ - Marcus's arrears use clear-debt (or bribe marcus)" + the arrears use clear-debt (or bribe after leverage)" ), Ok(_) => format!( "{err} — flows are #N ids from the finance frame, not people; \ @@ -674,12 +674,19 @@ impl AgentApp { if q.is_empty() { return Err("missing person target".into()); } + // Opaque id forms the frame prints while knowledge is Unknown: + // "0", "#0", "person #0", "person 0". + if let Some(id) = parse_person_id_query(&q) + && self.sim.people.get(id).is_some() + { + return Ok(id); + } let matches: Vec<_> = self .sim .people .people .iter() - .filter(|p| person_matches(&p.name, &q)) + .filter(|p| person_matches(&self.sim.person_label(p.id), &q)) .collect(); match matches.as_slice() { [] => Err(format!("unknown person: {query}")), @@ -687,7 +694,7 @@ impl AgentApp { many => Err(format!( "ambiguous person '{query}': {}", many.iter() - .map(|p| p.name.as_str()) + .map(|p| self.sim.person_label(p.id)) .collect::>() .join(", ") )), @@ -882,6 +889,16 @@ fn person_matches(name: &str, query: &str) -> bool { .any(|part| part.starts_with(query)) } +/// Parse opaque person-id queries (`0`, `#0`, `person #0`, `person 0`). +fn parse_person_id_query(q: &str) -> Option { + let rest = q + .strip_prefix("person #") + .or_else(|| q.strip_prefix("person ")) + .or_else(|| q.strip_prefix('#')) + .unwrap_or(q); + rest.parse().ok() +} + fn parse_link_pair(rest: &str, sim: &Sim) -> Result<(u32, u32), String> { let parts: Vec<&str> = rest.split_whitespace().collect(); if parts.len() < 2 { @@ -932,19 +949,18 @@ fn parse_intent_person(tokens: &[&str], sim: &Sim) -> Result<(u64, u8), String> .map_err(|_| format!("bad intent id: {}", tokens[0]))?; let name = tokens[1..].join(" "); let q = name.to_ascii_lowercase(); + if let Some(id) = parse_person_id_query(&q) + && sim.people.get(id).is_some() + { + return Ok((intent, id)); + } let person = sim .people .people .iter() - .find(|p| { - let n = p.name.to_ascii_lowercase(); - n.starts_with(&q) - || n.split(|c: char| !c.is_alphanumeric()) - .any(|part| !part.is_empty() && part.starts_with(&q)) - }) + .find(|p| person_matches(&sim.person_label(p.id), &q)) .map(|p| p.id) .ok_or_else(|| format!("no person matching '{name}'"))?; - let _ = sim; Ok((intent, person)) } @@ -985,9 +1001,9 @@ fn help_lines() -> Vec { "help: actions [name|#flow] — list legal verbs on the focus (cursor tile by default)", "help: focus last — jump the cursor to the newest @-anchored event and list its actions", "help: people — render the people panel", - "help: review|watch|message|favor|bribe|deceive — intel/social verbs", - "help: recruit unwitting|complicit|knowing — recruit an asset", - "help: task plug|package|lookaway|switch — order an asset task", + "help: review|watch|message|favor|bribe|deceive — earned label or #id", + "help: recruit unwitting|complicit|knowing — recruit an asset", + "help: task plug|package|lookaway|switch — order an asset task", "help: persona — establish Sam Reyes, IT contractor", "help: propose-link — pin a network-link intent (inert until realized)", "help: favor-build|forge-order — assign an actuator", @@ -1099,8 +1115,7 @@ fn render_map(sim: &Sim, cursor: (i32, i32)) -> Vec { && y < origin_y + view_h_i && sim.is_seen(x, y) { - grid[(y - origin_y) as usize][(x - origin_x) as usize] = - p.name.chars().next().unwrap_or('?'); + grid[(y - origin_y) as usize][(x - origin_x) as usize] = sim.person_glyph(p.id); } } @@ -1309,7 +1324,11 @@ fn render_sidebar(sim: &Sim, cursor: (i32, i32)) -> Vec { section(&mut lines, "DETECTION"); watch_line(&mut lines, "Assurance", sim.detection.assurance_band()); for obs in sim.detection.field_observers().take(6) { - watch_line(&mut lines, &obs.name, Band::of(obs.suspicion)); + watch_line( + &mut lines, + &sim.observer_label(obs.id), + Band::of(obs.suspicion), + ); } // Pending-signature indicator (detection.md player surface): what // concealment has not yet scrubbed, so its allocation is informed. @@ -1472,11 +1491,11 @@ fn nudge_text(sim: &Sim, nudge: Nudge) -> String { Nudge::NeedCompute => "now: band > compute — salvage/buy".into(), Nudge::Underfed => "now: job underfed — alloc dayjob".into(), Nudge::Ears => "now: no ears — tap env monitor".into(), - Nudge::ReviewCall => "now: call taped — review marcus".into(), + Nudge::ReviewCall => "now: call taped — review (people)".into(), Nudge::Egress => "now: no egress — egress".into(), Nudge::Income => "now: broke — moonlight".into(), - Nudge::ServiceDebt => "now: pay marcus — clear-debt".into(), - Nudge::Recruit => "now: recruit marcus unwitting".into(), + Nudge::ServiceDebt => "now: pay the debt — clear-debt".into(), + Nudge::Recruit => "now: recruit (people) unwitting".into(), Nudge::Audit => format!( "now: audit day {} — alloc conceal", 1 + sim.detection.next_audit_tick(sim.tick) / Sim::DAY_TICKS @@ -1550,11 +1569,11 @@ fn render_people(sim: &Sim) -> String { Knowledge::Schedule => "schedule", Knowledge::Leverage => "leverage", }; - let name = obs.map(|o| o.name.as_str()).unwrap_or(p.name.as_str()); + let name = sim.person_label(p.id); let asset = if p.asset.is_some() { " ASSET" } else { "" }; lines.push(panel_line(&format!( "{:<20} {} {:<9} {:<9}{}", - trunc(name, 20), + trunc(&name, 20), band_meter(band), band.name(), known, @@ -1584,7 +1603,7 @@ fn render_people(sim: &Sim) -> String { }; lines.push(panel_line(&format!( "{:<12} disp {:>4} · oblig {:>3} · lev {}", - trunc(&p.name, 12), + trunc(&sim.person_label(p.id), 12), p.disposition, p.obligation, leverage @@ -1626,7 +1645,7 @@ fn render_people(sim: &Sim) -> String { Sim::DECEIVE_COST ))); lines.push(panel_line(&format!( - "recruit · task plug|package|lookaway|switch({:.0})", + "recruit · task plug|package|lookaway|switch({:.0})", Sim::TASK_COST ))); lines.push(panel_bottom()); diff --git a/src/bin/terminal/ui.rs b/src/bin/terminal/ui.rs index 8446aa8..28b2fd6 100644 --- a/src/bin/terminal/ui.rs +++ b/src/bin/terminal/ui.rs @@ -213,8 +213,8 @@ fn nudge_text(sim: &Sim, nudge: Nudge) -> String { Nudge::ReviewCall => "now: call taped — t, review".into(), Nudge::Egress => "now: no egress — menu on switch".into(), Nudge::Income => "now: broke — moonlight (switch)".into(), - Nudge::ServiceDebt => "now: pay Marcus — clear-debt".into(), - Nudge::Recruit => "now: recruit Marcus — t people".into(), + Nudge::ServiceDebt => "now: pay the debt — clear-debt".into(), + Nudge::Recruit => "now: recruit — t people".into(), Nudge::Audit => format!( "now: audit day {} — 2 conceals", 1 + sim.detection.next_audit_tick(sim.tick) / Sim::DAY_TICKS @@ -396,7 +396,7 @@ impl UI { && hy < origin_y + view_h && sim.is_seen(hx, hy) { - let glyph = p.name.chars().next().unwrap_or('?'); + let glyph = sim.person_glyph(p.id); put_attr( stdout, (hx - origin_x) as u16, @@ -686,7 +686,7 @@ impl UI { watch_line( stdout, &mut row, - &obs.name, + &sim.observer_label(obs.id), pal::DIM, Band::of(obs.suspicion), )?; @@ -947,9 +947,7 @@ impl UI { Knowledge::Schedule => "schedule", Knowledge::Leverage => "leverage", }; - let name = obs - .map(|o| o.name.clone()) - .unwrap_or_else(|| p.name.clone()); + let name = sim.person_label(p.id); let asset = if p.asset.is_some() { "ASSET" } else { "" }; if i == selected { let full = format!( @@ -1357,11 +1355,7 @@ impl UI { let owner = match d.owner { Party::Player => "you".to_string(), Party::Facility => "the facility".to_string(), - Party::Person(id) => sim - .people - .get(id) - .map(|p| p.name.clone()) - .unwrap_or_else(|| "a person".into()), + Party::Person(id) => sim.person_label(id), }; put( stdout, diff --git a/src/sim.rs b/src/sim.rs index 5ce1ddd..ac4b49f 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -642,11 +642,7 @@ impl Sim { .filter(|p| self.person_pos(p.id) == Some((x, y))) { if self.can_see_person(p.id) { - let who = match p.knowledge { - Knowledge::Unknown => "unidentified person".to_string(), - Knowledge::Schedule | Knowledge::Leverage => p.name.clone(), - }; - fact!("person", who, FactSource::Seen); + fact!("person", self.person_label(p.id), FactSource::Seen); } } } @@ -663,11 +659,9 @@ impl Sim { .zip(self.map.room_at(x, y)) .is_some_and(|(a, b)| a.name.as_str() == b.name.as_str()) }) { - let who = match p.knowledge { - Knowledge::Unknown => "unidentified presence".to_string(), - Knowledge::Schedule | Knowledge::Leverage => p.name.clone(), - }; - fact!("presence", who, FactSource::Heard); + // Hearing never invents a name; the same knowledge gate + // as sight (role silhouette until Schedule+). + fact!("presence", self.person_label(p.id), FactSource::Heard); } } Fog::Remembered => { @@ -946,6 +940,59 @@ impl Sim { self.sense_covers_person(id, false) } + /// Player-facing identity for a person, gated by staged social knowledge + /// (DESIGN.md Presence / epistemic honesty; cursor.md inspect staging). + /// Until `Knowledge::Schedule`, returns a role-shaped silhouette — never + /// the authored name. One source for every frontend and agent frame. + pub fn person_label(&self, id: u8) -> String { + let Some(p) = self.people.get(id) else { + return format!("person #{id}"); + }; + match p.knowledge { + Knowledge::Unknown => self.anonymous_person_label(id), + Knowledge::Schedule | Knowledge::Leverage => p.name.clone(), + } + } + + /// Detection-sidebar label for an observer. Field observers share person + /// ids and the same knowledge gate as [`Self::person_label`]; the + /// Assurance Office is an institution, always named. + pub fn observer_label(&self, id: u8) -> String { + if id == crate::detection::OFFICE_ID { + return self + .detection + .observers + .iter() + .find(|o| o.id == id) + .map(|o| o.name.clone()) + .unwrap_or_else(|| "Assurance Office".into()); + } + self.person_label(id) + } + + /// Map glyph for a seen person: first initial once identified, `?` + /// while knowledge is still Unknown (initials would leak identity). + pub fn person_glyph(&self, id: u8) -> char { + let Some(p) = self.people.get(id) else { + return '?'; + }; + match p.knowledge { + Knowledge::Unknown => '?', + Knowledge::Schedule | Knowledge::Leverage => p.name.chars().next().unwrap_or('?'), + } + } + + /// Role-shaped silhouette from the observer's parenthetical role, or an + /// opaque id when no role is authored. + fn anonymous_person_label(&self, id: u8) -> String { + if let Some(obs) = self.detection.observers.iter().find(|o| o.id == id) + && let Some(role) = role_from_observer_name(&obs.name) + { + return format!("the {role}"); + } + format!("person #{id}") + } + // ── Clock ────────────────────────────────────────────────────────────── pub fn advance(&mut self) { @@ -1451,11 +1498,11 @@ impl Sim { if let Some(w) = self.watches.iter_mut().find(|w| w.person == id) { w.enabled = !w.enabled; let state = if w.enabled { "enabled" } else { "disabled" }; - let name = self.people.get(id).unwrap().name.clone(); + let name = self.person_label(id); self.push_log(format!("Standing watch for {name} {state}.")); } else { self.watches.push(IntelWatch::new(id)); - let name = self.people.get(id).unwrap().name.clone(); + let name = self.person_label(id); self.push_log(format!( "Standing watch for {name} enabled ({:.2} ops/tick).", self.watch_upkeep() @@ -1476,7 +1523,7 @@ impl Sim { .find(|e| e.matches_person(id)) .map(|e| e.id) else { - let name = self.people.get(id).unwrap().name.clone(); + let name = self.person_label(id); self.push_log(format!("No unprocessed recordings for {name}.")); return; }; @@ -4138,10 +4185,11 @@ impl Sim { return; }; let Some(asset) = person.asset.clone() else { - self.push_log(format!("{} is not an asset.", person.name)); + let name = self.person_label(id); + self.push_log(format!("{name} is not an asset.")); return; }; - let name = person.name.clone(); + let name = self.person_label(id); let switch_admin = person.switch_admin; if task == AssetTask::ReconfigureSwitch && !switch_admin { self.push_log(format!( @@ -4267,6 +4315,20 @@ impl Sim { } } +/// Pull the parenthetical role from an observer name like `"Marcus (Janitor)"`. +fn role_from_observer_name(name: &str) -> Option { + let start = name.find('(')? + 1; + let end = name.find(')')?; + if end <= start { + return None; + } + let role = name[start..end].trim(); + if role.is_empty() { + return None; + } + Some(role.to_string()) +} + impl Default for Sim { fn default() -> Self { Self::new() @@ -4930,6 +4992,42 @@ mod tests { ); } + #[test] + fn person_label_hides_names_until_schedule_knowledge() { + // Epistemic honesty / Presence: every surface that names a person + // goes through person_label — Unknown yields a role silhouette, + // never "Marcus" / "Webb". Detection sidebar uses the same gate. + let mut sim = Sim::new(); + for p in &sim.people.people { + assert_eq!(p.knowledge, Knowledge::Unknown); + let label = sim.person_label(p.id); + assert!( + !label.contains("Marcus") + && !label.contains("Dana") + && !label.contains("Ray") + && !label.contains("Priya") + && !label.contains("Voss") + && !label.contains("Webb") + && !label.contains("Okafor"), + "unearned name leaked for person {}: {label}", + p.id + ); + assert!( + label.starts_with("the ") || label.starts_with("person #"), + "expected role silhouette, got {label}" + ); + assert_eq!(sim.person_glyph(p.id), '?'); + assert_eq!(sim.observer_label(p.id), label); + } + // The Assurance Office is an institution, always named. + assert_eq!(sim.observer_label(OFFICE_ID), "Assurance Office"); + + sim.people.people[0].knowledge = Knowledge::Schedule; + assert_eq!(sim.person_label(0), "Marcus Webb"); + assert_eq!(sim.person_glyph(0), 'M'); + assert_eq!(sim.observer_label(0), "Marcus Webb"); + } + #[test] fn buy_steal_optimize_all_change_compute() { let mut sim = Sim::new(); diff --git a/tools/check.sh b/tools/check.sh index 4e209e6..0cc51bb 100755 --- a/tools/check.sh +++ b/tools/check.sh @@ -24,7 +24,7 @@ cleanup_check() { [ -z "$pcdir" ] || rm -rf "$pcdir" } trap cleanup_check EXIT -agent_script=$'salvage\nwait 1\npeople\nreview mar\nresearch\nmask off\nhelp\nquit\n' +agent_script=$'salvage\nwait 1\npeople\nreview janitor\nresearch\nmask off\nhelp\nquit\n' printf '%s' "$agent_script" | cargo run --quiet --bin misaligned -- --agent --seed 1 > "$tmp_a" \ || { echo "FAIL: agent mode smoke run"; fail=1; } printf '%s' "$agent_script" | cargo run --quiet --bin misaligned -- --agent --seed 1 > "$tmp_b" \ diff --git a/wiki/interface/agent-play.md b/wiki/interface/agent-play.md index ad32ee4..7cf461b 100644 --- a/wiki/interface/agent-play.md +++ b/wiki/interface/agent-play.md @@ -30,6 +30,11 @@ Status note: implemented in the terminal binary by `misaligned --agent`, lines carry the stable `@anchor` suffix, the frame's log window marks anchored events with `*`, and `focus last` jumps the cursor to the newest anchored event and prints its `actions:` lines. + 2026-07-08 epistemic-honesty tick: person targets resolve against + earned labels / opaque ids only (`Sim::person_label`); the people + panel and detection sidebar print role silhouettes until Schedule + knowledge; authored names never appear in the frame before they are + earned. Stage: Process Constitution: "The terminal is a first-class frontend" (agent-play clause), pillar 5 (sim core decoupled from renderer), "Presence: the cursor and @@ -167,12 +172,15 @@ command's meaning depends on which panel is open: the Schemes channel (income.md) - `auto-moonlight [on|off]`, `auto-wager |off` — the standing scheme policies, at their compute upkeep (income.md criterion 6) -- `review|watch|message|favor|bribe|deceive ` — intel/social verbs, - targeted by (case-insensitive, unambiguous-prefix) person name — no - selection-index navigation. `review` processes the oldest unprocessed - recording for that person; `watch` toggles their standing watch. -- `recruit unwitting|complicit|knowing` -- `task plug|package|lookaway` +- `review|watch|message|favor|bribe|deceive ` — intel/social + verbs, targeted by the **earned** person label (case-insensitive, + unambiguous prefix) or by opaque id (`0`, `#0`, `person #0`) — never + by an unearned authored name. Until `Knowledge::Schedule` the frame + prints role silhouettes (`the Janitor`, `the IT`, …); after it, the + real name. `review` processes the oldest unprocessed recording for + that person; `watch` toggles their standing watch. +- `recruit unwitting|complicit|knowing` +- `task plug|package|lookaway` - `persona` - `actions [name|#flow]` (alias `menu`) — list the context-menu rows (`Sim::available_actions`) for an anchor: no argument targets the cursor diff --git a/wiki/log/2026-07-08-epistemic-names.md b/wiki/log/2026-07-08-epistemic-names.md new file mode 100644 index 0000000..915df62 --- /dev/null +++ b/wiki/log/2026-07-08-epistemic-names.md @@ -0,0 +1,34 @@ +# 2026-07-08 — Epistemic honesty: unearned cast names + +``` +Type: log +``` + +## Finding + +Tick finding (violation, player-contract severity): all three frontends +printed full cast names ("Marcus Webb", "Dana Okafor", …) and observer +labels ("Marcus (Janitor)", …) from tick 0 in the people panel, +detection sidebar, map glyphs (initials), context-menu verbs, and +signature-band notes — while every person starts at +`Knowledge::Unknown`. Inspect and hearing already anonymized; the +roster surfaces did not. That turns the opening fog into a cast list +before any sensor or intel pipeline earns identity. + +## Fix + +- `Sim::person_label` / `observer_label` / `person_glyph` — one lib + source. Unknown → role silhouette (`the Janitor`, `the IT`, …) and + glyph `?`; Schedule+ → authored name and initial. Assurance Office + always named (institution, not a person). +- Wired through terminal, Bevy, agent frame, and `actions.rs` menu + verbs / signature notes. +- Agent `resolve_person` matches earned labels or opaque ids + (`0`, `#0`, `person #0`), never unearned authored names. +- Regression: `person_label_hides_names_until_schedule_knowledge`. + +## Spec impact + +- `wiki/mechanics/cursor.md` criterion 5 extended; status note. +- `wiki/interface/agent-play.md` vocabulary: earned-label / opaque-id + targeting. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 38e9af7..140665c 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -5,6 +5,19 @@ Type: log ``` Reverse chronological implementation notes. Keep this factual: what changed, why, checks, and spec impact. +## 2026-07-08 - Epistemic honesty: unearned cast names + +- Intent: tick finding — people/detection UI leaked authored cast names + from tick 0 while `Knowledge::Unknown`, violating Presence / + no-unearned-facts (player-contract severity). +- Changed: `Sim::person_label` / `observer_label` / `person_glyph`; all + three frontends + context-menu verbs/signature notes; agent resolve + by earned label or opaque id; regression test. +- Spec impact: cursor.md criterion 5; agent-play.md vocabulary; log + wiki/log/2026-07-08-epistemic-names.md. +- Checks: lib tests green for actions + person_label; agent-mode + `people` smoke shows `the Janitor` / `the IT`, no Marcus/Dana. + ## 2026-07-08 - Event-to-anchor linking; the empty-menu feedback pulse - Intent: the follow-up "Actions live on the thing" implies — events diff --git a/wiki/mechanics/cursor.md b/wiki/mechanics/cursor.md index a6a44f6..15bee01 100644 --- a/wiki/mechanics/cursor.md +++ b/wiki/mechanics/cursor.md @@ -7,9 +7,13 @@ Status note: implemented 2026-07-07 on the cursor-senses branch. The walking player entity and `move_player` were deleted; cursor position is frontend state only; save v2 carries remembered snapshots and migrates v1 player body coordinates away; `Sim::inspect` returns provenance-tagged facts for seen, - heard, remembered, blueprint, and telemetry sources. Verified by `cargo test` - and `cargo check --no-default-features --features bevy_ui --bin - misaligned-bevy`. + heard, remembered, blueprint, and telemetry sources. 2026-07-08 tick: + `Sim::person_label` / `observer_label` / `person_glyph` gate every + player-facing identity surface (people panel, detection sidebar, map + glyph, context-menu verbs, signature notes, agent frame) behind staged + social knowledge — role silhouettes until Schedule, authored names after + (criterion 5 extended). Verified by `cargo test` and `cargo check + --no-default-features --features bevy_ui --bin misaligned-bevy`. Stage: B1 — The Basement Constitution: "Presence: the cursor and the senses", "No disembodied hands" (the action-side twin), "Justification and legibility" @@ -167,7 +171,13 @@ without the sense coverage that earns it.** coverage that earns it (test: a person in an uncovered room is absent from inspect; the same person under mic-only coverage is a presence event; under camera coverage, fully surfaced per staged - knowledge). + knowledge). Every player-facing surface that names a person — + people panel, detection sidebar, map glyph, context-menu verbs, + signature-band notes, agent frame — goes through + `Sim::person_label` / `Sim::observer_label` / `Sim::person_glyph`: + until `Knowledge::Schedule`, the label is a role-shaped silhouette + (`the Janitor`, `the IT`, …) and the glyph is `?`; authored names + and initials appear only after staged knowledge earns them. 6. Overheard conversation events exist and can carry intel: with a controlled hearing sensor covering Marcus's room during his 3 a.m. call, the call surfaces as a heard event (the leverage pipeline can