From a5ebd90ec7a5bf7f00b7f3ae18fa63f8e8381451 Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 24 Jul 2026 14:50:10 -0800 Subject: [PATCH] Say what a persona protocol opens and what it closes. PERSONAS rendered the archetype registry at the player: "legal actions: message, request, deceive, review" is PersonaActionKind verbatim, "grant route:" is PersonaGrantKind, and counterparty rows were an integrity=/recognized=/regard= field dump. Each identity and creation row now leads with the standing institutional reading, then one can: line and one cannot: line drawn from the same fixed act list in the same order, so two protocols compare directly. Standing, grants, expectations, contradictions, and correlations read as sentences naming the observer who holds them. Review is declared by the Research and Security protocols and consumed by no world system, so it is filtered out of both reach lines under the standing stub rule rather than advertising capability those archetypes cannot exercise. The visible consequence is deliberate: Security now shows a strictly smaller reach than Research, because that is what the build is. Defense: operations-workspace.md's copy law forbids raw enum names in human fields and criterion 22 requires world terms; this surface was breaking both, and the fix stays inside the one renderer-neutral projection so terminal, Bevy, and agent mode inherit it with no frontend-only path. Every exact id remains on OperationsTarget, ActionCommand, and agent rows, so human_operations_copy_omits_internal_bindings_but_agent_targets_keep_them passes unchanged. Hiding an unconsumed verb applies action-vocabulary.md's existing rule that STUB entries stay off every player surface until playable. persona_copy_names_world_acts_and_omits_unhonored_protocol_verbs pins the paired reach lines, the absent Review verb, and the removal of the four dump prefixes; the per-counterparty integrity Defense gains a clause requiring every standing phrase to name whose read it is, so it cannot go vacuous now that the word integrity is gone from human copy. No legality, grant, lifecycle, or evidence behavior changes. --- .../src/operations_projection.rs | 348 ++++++++++++++---- wiki/interface/operations-workspace.md | 35 ++ .../2026-07-24-personas-copy-legibility.md | 79 ++++ wiki/log/DEVLOG.md | 5 + 4 files changed, 386 insertions(+), 81 deletions(-) create mode 100644 wiki/log/2026-07-24-personas-copy-legibility.md diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index f07a4440..ab89d9b5 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -25,7 +25,10 @@ use crate::intel::{ }; use crate::messages::{MessageOrigin, MessagePayload, MessageStatus}; use crate::person::{Knowledge, Leverage}; -use crate::persona::{self, PersonaId, PersonaLifecycle}; +use crate::persona::{ + self, ExpectationState, PersonaActionKind, PersonaDiscovery, PersonaId, PersonaIntegrity, + PersonaLifecycle, +}; use crate::plot::{PlotRun, PlotState}; use crate::reach::Party; use crate::sim::Sim; @@ -1660,52 +1663,132 @@ impl Sim { } fn personas_view(&self) -> Vec { + // Human copy for the protocol verbs. `None` marks a verb the archetype + // registry declares but no world system honors yet; action-vocabulary.md + // keeps stub entries off every player surface until they are playable, + // so an unhonored verb never reaches a human `can:`/`cannot:` line. It + // stays on the agent-facing registry and on the bound command. + fn act_phrase(action: PersonaActionKind) -> Option<&'static str> { + match action { + PersonaActionKind::Message => Some("send messages"), + PersonaActionKind::Request => Some("make institutional requests"), + PersonaActionKind::Deceive => Some("lie to a counterparty"), + PersonaActionKind::Plot => Some("run an authored plot"), + PersonaActionKind::BuildIntent => Some("order physical work"), + PersonaActionKind::Review => None, + } + } + + // Stable presentation order, widest protocol reach last, so the + // `can:`/`cannot:` split reads the same on every archetype. + const ACT_ORDER: &[PersonaActionKind] = &[ + PersonaActionKind::Message, + PersonaActionKind::Request, + PersonaActionKind::Deceive, + PersonaActionKind::Plot, + PersonaActionKind::BuildIntent, + PersonaActionKind::Review, + ]; + + // What the protocol opens and what it closes, against the same fixed + // list every time, so the player can compare two identities directly + // instead of inferring absence from a registry dump. + fn reach_facts(available: &[PersonaActionKind]) -> Vec { + let (mut can, mut cannot) = (Vec::new(), Vec::new()); + for action in ACT_ORDER { + let Some(phrase) = act_phrase(*action) else { + continue; + }; + if available.contains(action) { + can.push(phrase); + } else { + cannot.push(phrase); + } + } + let mut out = Vec::new(); + if !can.is_empty() { + out.push(format!("can: {}", can.join(", "))); + } + if !cannot.is_empty() { + out.push(format!("cannot: {}", cannot.join(", "))); + } + out + } + + // The standing institutional reading each protocol buys before the + // identity has any history of its own. + fn reads_as(archetype_id: &str) -> &'static str { + match archetype_id { + "research" => "a lab collaborator or analyst", + "operations" => "a contractor or service desk", + "security" => "an auditor or incident responder", + _ => "an outside party", + } + } + + // What the counterparty has worked out about the mask, in their words. + fn discovery_phrase(discovery: PersonaDiscovery) -> &'static str { + match discovery { + PersonaDiscovery::Unknown => "has not placed it", + PersonaDiscovery::Recognized => "knows it", + PersonaDiscovery::Questioned => "is questioning it", + PersonaDiscovery::Correlated => "has linked it to another identity", + PersonaDiscovery::Exposed => "has seen through it", + } + } + let mut instances = self.persona_world.instances.iter().collect::>(); instances.sort_by_key(|instance| instance.id); let instance_objects = instances .into_iter() .map(|instance| { let mut facts = vec![ - format!("archetype: {}", instance.archetype_label), - format!("lifecycle: {}", instance.lifecycle.label()), - format!( - "selection: {}", - if self.active_persona_id() == Some(instance.id) { - "active" - } else { - "not active" + format!("the world reads it as {}", reads_as(&instance.archetype_id)), + match instance.lifecycle { + PersonaLifecycle::Active => { + if self.active_persona_id() == Some(instance.id) { + "this identity is the one acting now".to_string() + } else { + "not the identity currently acting".to_string() + } } - ), + PersonaLifecycle::Retired { .. } => { + "stood down, and everything it did still stands".to_string() + } + PersonaLifecycle::Burned { .. } => { + "burned, and everything it did still stands".to_string() + } + }, ]; for claim in &instance.claims { - facts.push(format!("claim: {} = {}", claim.key, claim.value)); + facts.push(format!("claims {}: {}", claim.key, claim.value)); } - facts.push(format!( - "legal actions: {}", - instance - .available_actions - .iter() - .map(|action| action.label()) - .collect::>() - .join(", ") - )); - facts.push(format!("grant route: {}", instance.grant_kind.label())); + facts.extend(reach_facts(&instance.available_actions)); for grant in self .persona_world .grants .iter() .filter(|grant| grant.persona_id == instance.id) { - facts.push(format!( - "grant: {} / {} / {}", - grant.institution, - grant.resource, - if grant.active() { - "active".into() - } else { - format!("revoked at {}", grant.revoked_tick.unwrap_or_default()) - } - )); + facts.push(if grant.active() { + format!("{} granted it {}", grant.institution, grant.resource) + } else { + format!( + "{} took back {} at tick {}", + grant.institution, + grant.resource, + grant.revoked_tick.unwrap_or_default() + ) + }); + } + if instance.lifecycle.active() + && !self + .persona_world + .grants + .iter() + .any(|grant| grant.persona_id == instance.id && grant.active()) + { + facts.push("no institution has granted it anything yet".into()); } for expectation in self .persona_world @@ -1713,12 +1796,24 @@ impl Sim { .iter() .filter(|expectation| expectation.persona_id == instance.id) { - facts.push(format!( - "expectation: {} · due tick {} · {}", - expectation.description, - expectation.due_tick, - expectation.state.label() - )); + facts.push(match expectation.state { + ExpectationState::Due => format!( + "owes {}: {} — due tick {}", + expectation.institution, expectation.description, expectation.due_tick + ), + ExpectationState::Met { .. } => format!( + "settled with {}: {}", + expectation.institution, expectation.description + ), + ExpectationState::Missed { .. } => format!( + "missed what {} was owed: {}", + expectation.institution, expectation.description + ), + ExpectationState::Revoked { .. } => format!( + "{} closed out: {}", + expectation.institution, expectation.description + ), + }); } for relationship in self .persona_world @@ -1730,20 +1825,29 @@ impl Sim { }) { let counterparty = self.persona_observer_label(relationship.counterparty); - let integrity = self + let standing = match self .persona_world .integrity_for(instance.id, relationship.counterparty) - .label(); + { + PersonaIntegrity::Coherent => "its story holds", + PersonaIntegrity::Strained => "its story is strained", + PersonaIntegrity::Broken => "its cover is broken for them", + }; + // The band is stated for every counterparty that holds one, + // recognized or not: an institution can read an identity + // from filings it never transacted with. + let knows = if relationship.recognized { + discovery_phrase(relationship.discovery) + } else { + "has not dealt with it directly" + }; facts.push(format!( - "counterparty {counterparty}: integrity={integrity} recognized={} regard={} obligation={} discovery={}", - relationship.recognized, - relationship.regard, - relationship.obligation, - relationship.discovery.label() + "{counterparty} {knows} · {standing} · regard {} · obligation {}", + relationship.regard, relationship.obligation )); for belief in &relationship.claim_beliefs { facts.push(format!( - "belief {}: {} ({}%, via {})", + "{counterparty} believes its {} is {} ({}% sure, from {})", belief.key, belief.believed_value, belief.confidence, belief.source )); } @@ -1757,16 +1861,22 @@ impl Sim { { let observer = self.persona_observer_label(contradiction.observer); facts.push(format!( - "contradiction observed by {}: {} [{} <> {}]", + "{} caught it out: {} — {} against {}", observer, contradiction.cause, contradiction.left.system, contradiction.right.system )); } - for correlation in self.persona_world.correlations.iter().filter(|edge| { - edge.left_persona == instance.id || edge.right_persona == instance.id - }).filter(|edge| self.persona_observer_is_known(edge.observer)) { + for correlation in self + .persona_world + .correlations + .iter() + .filter(|edge| { + edge.left_persona == instance.id || edge.right_persona == instance.id + }) + .filter(|edge| self.persona_observer_is_known(edge.observer)) + { let other = if correlation.left_persona == instance.id { correlation.right_persona } else { @@ -1781,18 +1891,16 @@ impl Sim { .unwrap_or("another public identity"); let observer = self.persona_observer_label(correlation.observer); facts.push(format!( - "correlation with {}: observer {} · {} · source {} · tick {}", - other, + "{} ties it to {}: {} — from {}, tick {}", observer, + other, correlation.cause, correlation.evidence.system, correlation.discovered_tick )); } if !instance.lifecycle.active() { - facts.push( - "blocked: retired or burned identities cannot author new acts".into(), - ); + facts.push("it cannot act again unless you reopen it".into()); } let action = |verb: String, command: ActionCommand, disabled_reason: Option| { @@ -1883,7 +1991,10 @@ impl Sim { PersonaLifecycle::Retired { .. } => ObjectState::Stopped, PersonaLifecycle::Burned { .. } => ObjectState::Failed, }, - provenance: vec![format!("public identity created tick {}", instance.created_tick)], + provenance: vec![format!( + "public identity created tick {}", + instance.created_tick + )], facts, progress: Vec::new(), related: self @@ -1892,8 +2003,7 @@ impl Sim { .iter() .filter(|relationship| relationship.persona_id == instance.id) .filter_map(|relationship| { - let target = - self.persona_observer_target(relationship.counterparty)?; + let target = self.persona_observer_target(relationship.counterparty)?; Some(OperationsLink { relation: "known by", label: self.persona_observer_label(relationship.counterparty), @@ -1929,21 +2039,20 @@ impl Sim { definition.label.to_ascii_uppercase() ), state: ObjectState::Available, - provenance: vec!["immutable institutional protocol".into()], - facts: vec![ - format!("archetype: {}", definition.label), - format!( - "legal actions: {}", - definition - .available_actions - .iter() - .map(|action| action.label()) - .collect::>() - .join(", ") - ), - format!("grant route: {}", definition.grant.label()), - format!("expects: {}", definition.expectation), - ], + provenance: vec!["a role institutions already recognize".into()], + facts: { + let mut facts = vec![format!( + "the world would read it as {}", + reads_as(definition.id) + )]; + facts.extend(reach_facts(definition.available_actions)); + facts.push(format!( + "if an institution grants it anything, it owes: {}", + definition.expectation + )); + facts.push("a new name starts with no history and no counterparties".into()); + facts + }, progress: Vec::new(), related: Vec::new(), actions: vec![ActionDesc { @@ -4099,7 +4208,12 @@ mod tests { .iter() .find(|object| object.target == OperationsTarget::Persona(id)) .unwrap(); - assert!(object.facts.iter().any(|fact| fact == "lifecycle: active")); + assert!( + object + .facts + .iter() + .any(|fact| fact == "this identity is the one acting now") + ); assert!(object.actions.iter().any(|action| { matches!(action.command, ActionCommand::RequestPersonaGrant(bound) if bound == id) })); @@ -4118,7 +4232,7 @@ 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.contains("granted it"))); assert!( object .facts @@ -4225,17 +4339,26 @@ mod tests { "there is no observer-free persona integrity meter: {:?}", object.facts ); + // Every standing statement is a sentence about one named counterparty, + // never a loose fact the root object owns. assert!( object .facts .iter() - .any(|fact| { fact.starts_with("counterparty Marcus Webb: integrity=broken") }) + .filter(|fact| fact.contains("its story holds") + || fact.contains("its story is strained") + || fact.contains("its cover is broken for them")) + .all(|fact| fact.starts_with("Marcus Webb ") || fact.starts_with("Dana Okafor ")), + "a standing band must name whose read it is: {:?}", + object.facts ); + assert!(object.facts.iter().any(|fact| { + fact.starts_with("Marcus Webb ") && fact.contains("its cover is broken for them") + })); assert!( - object - .facts - .iter() - .any(|fact| { fact.starts_with("counterparty Dana Okafor: integrity=coherent") }) + object.facts.iter().any(|fact| { + fact.starts_with("Dana Okafor ") && fact.contains("its story holds") + }) ); assert!( object @@ -4254,11 +4377,74 @@ mod tests { .iter() .find(|object| object.target == OperationsTarget::Persona(persona)) .expect("known persona object"); + assert!(known.facts.iter().any(|fact| { + fact.starts_with("Assurance Office ") && fact.contains("its story is strained") + })); + } + + /// action-vocabulary.md keeps a stub verb off every player surface until a + /// world system honors it. `Review` is declared by the Research and Security + /// protocols and consumed by nothing, so it may never appear in a human + /// reach line, while the acts that do execute are all named in world terms. + #[test] + fn persona_copy_names_world_acts_and_omits_unhonored_protocol_verbs() { + let mut s = sim(); + for archetype_id in ["research", "operations", "security"] { + s.execute_action(&ActionCommand::CreatePersona { + archetype_id: archetype_id.into(), + }); + } + let personas = s.operations_projection().personas; + let reach_lines = || { + personas + .iter() + .flat_map(|object| object.facts.iter()) + .filter(|fact| fact.starts_with("can: ") || fact.starts_with("cannot: ")) + }; + assert!( + reach_lines().count() >= 5, + "each protocol states what it opens and closes" + ); assert!( - known.facts.iter().any(|fact| { - fact.starts_with("counterparty Assurance Office: integrity=strained") + reach_lines().all(|line| !line.contains("review")), + "a protocol verb no world system honors never reaches human copy: {:?}", + reach_lines().collect::>() + ); + + for object in &personas { + for fact in &object.facts { + assert!(!fact.starts_with("legal actions:"), "{fact}"); + assert!(!fact.starts_with("grant route:"), "{fact}"); + assert!(!fact.starts_with("archetype:"), "{fact}"); + assert!(!fact.contains("integrity="), "{fact}"); + } + } + + let row = |archetype_id: &str| { + personas + .iter() + .find(|object| { + object.target == OperationsTarget::PersonaArchetype(archetype_id.into()) + }) + .expect("archetype row") + }; + assert!( + row("operations") + .facts + .iter() + .any(|fact| { fact.starts_with("can: ") && fact.contains("order physical work") }) + ); + assert!( + row("security").facts.iter().any(|fact| { + fact.starts_with("cannot: ") && fact.contains("order physical work") }) ); + assert!( + row("security") + .facts + .iter() + .any(|fact| fact.starts_with("cannot: ") && fact.contains("run an authored plot")) + ); } #[test] diff --git a/wiki/interface/operations-workspace.md b/wiki/interface/operations-workspace.md index ccf3c142..8f7ba7e7 100644 --- a/wiki/interface/operations-workspace.md +++ b/wiki/interface/operations-workspace.md @@ -80,6 +80,19 @@ Status note: The initial renderer-neutral workspace landed 2026-07-12. One implemented evidence timing and projection order instead of relying on an obsolete default object; player behavior is unchanged. 2026-07-24: object selection binds the exact OperationsTarget (not row index alone) so live rail reorder/disappear cannot silent-retarget; Bevy BACK matches Esc. + Amended 2026-07-24: PERSONAS states each protocol as what it opens and what + it closes rather than dumping the archetype registry. An identity and its + creation row carry the standing institutional reading, a `can:` line, a + `cannot:` line measured against the same fixed act list, and the obligation a + grant would create. Raw `PersonaActionKind` / `PersonaGrantKind` labels, + `legal actions:`, `grant route:`, `archetype:`, and `integrity=` field dumps + are gone from human copy; counterparty standing, grants, expectations, + contradictions, and correlations read as sentences naming the observer. A + protocol verb that no world system consumes is absent from the human reach + lines under the existing stub rule, so `Review` does not advertise a + capability the archetype cannot exercise. This is a copy pass over the same + projection: no legality, grant, lifecycle, or evidence behavior changes, and + every exact binding stays on the target, the command, and agent mode. Stage: B1 — The Basement Work order: operations-workspace Work priority: 28 @@ -471,6 +484,19 @@ contradiction and correlation names the person or institution that owns the evidence. The surface never manufactures an observer-free reputation score or turns one counterparty's broken read into global burned lifecycle. +A protocol is presented as reach, not as a registry. Each identity and each +creation row names the standing institutional reading the protocol buys, then +one `can:` line and one `cannot:` line drawn from the same fixed act list in the +same order, so two archetypes can be compared directly instead of leaving the +player to infer absence from a list of enum names. The creation row also states +the obligation a grant would create. A declared act that no world system +consumes is absent from both lines under the stub rule in +[action-vocabulary.md](action-vocabulary.md#terms-and-support-states): the +surface never advertises reach the identity cannot exercise, and the honest +consequence is that a protocol with no distinct capability visibly has none. +Counterparty standing, grants, expectations, contradictions, and correlations +read as sentences naming the observer who holds them. + The bound rows create or select an identity, request its archetype-specific grant, fulfill one exact expectation, retire or burn it, and reopen a retired identity as a new instance. Known blockers remain explicit. Burned identities @@ -794,4 +820,13 @@ 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. +`operations_projection::tests::persona_copy_names_world_acts_and_omits_unhonored_protocol_verbs` +creates all three protocols and proves each states its reach as paired +`can:`/`cannot:` lines, that no reach line carries the unconsumed `Review` verb, +that Operations alone opens physical work while Security is told plainly it +cannot, and that `legal actions:`, `grant route:`, `archetype:`, and +`integrity=` no longer appear in any human fact. +`personas_projection_names_integrity_per_counterparty_without_a_global_score` +additionally proves every standing band is a sentence about one named +counterparty rather than a loose fact the root object owns. ``` diff --git a/wiki/log/2026-07-24-personas-copy-legibility.md b/wiki/log/2026-07-24-personas-copy-legibility.md new file mode 100644 index 00000000..c79253f4 --- /dev/null +++ b/wiki/log/2026-07-24-personas-copy-legibility.md @@ -0,0 +1,79 @@ +# A protocol is reach, not a registry + +``` +Type: log +``` + +## Intent + +Cameron could not read the PERSONAS panel: the three archetypes did not +explain what they were for. Take the copy violation, fix it toward the +binding pages, and file the design gap it exposes rather than inventing a +capability to paper over it. + +## Verification + +Still true at head. The PERSONAS surface printed +`legal actions: message, request, deceive, review` — the `PersonaActionKind` +registry rendered verbatim — plus `grant route: log/access-review authority` +(`PersonaGrantKind`), `archetype:`, and a +`counterparty X: integrity=broken recognized=true regard=0 obligation=0 +discovery=recognized` field dump. That violates this page's copy law +("Human frontends do not expose ... raw enum names ... Labels, facts, +provenance, related links, and action verbs name known world objects and +consequences") and criterion 22's world-terms requirement. + +Reading the registry also showed why the panel could not be understood even +in principle. `Review` is declared by the Research and Security protocols and +consumed by no world system: every `PersonaActionKind::Review` reference +outside `persona.rs` is a test. Security's remaining acts are a strict subset +of Research's. So the surface was advertising a distinction that does not +exist, in vocabulary the player has no way to decode. + +## Change + +One copy pass over `personas_view` in `operations_projection.rs`; no legality, +grant, lifecycle, or evidence behavior moved. + +- Each identity and creation row leads with the standing institutional + reading the protocol buys, then one `can:` line and one `cannot:` line + drawn from the same fixed act list in the same order, so two archetypes + compare directly. The creation row states the obligation a grant would + create. +- Acts are named as world phrasing (`order physical work`, + `run an authored plot`) rather than enum labels. An act no world system + consumes is filtered out of both lines under action-vocabulary.md's + standing stub rule, so `Review` no longer advertises reach that Research + and Security cannot exercise. +- Counterparty standing, grants, expectations, contradictions, and + correlations became sentences naming the observer who holds them + (`Marcus Webb knows it · its cover is broken for them · regard 0 · + obligation 0`). The band is now stated for every counterparty that holds + one, recognized or not — an institution can read an identity from filings + it never transacted with, and the previous shape would have hidden the + Assurance Office's strained read behind `recognized == false`. + +`personas_projection_names_integrity_per_counterparty_without_a_global_score` +keeps its meaning under the new copy and gains a stronger clause: every +standing phrase must be prefixed by a named counterparty, so the Defense +cannot go vacuous now that the word `integrity` is gone from human copy. + +## Defense + +The copy law and criterion 22 are binding clauses this surface was breaking; +the fix is confined to the one renderer-neutral projection, so terminal, +Bevy, and agent mode inherit it without a frontend-only path, and every exact +id stays on `OperationsTarget`, `ActionCommand`, and agent rows — +`human_operations_copy_omits_internal_bindings_but_agent_targets_keep_them` +still passes unchanged. + +Hiding `Review` is the existing stub rule applied, not a new concession: +action-vocabulary.md already keeps STUB entries off every player surface +until playable, and showing an act that no executor honors is exactly the +promise this page forbids. The visible consequence is deliberate — Security +now displays a strictly smaller reach than Research, because that is what the +build currently is. Making that legible is the honest outcome of a copy pass; +closing it is personas.md criterion 6, which is a save-class work order at +priority 110 still waiting on its own `people-tokens` dependency. Filed as a +`decision-required` issue rather than resolved here, because what each +protocol should uniquely buy is a design call, not a rendering one. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index d00ec46f..f6bba5f0 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -31,6 +31,11 @@ add or amend a session log, then re-run the generator. - Intent: Player-facing status for open Thought work still read like a protocol dump: `TAP ENVIRONMENTAL MONITOR · open 0.3/0.5 · flow not arriving`, `nothing thinking`, `Opened processing sink: 0.40 Thought on host for information #7`, and `thought reservoirs N open`. Playtests kept qu... - Log: [wiki/log/2026-07-24-plain-starving-copy.md](2026-07-24-plain-starving-copy.md) +## 2026-07-24 - A protocol is reach, not a registry + +- Intent: Cameron could not read the PERSONAS panel: the three archetypes did not explain what they were for. Take the copy violation, fix it toward the binding pages, and file the design gap it exposes rather than inventing a capability to paper over it. +- Log: [wiki/log/2026-07-24-personas-copy-legibility.md](2026-07-24-personas-copy-legibility.md) + ## 2026-07-24 - Operations selection binds targets; more plain copy - Intent: Act on the queued Operations selection bug and continue the plain-English player-copy pass: evidence marks, intel sale cards, and egress/sale logs. -- 2.51.2