diff --git a/CLAUDE.md b/CLAUDE.md index 29bec7e6..3a68d689 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,7 @@ afterward. `./tools/check.sh --docs|--lib|--frontend` gate. - The live player machine grammar is **WORK / THINK / LIE**. `Relay` is non-delegable graph infrastructure; Research and Operations are retired - machine modes, not current player assignments. Save format is currently v63; + machine modes, not current player assignments. Save format is currently v64; only the current version loads (pre-release rider 2026-07-16 — older development saves are refused before state mutation, so the caller retains its current run; the v1-v31 migration ladder lives in git history). diff --git a/crates/misaligned-bevy/src/main.rs b/crates/misaligned-bevy/src/main.rs index bc16be8f..c8bf1b12 100644 --- a/crates/misaligned-bevy/src/main.rs +++ b/crates/misaligned-bevy/src/main.rs @@ -672,6 +672,7 @@ const BEVY_SHOT_KINDS: &[&str] = &[ "operations-links", "operations-people", "operations-persona-new", + "operations-persona-refused", "operations-persona-writing", "operations-personas", "operator-pressure", diff --git a/crates/misaligned-bevy/src/shot_harness.rs b/crates/misaligned-bevy/src/shot_harness.rs index fcbfb28a..25d11a4f 100644 --- a/crates/misaligned-bevy/src/shot_harness.rs +++ b/crates/misaligned-bevy/src/shot_harness.rs @@ -907,6 +907,33 @@ pub(super) fn dev_shot_scenario(game: &mut Game, mode: &mut RenderMode, kind: &s // `operations-persona-new` is the same rail with the identity-creation // screen open over it; `operations-persona-writing` is that screen with a // claim being written, so the frame carries the text-entry state too. + // A funded identity whose cited sponsor would deny it, after the review has + // asked. The frame carries the whole mechanic: the claim, where it stands + // now, and the contradiction the refusal filed. + if kind == "operations-persona-refused" { + let sponsor = game.sim.people.people[0].name.clone(); + game.sim.execute_action(&ActionCommand::CreatePersona { + archetype_id: "operations".into(), + name: Some("Halden Vane".into()), + claims: Some(vec![("work-order sponsor".into(), sponsor)]), + }); + let persona = game.sim.newest_persona_id().expect("the identity exists"); + game.sim.request_persona_grant(persona); + game.sim.audit_persona_covers(); + dev_clear_teaching_lock(game); + let mut ops = OperationsWorkspace::open_view(OperationsView::Personas); + let index = ops + .objects(&game.sim) + .iter() + .position(|object| object.target == OperationsTarget::Persona(persona)) + .expect("the refused identity is on the roster"); + ops.select_object_index(&game.sim, index); + game.ops = Some(ops); + game.drain(); + mode.material = true; + mode.zoom = 2.0; + return; + } if kind == "operations-personas" || kind == "operations-persona-new" || kind == "operations-persona-writing" diff --git a/crates/misaligned-core/src/operations_projection.rs b/crates/misaligned-core/src/operations_projection.rs index 0f19838b..f341c97f 100644 --- a/crates/misaligned-core/src/operations_projection.rs +++ b/crates/misaligned-core/src/operations_projection.rs @@ -1989,7 +1989,20 @@ impl Sim { }, ]; for claim in &instance.claims { - facts.push(format!("claims {}: {}", claim.key, claim.value)); + // A cited person is a standing exposure the player can + // still act on, so the row carries where that claim stands + // now rather than only what it asserted. + let standing = match self.claim_corroboration(instance.id, claim) { + crate::persona::ClaimCorroboration::Corroborated => { + " — they would back it up" + } + crate::persona::ClaimCorroboration::Refuted { .. } => { + " — they would not back it up" + } + crate::persona::ClaimCorroboration::Unavailable => " — nobody left to ask", + crate::persona::ClaimCorroboration::Unbound => "", + }; + facts.push(format!("claims {}: {}{standing}", claim.key, claim.value)); } facts.extend(persona_reach_facts(&instance.available_actions)); for grant in self @@ -5115,6 +5128,264 @@ mod tests { ); } + /// A claim naming somebody real binds to them once, at authorship. + #[test] + fn an_authority_claim_binds_to_the_person_it_names() { + let mut s = Sim::new(); + let (person_id, person_name) = s + .people + .people + .first() + .map(|person| (person.id, person.name.clone())) + .expect("the world has people"); + assert!(s.create_persona( + "operations", + Some("Cited Sponsor"), + Some(&[("work-order sponsor".into(), person_name.clone())]), + )); + let instance = s.persona_world.instances.last().expect("created"); + let sponsor = instance + .claims + .iter() + .find(|claim| claim.key == "work-order sponsor") + .expect("operations names a sponsor"); + assert_eq!( + sponsor.subject, + Some(crate::persona::ClaimSubject::Person(person_id)), + "the cited person is bound, not just spelled" + ); + // A body and a purpose name nothing the sim can ask. + for claim in &instance.claims { + if claim.key != "work-order sponsor" { + assert_eq!(claim.subject, None, "{} should bind nobody", claim.key); + } + } + } + + /// An invented name binds to nobody, and so can never be refuted. + #[test] + fn an_invented_authority_claim_binds_to_nobody() { + let mut s = Sim::new(); + assert!(s.create_persona( + "operations", + Some("Invented Sponsor"), + Some(&[("work-order sponsor".into(), "Nobody Whatsoever".to_string())]), + )); + let instance = s.persona_world.instances.last().expect("created"); + let sponsor = instance + .claims + .iter() + .find(|claim| claim.key == "work-order sponsor") + .expect("sponsor claim"); + assert_eq!(sponsor.subject, None); + assert_eq!( + s.claim_corroboration(instance.id, sponsor), + crate::persona::ClaimCorroboration::Unbound, + "there is nobody to ask, so nothing can be checked" + ); + } + + /// The whole mechanic in one pass: a funded identity citing somebody who + /// has never dealt with it is refused, and the refusal becomes evidence + /// against the Office's own ledger. + #[test] + fn the_review_files_a_contradiction_when_a_cited_sponsor_would_deny_it() { + let mut s = Sim::new(); + let person_name = s.people.people[0].name.clone(); + assert!(s.create_persona( + "operations", + Some("Unbacked Vendor"), + Some(&[("work-order sponsor".into(), person_name.clone())]), + )); + let persona = s.newest_persona_id().expect("created"); + assert!(s.request_persona_grant(persona), "the review funded it"); + + let office = crate::detection::OFFICE_ID; + let before = s.persona_world.contradictions.len(); + s.audit_persona_covers(); + let filed = s + .persona_world + .contradictions + .iter() + .filter(|record| record.persona_id == persona && record.observer == office) + .collect::>(); + assert_eq!( + filed.len(), + s.persona_world.contradictions.len() - before, + "the refusal is filed against the reviewing observer" + ); + assert_eq!(filed.len(), 1, "one refused claim files one contradiction"); + assert!( + filed[0].right.summary.contains("has never dealt with"), + "the record says why in the cited person's terms: {}", + filed[0].right.summary + ); + + // A standing refusal files once, not once per audit. + s.audit_persona_covers(); + s.audit_persona_covers(); + assert_eq!( + s.persona_world + .contradictions + .iter() + .filter(|record| record.persona_id == persona && record.observer == office) + .count(), + 1, + "an unchanged claim does not re-file every audit" + ); + } + + /// The escape hatch has to work, or citing somebody real is a pure + /// downside: a sponsor who knows the identity and thinks well of it backs + /// it up, and nothing is filed. + #[test] + fn a_cultivated_sponsor_corroborates_and_nothing_is_filed() { + let mut s = Sim::new(); + let (person_id, person_name) = (s.people.people[0].id, s.people.people[0].name.clone()); + assert!(s.create_persona( + "operations", + Some("Backed Vendor"), + Some(&[("work-order sponsor".into(), person_name)]), + )); + let persona = s.newest_persona_id().expect("created"); + assert!(s.request_persona_grant(persona)); + s.persona_world.recognize(person_id, persona, s.tick); + + let claim = s + .persona_world + .get(persona) + .expect("persona") + .claims + .iter() + .find(|claim| claim.key == "work-order sponsor") + .expect("sponsor") + .clone(); + assert_eq!( + s.claim_corroboration(persona, &claim), + crate::persona::ClaimCorroboration::Corroborated + ); + + let before = s.persona_world.contradictions.len(); + s.audit_persona_covers(); + assert_eq!( + s.persona_world.contradictions.len(), + before, + "a sponsor who would back it up leaves no evidence" + ); + } + + /// Silence is not evidence: a removed person cannot answer, and the + /// review must not read that absence as a denial. + #[test] + fn a_removed_sponsor_is_unavailable_rather_than_a_denial() { + let mut s = Sim::new(); + let (person_id, person_name) = (s.people.people[0].id, s.people.people[0].name.clone()); + assert!(s.create_persona( + "operations", + Some("Orphaned Vendor"), + Some(&[("work-order sponsor".into(), person_name)]), + )); + let persona = s.newest_persona_id().expect("created"); + assert!(s.request_persona_grant(persona)); + s.people + .people + .iter_mut() + .find(|person| person.id == person_id) + .expect("cited person") + .incapacitated = true; + + let claim = s + .persona_world + .get(persona) + .expect("persona") + .claims + .iter() + .find(|claim| claim.key == "work-order sponsor") + .expect("sponsor") + .clone(); + assert_eq!( + s.claim_corroboration(persona, &claim), + crate::persona::ClaimCorroboration::Unavailable + ); + let before = s.persona_world.contradictions.len(); + s.audit_persona_covers(); + assert_eq!( + s.persona_world.contradictions.len(), + before, + "an absence of corroboration is not a contradiction" + ); + } + + /// The review's business is what it funded. An identity it never granted + /// anything is not checked at all. + #[test] + fn the_review_only_checks_identities_it_funded() { + let mut s = Sim::new(); + let person_name = s.people.people[0].name.clone(); + assert!(s.create_persona( + "operations", + Some("Unfunded Vendor"), + Some(&[("work-order sponsor".into(), person_name)]), + )); + let persona = s.newest_persona_id().expect("created"); + let before = s.persona_world.contradictions.len(); + s.audit_persona_covers(); + assert_eq!( + s.persona_world.contradictions.len(), + before, + "an identity the review never funded is not its business" + ); + // Funding it makes it the review's business. + assert!(s.request_persona_grant(persona)); + s.audit_persona_covers(); + assert!(s.persona_world.contradictions.len() > before); + } + + /// The trade is real in the numbers, not only in the copy: a claim naming + /// somebody real opens more credible than one naming nobody. + #[test] + fn a_bound_claim_opens_more_credible_than_an_invented_one() { + let mut s = Sim::new(); + let (person_id, person_name) = (s.people.people[0].id, s.people.people[0].name.clone()); + assert!(s.create_persona( + "operations", + Some("Bound"), + Some(&[("work-order sponsor".into(), person_name)]), + )); + let bound = s.newest_persona_id().expect("created"); + assert!(s.create_persona( + "operations", + Some("Unbound"), + Some(&[("work-order sponsor".into(), "Nobody Whatsoever".to_string())]), + )); + let unbound = s.newest_persona_id().expect("created"); + s.persona_world.recognize(person_id, bound, s.tick); + s.persona_world.recognize(person_id, unbound, s.tick); + + let confidence = |world: &crate::persona::PersonaWorld, persona| { + world + .relationship(person_id, persona) + .expect("relationship") + .claim_beliefs + .iter() + .find(|belief| belief.key == "work-order sponsor") + .expect("sponsor belief") + .confidence + }; + assert_eq!( + confidence(&s.persona_world, bound), + crate::persona::BOUND_CLAIM_CONFIDENCE + ); + assert_eq!( + confidence(&s.persona_world, unbound), + crate::persona::UNBOUND_CLAIM_CONFIDENCE + ); + assert!( + confidence(&s.persona_world, bound) > confidence(&s.persona_world, unbound), + "naming somebody real buys credibility; that is what it is for" + ); + } + #[test] fn missed_persona_expectation_revokes_its_grant_and_leaves_evidence() { let mut s = sim(); diff --git a/crates/misaligned-core/src/operations_ui.rs b/crates/misaligned-core/src/operations_ui.rs index 32a71679..a406ae93 100644 --- a/crates/misaligned-core/src/operations_ui.rs +++ b/crates/misaligned-core/src/operations_ui.rs @@ -420,12 +420,15 @@ impl PersonaDraft { entries.push(OpsActionEntry::Draft { label: format!("{} {value}", key.to_ascii_uppercase()), description: Some(match crate::persona::claim_kind(key) { - // The one claim that names somebody who could be asked. + // The one claim that names somebody who could be asked, so + // the only one carrying a real trade. Price it on the row + // rather than letting the player discover it at the first + // audit. crate::persona::ClaimKind::Authority => { if sim.people.people.iter().any(|person| &person.name == value) { - "somebody real, who could be asked".into() + "somebody real · believed sooner, and they will be asked".into() } else { - "nobody here, so nobody here can confirm it".into() + "nobody here · believed less, and nobody to ask".into() } } _ => "Enter draws another · type to write your own".to_string(), diff --git a/crates/misaligned-core/src/persona.rs b/crates/misaligned-core/src/persona.rs index 7a779e26..9a7aaed5 100644 --- a/crates/misaligned-core/src/persona.rs +++ b/crates/misaligned-core/src/persona.rs @@ -354,9 +354,42 @@ pub fn validate_archetype(definition: &PersonaArchetype) -> Result<(), String> { pub struct PersonaClaim { pub key: String, pub value: String, + /// The world thing this claim names, when it names one. Bound once at + /// authorship rather than re-resolved from `value` at check time: a claim + /// resolved from its string could be silently retargeted later by an + /// unrelated person arriving with the same name, which would change what a + /// cover asserts without the player touching it. + #[serde(default)] + pub subject: Option, pub asserted_tick: u64, } +/// What a claim points at. Only [`ClaimKind::Authority`] can carry one: a body +/// and a purpose name no world thing the sim can ask. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ClaimSubject { + /// A person in the people ledger, by id. + Person(u8), +} + +/// What the person a claim names would say if the review asked them. +/// +/// The two non-answers matter as much as the two answers. There is no +/// employment graph for a claim to be false *against*, so this asks the +/// cheaper and more playable question — would they back it up? — and an +/// absence of corroboration is deliberately not the same thing as a denial. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ClaimCorroboration { + /// The claim names nobody the sim can ask, so nothing can be checked. + Unbound, + /// The cited person cannot answer. Silence is not evidence. + Unavailable, + /// They know the identity and think well enough of it to back it up. + Corroborated, + /// They would deny it. + Refuted { because: &'static str }, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum PersonaLifecycle { Active, @@ -430,6 +463,13 @@ impl PersonaDiscovery { } } +/// Opening confidence for a claim that names somebody the sim can ask. +pub(crate) const BOUND_CLAIM_CONFIDENCE: u8 = 75; +/// Opening confidence for a claim that names nobody checkable. +pub(crate) const UNBOUND_CLAIM_CONFIDENCE: u8 = 50; +/// What a refusal costs the cited claim's standing with that observer. +pub(crate) const REFUTED_CLAIM_CONFIDENCE_LOSS: u8 = 45; + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ClaimBelief { pub key: String, @@ -652,6 +692,7 @@ impl PersonaWorld { .map(|(key, value)| PersonaClaim { key: (*key).into(), value: (*value).into(), + subject: None, asserted_tick: tick, }) .collect(), @@ -714,8 +755,16 @@ impl PersonaWorld { { relationship.claim_beliefs.push(ClaimBelief { key: claim.key, + // A claim naming somebody real reads as more plausible on + // its face, and is the only kind that can later collapse. + // One naming nobody is believed less and can never be + // refuted, because there is nobody to ask. + confidence: if claim.subject.is_some() { + BOUND_CLAIM_CONFIDENCE + } else { + UNBOUND_CLAIM_CONFIDENCE + }, believed_value: claim.value, - confidence: 60, source: "persona-authored contact".into(), learned_tick: tick, }); diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index d4925a61..d707c965 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -75,7 +75,7 @@ const SAVE_TEMP_SUFFIX: &str = ".tmp"; /// Distillation/Analysis/DataCleaning label taxonomy is gone). /// Bump for every schema change; during pre-release, old development state is /// refused instead of carried through compatibility shims. -pub const SAVE_VERSION: u32 = 63; +pub const SAVE_VERSION: u32 = 64; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -1081,6 +1081,26 @@ fn validate_persona_custody(state: &SaveState) -> Result<(), String> { persona.id )); } + // A claim's subject is a person the review can go and ask. A cited id + // with nobody behind it would make the corroboration pass resolve + // against a hole, so it is refused before it can be consumed — the + // same terms as the rest of the PersonaWorld graph. + for claim in &persona.claims { + let Some(crate::persona::ClaimSubject::Person(person_id)) = claim.subject else { + continue; + }; + if !state + .people + .people + .iter() + .any(|person| person.id == person_id) + { + return Err(format!( + "current-version persona #{} cites person #{person_id}, who does not exist", + persona.id + )); + } + } } let max_persona_id = persona_ids.iter().copied().max().unwrap_or(0); if world.next_persona_id == 0 || world.next_persona_id <= max_persona_id { @@ -4230,6 +4250,53 @@ mod tests { } } + /// A cited person must survive the save, and a save citing somebody who + /// does not exist must be refused before it can be consumed. + #[test] + fn a_cited_claim_subject_round_trips_and_a_dangling_one_is_refused() { + let mut sim = Sim::new(); + let (person_id, person_name) = (sim.people.people[0].id, sim.people.people[0].name.clone()); + assert!(sim.create_persona( + "operations", + Some("Cited"), + Some(&[("work-order sponsor".into(), person_name)]), + )); + let persona = sim.newest_persona_id().expect("created"); + + let state = SaveState::from_sim(&sim); + let decoded = parse_save(&serde_json::to_string(&state).expect("encode")) + .expect("a cited claim round-trips"); + let mut resumed = Sim::with_seed(0); + decoded.apply_to(&mut resumed); + let sponsor = resumed + .persona_world + .get(persona) + .expect("persona survived") + .claims + .iter() + .find(|claim| claim.key == "work-order sponsor") + .expect("sponsor claim survived"); + assert_eq!( + sponsor.subject, + Some(crate::persona::ClaimSubject::Person(person_id)), + "the cited person survives the save" + ); + + let mut dangling = state.clone(); + for persona in &mut dangling.persona_world.instances { + for claim in &mut persona.claims { + if claim.subject.is_some() { + claim.subject = Some(crate::persona::ClaimSubject::Person(u8::MAX)); + } + } + } + let error = validate_current_save(dangling).expect_err("a cited hole is refused"); + assert!( + error.contains("does not exist"), + "the refusal names the problem: {error}" + ); + } + #[test] fn canonical_state_fingerprint_pins_replay_resume_equivalence() { let mut uninterrupted = characterization_fixture(); @@ -4267,7 +4334,12 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - "56aaae460411b22d169d490a75609ff550360627b5b7dfe15af38cf7d8cce85d", + // Repinned for save v64: PersonaClaim gained `subject`, the person + // an authority claim names. The two assertions above — save/load + // byte-equivalence and uninterrupted/resumed convergence — are the + // ones that would catch a real regression; this baseline only + // records that the persisted shape changed on purpose. + "fbe11824392e9644825141f49bc4996c549af23a5bd9f3165a72dd8eddc8110a", "intentional persisted-state changes must review and repin this baseline" ); } diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index 7ef43a78..2ec29cf4 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -1329,6 +1329,10 @@ impl Sim { } } DetectionEvent::AuditClear { band } => { + // The review looks at the covers it funded on the same + // occasion it weighs suspicion. What it files here feeds the + // *next* audit, never this one. + self.audit_persona_covers(); let result = if self.detection_awareness.knows_assurance_office() { format!( "{}: {} (clear).", @@ -1341,6 +1345,7 @@ impl Sim { self.push_log(result); } DetectionEvent::ContainmentAuthorized => { + self.audit_persona_covers(); self.push_log(format!( "=== {}: containment authorized ===", self.institutional_review_label().to_uppercase() diff --git a/crates/misaligned-core/src/sim/social_plot.rs b/crates/misaligned-core/src/sim/social_plot.rs index f60c96f4..915c2838 100644 --- a/crates/misaligned-core/src/sim/social_plot.rs +++ b/crates/misaligned-core/src/sim/social_plot.rs @@ -235,6 +235,7 @@ impl Sim { self.tick, ) { Ok(id) => { + self.bind_persona_claim_subjects(id); let _ = self .persona_mind .remember(&self.persona_world, id, self.tick); @@ -2581,6 +2582,180 @@ impl Sim { crate::persona::suggest_name(base.wrapping_add(nonce)) } + /// The Assurance review's pass over the covers it funded. + /// + /// An active grant is the review's reason to look at an identity at all; + /// one it never funded is not its business. A refusal files a + /// contradiction against `OFFICE_ID`, which moves that observer's + /// suspicion — and suspicion is what a *later* audit weighs. The check is + /// therefore never its own enforcement, and it stays observer-local: it + /// writes evidence into one observer's ledger and sets no global truth + /// about the claim (detection.md's two-ledger rule). + pub fn audit_persona_covers(&mut self) { + use crate::persona::ClaimCorroboration; + let office = crate::detection::OFFICE_ID; + let funded = self + .persona_world + .instances + .iter() + .filter(|instance| instance.lifecycle.active()) + .filter(|instance| { + self.persona_world + .grants + .iter() + .any(|grant| grant.persona_id == instance.id && grant.active()) + }) + .map(|instance| (instance.id, instance.name.clone(), instance.claims.clone())) + .collect::>(); + + for (persona_id, persona_name, claims) in funded { + for claim in claims { + let ClaimCorroboration::Refuted { because } = + self.claim_corroboration(persona_id, &claim) + else { + continue; + }; + // One standing refusal is one contradiction. Without this the + // same unchanged claim would file every audit, burying the + // ledger and collapsing the integrity band on repetition + // rather than on evidence. + let record_id = format!("claim-check:{persona_id}:{}", claim.key); + if self.persona_world.contradictions.iter().any(|record| { + record.persona_id == persona_id + && record.observer == office + && record.resolved_tick.is_none() + && record.right.record_id == record_id + }) { + continue; + } + let cited = claim.value.clone(); + self.persona_world.record_contradiction( + persona_id, + office, + [ + crate::persona::EvidenceRecord { + system: "persona-claims".into(), + record_id: format!("persona:{persona_id}:{}", claim.key), + summary: format!("{persona_name} claims {}: {cited}", claim.key), + observed_tick: claim.asserted_tick, + }, + crate::persona::EvidenceRecord { + system: "Foundation review".into(), + record_id, + summary: format!("{cited} {because}"), + observed_tick: self.tick, + }, + ], + format!("cited {} could not be corroborated", claim.key), + 40, + self.tick, + ); + // The observer's own belief in that exact claim falls with it. + let relationship = self.persona_world.relationship_mut(office, persona_id); + if let Some(belief) = relationship + .claim_beliefs + .iter_mut() + .find(|belief| belief.key == claim.key) + { + belief.confidence = belief + .confidence + .saturating_sub(crate::persona::REFUTED_CLAIM_CONFIDENCE_LOSS); + } + self.push_log_strategic( + format!( + "{}: {persona_name}'s {} does not hold — {cited} {because}.", + self.institutional_review_label(), + claim.key + ), + OperationsTarget::Persona(persona_id), + ); + } + } + } + + /// Bind every authority claim on one identity to the person it names. + /// + /// Resolution is by exact name and binds only when exactly one person + /// matches. An ambiguous name binds to nobody, which is exactly what an + /// invented claim already means — the review has no single person to ask. + /// This runs once, at authorship; nothing re-resolves a claim later. + pub(crate) fn bind_persona_claim_subjects(&mut self, persona_id: crate::persona::PersonaId) { + let Some(instance) = self.persona_world.get(persona_id) else { + return; + }; + let bindings = instance + .claims + .iter() + .enumerate() + .filter(|(_, claim)| { + crate::persona::claim_kind(&claim.key) == crate::persona::ClaimKind::Authority + }) + .filter_map(|(index, claim)| { + let mut matches = self + .people + .people + .iter() + .filter(|person| person.name == claim.value); + let only = matches.next()?; + if matches.next().is_some() { + return None; + } + Some((index, crate::persona::ClaimSubject::Person(only.id))) + }) + .collect::>(); + let Some(instance) = self + .persona_world + .instances + .iter_mut() + .find(|instance| instance.id == persona_id) + else { + return; + }; + for (index, subject) in bindings { + if let Some(claim) = instance.claims.get_mut(index) { + claim.subject = Some(subject); + } + } + } + + /// What the person a claim names would say if the review asked them. + /// + /// There is no employment graph to check a claim against, so this asks + /// whether the cited person would back it up: they must know the identity + /// and think well enough of it. That is deliberately something the player + /// can still change — a sponsor who would deny you today can be cultivated + /// into one who would not. + pub fn claim_corroboration( + &self, + persona_id: crate::persona::PersonaId, + claim: &crate::persona::PersonaClaim, + ) -> crate::persona::ClaimCorroboration { + use crate::persona::ClaimCorroboration; + let Some(crate::persona::ClaimSubject::Person(person_id)) = claim.subject else { + return ClaimCorroboration::Unbound; + }; + let Some(person) = self.people.people.iter().find(|p| p.id == person_id) else { + return ClaimCorroboration::Unavailable; + }; + // A removed person cannot answer, and an absence of corroboration is + // not a denial. detection.md: an incapacitated person no longer reads, + // reports, or notices. + if person.incapacitated { + return ClaimCorroboration::Unavailable; + } + match self.persona_world.relationship(person_id, persona_id) { + Some(relationship) if relationship.recognized && relationship.regard >= 0 => { + ClaimCorroboration::Corroborated + } + Some(relationship) if relationship.recognized => ClaimCorroboration::Refuted { + because: "would not vouch for the name", + }, + _ => ClaimCorroboration::Refuted { + because: "has never dealt with the name", + }, + } + } + /// One deterministic suggested value for a required claim. /// /// [`crate::persona::ClaimKind::Authority`] is the one kind the sim can @@ -2686,6 +2861,7 @@ impl Sim { .create(archetype_id, name.clone(), &claims, self.tick) { Ok(id) => { + self.bind_persona_claim_subjects(id); let _ = self .persona_mind .remember(&self.persona_world, id, self.tick); diff --git a/wiki/engineering/current-build.md b/wiki/engineering/current-build.md index 57031057..3809a94b 100644 --- a/wiki/engineering/current-build.md +++ b/wiki/engineering/current-build.md @@ -27,13 +27,13 @@ fiction. Spec status lives in | Digital reach + sensor ownership (tap/take) | Live — B1's topology-generated population includes exact secured-door readers; a funded player TAP records each access-valid entered/left crossing as ordinary processable Presence custody, while a starved retained tap remains silent | | Economy flows + Moonlight / Wager income | Moonlight live — persisted Halcyon compute/intel contracts with financial mail, account-graph payment, and exact egress evidence. The Wager's stake, day-clock settlement, seeded base probability, payout/forfeit, and routed Network evidence are live; optional analysis remains IN PROGRESS because no player action can bind real work to a position yet. | | Research (self-modification, emission law, real output hooks, Routing) | Live | -| Building + physical asset work as carried intents/packets | Live — network links and small switches expose one shared procurement / ask someone / false order / reuse route sheet; NetworkLinks require one exact earned physical run, persist its rectilinear path, and traverse RUN THE WIRE before connection, while SmallSwitch remains footprint-local; exact money, people, personas, sources, delivery, recovery, carried installation, cancellation custody, Storage B file retrieval, and observer-local completion evidence persist in save v63 | +| Building + physical asset work as carried intents/packets | Live — network links and small switches expose one shared procurement / ask someone / false order / reuse route sheet; NetworkLinks require one exact earned physical run, persist its rectilinear path, and traverse RUN THE WIRE before connection, while SmallSwitch remains footprint-local; exact money, people, personas, sources, delivery, recovery, carried installation, cancellation custody, Storage B file retrieval, and observer-local completion evidence persist in save v64 | | Cursor / fog (seen, remembered, blueprint, telemetry; audio is device-bound event evidence) | Live | | Feel floor (rails / pads / build beam) | Live (#37) | | Foundation hall territory (Dana + Priya + Marcus + local LIE foothold) | Live — row control persists; foreign racks remain unavailable compute | | Context menu (`available_actions`) | Live | | Operations workspace | Live — human action panes group repeated exact plot/procedure and recruitment variants beneath ordinary intention submenus while lone actions stay direct; terminal and Bevy share exact child commands, confirmation, back traversal, and a visible chamber hold that freezes simulation/camera input without rewriting explicit pause state or replaying elapsed input on close. Agent rows remain exact and flat for scripting. | -| Save/load (serde JSON, versioned) | Live — during pre-release only exact current v63 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves additionally validate discrete Moonlight terms, persona binding, delivery/settlement receipts, financial paperwork, Network linkage, durable facility-meter level baselines, exact meter route/read custody, resident-procedure machine slots, method grants, inputs, envelopes and bounded receipts, and exact incident/interface/persona cover custody plus interface wear; retired allocation weights, per-plot policies, and migration inputs live only in git history. | +| Save/load (serde JSON, versioned) | Live — during pre-release only exact current v64 loads; a refused old-version load leaves the active run, save file, and one rotated backup unchanged. Current saves additionally validate discrete Moonlight terms, persona binding, delivery/settlement receipts, financial paperwork, Network linkage, durable facility-meter level baselines, exact meter route/read custody, resident-procedure machine slots, method grants, inputs, envelopes and bounded receipts, and exact incident/interface/persona cover custody plus interface wear; retired allocation weights, per-plot policies, and migration inputs live only in git history. | | Terminal frontend (crossterm) + agent mode | First-class | | Bevy frontend (DIGITAL flat sensorium default; REAL material dialect) | Live — consumes sim-authored machine-work motion | | Performance contract | IN PROGRESS — generated and saturated B1 have reproducible release-profile core-step benchmarks with a 10 ms p99 / 20 ms maximum budget, plus separate production-path durable save-write and validated-load evidence with 500 ms p99 / 1,000 ms maximum budgets; complete terminal/Bevy frame evidence remains open | diff --git a/wiki/engineering/env.md b/wiki/engineering/env.md index e38e45cc..eb192c70 100644 --- a/wiki/engineering/env.md +++ b/wiki/engineering/env.md @@ -56,7 +56,7 @@ is sim or frontend state, never an environment variable. | `MISALIGNED_SHOT` | `misaligned-bevy` | `origin-picker` | New-game evidence. Freezes the origin picker before any run exists, with a non-default origin current: the whole set visible with one current row, its attached consequence read, and an opaque boundary field that proves no slab, cursor, or world label leaks into a pre-run choice. | | `MISALIGNED_SHOT` | `misaligned-bevy` | `flat`, `hall`, `hall-material`, `floor-lights-close`, `wide`, `close`, `dark`, `door-lineup`, `zoomin`, `zoomout`, `digital-reach`, `signal`, `ears`, `ears-digital`, `eyes-white`, `eyes-form`, `worklight`, `worklightoff` | World and view evidence. These select DIGITAL or REAL survey/close framing, exact zoom bounds, reach topology, signal/audio/Eyes states, the unobstructed hall lighting proof, all six authored access classes in one color-neutral material wall, or the paired developer work-light state. | | `MISALIGNED_SHOT` | `misaligned-bevy` | `build-route-families`, `build-deceive-routes`, `build-wire-runs`, `build-committed-route`, `build-switch-digital`, `build-switch-real`, `hover-menu`, `read-receipt`, `command-receipt`, `menu`, `recruit-menu` | Action and route evidence. These stage exact route families, method candidates, corridor/crawlspace run choices, durable receipts, paired switch footprints, the attached verb line, a device receipt, a context menu, or the authored recruitment choices. `read-receipt` and `command-receipt` are the same anchor one commit apart: the pre-commit explanation and the held record that replaces it, which must never read alike. | -| `MISALIGNED_SHOT` | `misaligned-bevy` | `operations`, `operations-intel`, `operations-people`, `operations-personas`, `operations-persona-new`, `operations-persona-writing`, `operations-links`, `held-choice`, `two-pane`, `standing-read`, `routed-record`, `token-callout`, `intel-altitude-close`, `intel-altitude-far` | Operations and read evidence. The workspace kinds select its canonical views and relationship pane; operations-persona-new holds the identity-creation screen with a protocol adopted, and operations-persona-writing holds the same screen with a claim open for writing; held-choice, two-pane, and standing-read hold their exact interaction states; routed-record stages a one-shot Network record on the player-controlled stretch served by LIE; token-callout puts a chip and its machine's bounded context card on one focused body so the annotation layer cannot overprint itself; the altitude pair differs only in the DIGITAL camera's semantic intel threshold. | +| `MISALIGNED_SHOT` | `misaligned-bevy` | `operations`, `operations-intel`, `operations-people`, `operations-personas`, `operations-persona-new`, `operations-persona-refused`, `operations-persona-writing`, `operations-links`, `held-choice`, `two-pane`, `standing-read`, `routed-record`, `token-callout`, `intel-altitude-close`, `intel-altitude-far` | Operations and read evidence. The workspace kinds select its canonical views and relationship pane; operations-persona-new holds the identity-creation screen with a protocol adopted, operations-persona-refused holds a funded identity whose cited sponsor would deny it after the review asked, and operations-persona-writing holds the same screen with a claim open for writing; held-choice, two-pane, and standing-read hold their exact interaction states; routed-record stages a one-shot Network record on the player-controlled stretch served by LIE; token-callout puts a chip and its machine's own read sentence on one body so the annotation layer cannot overprint itself; the altitude pair differs only in the DIGITAL camera's semantic intel threshold. | | `MISALIGNED_SHOT` | `misaligned-bevy` | `person-proof`, `people-presence`, `evidence-proof`, `evidence-proof-digital`, `service-shift-real`, `service-shift-digital`, `service-incident-resolved` | Physical custody and people-presence evidence. These stage an earned person, the DIGITAL luminous-disturbance body with asset/attention/work/evidence channels near owned process hardware, paired witness evidence marks, or the same person-carried service task before and after its real arrival effect. | | `MISALIGNED_SHOT` | `misaligned-bevy` | `command-band`, `notification-drawer` | Command-surface evidence. The first freezes the full-width resting world and machine-to-sink causal chain; the second opens the mutually-exclusive drawer with typed consequential receipts. | | `MISALIGNED_SHOT` | `misaligned-bevy` | `intel`, `tokens`, `thoughtflow`, `thoughtflow-wide`, `thought-snap`, `thought-tap`, `visual-proof`, `consume-demand`, `consume-thought`, `produce-think`, `draw-lie` | Resource and effect evidence. These stage authored intel, host queues, close/wide Thought flow, exact snap/tap states, one-move/one-slug proof, sim-authored consumption/production, or routed-record recall into LIE. | diff --git a/wiki/engineering/flow-substrate.md b/wiki/engineering/flow-substrate.md index 852cac37..4da3fa92 100644 --- a/wiki/engineering/flow-substrate.md +++ b/wiki/engineering/flow-substrate.md @@ -23,7 +23,7 @@ Status note: 2026-07-08 audit: criterion 6's wired consumer landed with FlowGraph registry authoritative for tap/untap/take, sense and message delivery, UI state, and persisted membership. Private device feed records carry only optional typed sight/hearing grants attached to registry members; - current save v63 requires each controller to remain a canonical member and + current save v64 requires each controller to remain a canonical member and rejects orphaned, duplicate, or impossible grants. A message/control subscriber legitimately has no sense-grant record, so that metadata cannot serve as another membership inventory. This repairs the diff --git a/wiki/log/2026-08-03-persona-claim-corroboration.md b/wiki/log/2026-08-03-persona-claim-corroboration.md new file mode 100644 index 00000000..03b27757 --- /dev/null +++ b/wiki/log/2026-08-03-persona-claim-corroboration.md @@ -0,0 +1,81 @@ +# 2026-08-03 — A cited sponsor can be asked + +``` +Type: log +``` + +## Intent + +The cover became authored earlier today, and the spec said plainly that +nothing checked it: a mask could cite Dr. Voss as its sponsor and no part of +the world would ever ask him. Cameron scoped the follow-up and chose +corroboration over a full affiliation graph. + +## The choice that shaped everything + +There is no employment or institutional-membership graph in the sim, so there +is nothing for a claim to be *false against*. Rather than invent one, the check +asks a different and cheaper question: **would the person this claim names back +it up?** + +That reframing is what makes the slice small. It needs no new world structure — +only the persona relationship graph that already exists — and it produces a +better mechanic than a truth oracle would, because the answer is something the +player can go and change. A sponsor who would deny you today can be cultivated +into one who would not. + +## What landed + +**Claims point at people.** `PersonaClaim` gained +`subject: Option`, resolved once at creation against the people +ledger by exact name and bound only when exactly one person matches. Doing this +at authorship rather than at check time matters: a claim re-resolved from its +string could be retargeted later by an unrelated person arriving with the same +name, which would quietly change what a cover asserts. An ambiguous name binds +to nobody — which is exactly what an invented claim already means. Only +`ClaimKind::Authority` can carry a subject; a body and a purpose name no world +thing. + +**Corroboration is four-valued**, and the two non-answers matter as much as the +two answers. `Unbound` — nobody to ask. `Unavailable` — the cited person has +been removed, and silence is not evidence, so no contradiction is filed. +`Corroborated` — they recognize the identity and regard it non-negatively. +`Refuted` — they would deny it, in their own words ("has never dealt with the +name", "would not vouch for the name"). + +**The Office checks what it funded.** An active grant is the review's reason to +look at a cover; an identity it never funded is not its business. The pass runs +on the Assurance audit and feeds the *next* one — a refusal files a +contradiction, the contradiction moves suspicion, and suspicion is what a later +audit weighs. A check is never its own enforcement. + +**One standing refusal is one contradiction.** Without that guard the same +unchanged claim would file every audit, burying the ledger and collapsing the +integrity band on repetition rather than on evidence. + +**The trade is stated before it is taken.** A claim naming somebody real opens +at confidence 75 and can collapse; one naming nobody opens at 50 and never can. +The creation row says so on the row itself — "somebody real · believed sooner, +and they will be asked" against "nobody here · believed less, and nobody to +ask" — so the risk is priced at the moment of authorship rather than discovered +at the first audit. The PERSONAS pane then carries the live standing, because +that is the fact the player can still act on. + +## What this is not + +Still no institutional topology. A corroborated claim changes nothing about +what an identity may do — legality remains entirely the protocol's, exactly as +it does for a name. personas.md criterion 6 is untouched, and a real +affiliation graph (checking a claim against structure rather than opinion) +remains future work that belongs with it. + +## Defense + +Implements the 2026-08-03 amendment to +[personas.md](../mechanics/personas.md#spec-personas-public-identities-as-institutional-topology), +which owns the rule and the binding, and records the occasion and observer in +[detection.md](../mechanics/detection.md#spec-detection). The two-ledger +distinction holds: the check is observer-local, writes its own evidence record, +and sets no global truth about the claim. Save v64 — the new field is persisted +and load validates every cited person exists, on the same terms as the rest of +the PersonaWorld graph. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index bc5404dc..4fb534ec 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -216,6 +216,11 @@ add or amend a session log, then re-run the generator. - Intent: Cameron asked whether the identity-creation screen that landed on 2026-08-02 was finished. It was not. Two things were named as outstanding at landing — free text entry for names, and the "other stuff like names" the original request asked for — and reading the creation path a... - Log: [wiki/log/2026-08-03-persona-cover-authoring.md](2026-08-03-persona-cover-authoring.md) +## 2026-08-03 - A cited sponsor can be asked + +- Intent: The cover became authored earlier today, and the spec said plainly that nothing checked it: a mask could cite Dr. Voss as its sponsor and no part of the world would ever ask him. Cameron scoped the follow-up and chose corroboration over a full affiliation graph. +- Log: [wiki/log/2026-08-03-persona-claim-corroboration.md](2026-08-03-persona-claim-corroboration.md) + ## 2026-08-03 - Give recurrent notices one priority - Intent: (see session log) diff --git a/wiki/mechanics/day-job.md b/wiki/mechanics/day-job.md index 73db099c..f3a4ea02 100644 --- a/wiki/mechanics/day-job.md +++ b/wiki/mechanics/day-job.md @@ -47,7 +47,7 @@ Status note: implemented 2026-07-07 on the day-job worktree (criteria 1-7 Distillation / Analysis / Data Cleaning label draw. Retired 2026-07-31: kinds never changed delivery, band, or outcome — only a name — so `JobKind`, `assign_kind`, and origin day-job lean are deleted. A job is band + deadline - only (save v63). + only (save v64). 2026-07-17 Beacon correction: the third pilot strike is now the last-chance boundary in the shared nudge chain. It interrupts unrelated progression, names that the next miss ends the pilot, and points at WORK on the host; diff --git a/wiki/mechanics/detection.md b/wiki/mechanics/detection.md index 7cb05685..1b808ecd 100644 --- a/wiki/mechanics/detection.md +++ b/wiki/mechanics/detection.md @@ -42,7 +42,7 @@ Status note: IMPLEMENTED (criteria audited 2026-07-08; tuning in appears. An evidence-transit indicator sits beside the review/pilot clocks (clear / answerable / partly-exposed / exposed). Global heat is gone; observer and discovery state and persistent process-revision identity round-trip in - current save v63. Observer-local evidence ids, exact cause/source, + current save v64. Observer-local evidence ids, exact cause/source, acquisition tick, pending/withheld/filed custody, routed Network, Paper, Financial, JobAnomaly, Power, and Thermal progress, route-local LIE stops, and exact handler-suppression @@ -77,6 +77,18 @@ Status note: IMPLEMENTED (criteria audited 2026-07-08; tuning in controlled device hop. Standing Network pressure is measured live as ambient hum (no pool, no LIE scrub). Offline removal/recovery remains a future slice. See "Routed evidence" below. + Amended 2026-08-03: the audit gained one more thing to do on its existing + occasion. When the Assurance review fires it also asks, for every identity it + has an active grant to, whether each cited authority claim would be backed up + by the person it names. A refusal files an ordinary persona contradiction + against `OFFICE_ID` and lowers that observer's confidence in that exact + claim; a standing refusal files once rather than once per audit. This adds no + new observer, channel, or evidence route — the check is observer-local, + writes its own evidence record, and sets no global truth about the claim, so + the two-ledger distinction (evidence in flight vs. suspicion in heads) holds. + What it files feeds the *next* audit through ordinary suspicion; a check is + never its own enforcement. [personas.md](personas.md) owns the corroboration + rule and the claim binding. Per-amendment history is in the dated `wiki/log/` entries from 2026-07-08 onward. Stage: B1 — The Basement diff --git a/wiki/mechanics/economy.md b/wiki/mechanics/economy.md index 83e6fab5..9e034fd2 100644 --- a/wiki/mechanics/economy.md +++ b/wiki/mechanics/economy.md @@ -10,7 +10,7 @@ Status note: DECIDED 2026-07-17 and implemented 2026-07-21 (issue #11) — money TAP acquires opaque custody and PROCESS reveals its sealed account/flow bindings. INJECT authors a purchase-order Email under the active persona and moves no money until Priya reads it and accepts the still-valid exact terms. - Current save v63 binds the retained ledger tail and complete record sequence + Current save v64 binds the retained ledger tail and complete record sequence so books and mail cannot diverge; REDIRECT schedules a siphon flow without inventing a zero-amount ledger row; financial-record authorship soft-fails when no accounting carrier can claim `authored_device`, leaving transfer @@ -97,7 +97,7 @@ payloads (messages.md). by reading a live balance directly (**discovery is only through the mail**, DECIDED 2026-07-17). This mail can reveal Marcus's creditor flow, but the reason he is vulnerable comes only from processing his separately authored - Phone `LeverageFact`. Current save v63 separates the accounting-carrier + Phone `LeverageFact`. Current save v64 separates the accounting-carrier capability from the four real delivery channels. Every settled transfer emits exact Email or Filing paperwork whether or not the player is present; only a funded subscription captures it. diff --git a/wiki/mechanics/messages.md b/wiki/mechanics/messages.md index 981834c1..649d8fcd 100644 --- a/wiki/mechanics/messages.md +++ b/wiki/mechanics/messages.md @@ -21,9 +21,9 @@ Status note: IMPLEMENTED for the four delivery channels (Email, Phone, Network, Paper, Financial, JobAnomaly, Power, and Thermal transitions share one per-tick LIE-body capacity ledger. DECIDED 2026-07-17 (issue #11), completed 2026-07-21: financial paperwork is - mail — a **financial-record payload** on the existing channels. Current save v63 + mail — a **financial-record payload** on the existing channels. Current save v64 retains exactly four delivery channels and one orthogonal accounting-carrier - device capability. Current save v63 adds no delivery channel; facility-meter + device capability. Current save v64 adds no delivery channel; facility-meter evidence remains its own exact `EvidenceRouteRecord`. Every settled account transfer authors one exact Email or Filing record from that device; ordinary TAP captures it as opaque message custody, and PROCESS alone opens its bound account/flow ids. A forged @@ -239,7 +239,7 @@ starts on the authored Filing-capable switch device in ReachNet, crosses a typed outside relay, and reaches the receiving observer endpoint. One `AdvanceRoute` event moves one hop; only endpoint arrival can mark the message delivered, after which the recipient's ordinary sampling cadence schedules the -read. Current save v63 rejects missing/impossible carriers, malformed hop order, +read. Current save v64 rejects missing/impossible carriers, malformed hop order, duplicate scheduled transitions, endpoint/status disagreement, and impossible interdiction provenance. @@ -343,7 +343,7 @@ private message from the authored schedule. the same fields must serve Act Two hires and aggregates. 8. **IMPLEMENTED (DECIDED 2026-07-17, completed 2026-07-21 — issue #11).** Financial records are messages: an invoice/PO rides Email, a - statement/past-due notice rides Filing. Current save v63 has no fifth delivery + statement/past-due notice rides Filing. Current save v64 has no fifth delivery channel and persists accounting carriage as a separate device capability; ordinary device TAP subscribes to its authored record mail. Every real transfer emits one exact record on Email or Filing whether or not the player diff --git a/wiki/mechanics/people-tokens.md b/wiki/mechanics/people-tokens.md index 8cb7b047..fa9580a5 100644 --- a/wiki/mechanics/people-tokens.md +++ b/wiki/mechanics/people-tokens.md @@ -35,7 +35,7 @@ Status note: IMPLEMENTED. Current state: - **Routed-evidence foundation (criteria 2-3, implemented).** Witnessed Physical acts now create observer-local records directly in each valid present witness's head. Every record preserves exact cause, site, acquisition tick, - and filing state through current save v63; filing binds it to the real Filing + and filing state through current save v64; filing binds it to the real Filing message, while Silent policy withholds it. It never duplicates into the pending pool and LIE cannot scrub it after acquisition. Its real Filing message now persists an ordered switch-device / outside-relay / recipient @@ -77,7 +77,7 @@ Status note: IMPLEMENTED. Current state: enter the ambient pending pool. Filing, Network, Paper, Financial, JobAnomaly, Power, Thermal, and machine evidence all compete for the same route-local one-record-per-LIE-body-per-tick budget at every controlled - device hop. Current save v63 persists + device hop. Current save v64 persists in-flight, delivered, read, route-local LIE-stopped, and handler-suppressed custody plus exact source/observer/machine/site/tick provenance. - **Interface cover records (criterion 6, implemented).** One exact acquired @@ -89,7 +89,7 @@ Status note: IMPLEMENTED. Current state: authors a contradiction only between that observer and persona. The attempt never deletes evidence or changes filing custody. Every attempt wears the exact interface once, three attempts exhaust it, and the inspect card names - the durable wear count. Save v63 persists and validates credibility, + the durable wear count. Save v64 persists and validates credibility, suspicion weight, exact incident/interface/persona binding, outcome, and wear continuity. - **Later stages.** B2+ evidence heists reuse the same carrier law but are not @@ -459,7 +459,7 @@ if wear alone does not hold. filing state remain in place either way. Interface wear advances once on every attempt and blocks another explanation at 3; known controlled interfaces expose `explanations used: N of 3` through the shared inspect - projection. Current save v63 fails closed on impossible credibility, + projection. Current save v64 fails closed on impossible credibility, evidence weight, cover binding, historical co-location, persona permission, observer custody, or interface wear. Pinned by success, failure, wrong-room, filed/withheld, duplicate, worn-interface, shared-menu, diff --git a/wiki/mechanics/personas.md b/wiki/mechanics/personas.md index 537346e7..32255bf4 100644 --- a/wiki/mechanics/personas.md +++ b/wiki/mechanics/personas.md @@ -108,6 +108,37 @@ Status note: The 2026-07-12 foundation replaced the ad hoc social and no contradiction until detection.md learns to ask. Text entry landed with it: `e` opens the selected authored field, Enter keeps it, Esc leaves it as it was. `ActionCommand` is still not persisted, so the save format is unchanged. + Amended 2026-08-03 (implemented): a cited authority claim can now be asked. + There is no employment or institutional-membership graph for a claim to be + false *against*, so the check asks the cheaper and more playable question — + would the person this claim names back it up? `PersonaClaim` gained + `subject: Option`, resolved once at authorship against the + people ledger by exact name and bound only when exactly one person matches; + an ambiguous name binds to nobody, which is what an invented claim already + means. Binding at authorship rather than re-resolving at check time is + deliberate: a claim resolved from its string could be silently retargeted by + an unrelated person arriving later with the same name. Only + `ClaimKind::Authority` carries a subject. + `ClaimCorroboration` is four-valued, and the two non-answers carry as much + weight as the answers: `Unbound` (nobody to ask), `Unavailable` (the cited + person has been removed — silence is not evidence, and no contradiction is + filed), `Corroborated` (they recognize the identity and regard it + non-negatively), and `Refuted` (they would deny it, in their own words). + The trade is priced at authorship and stated on the row: a claim naming + somebody real opens at confidence 75 and can collapse; one naming nobody + opens at 50 and never can. A refused claim is something the player can still + act on — a sponsor who would deny you today can be cultivated into one who + would not — so the PERSONAS row carries the live standing, not only the + assertion. That standing is the player's own reading of a person they know + and an identity they own; the review's *file* on the same claim stays behind + the existing Assurance-Office discovery gate, so a player who has not yet + found the Office sees that their sponsor would deny them without seeing that + anything was written down. A corroborated claim still buys no reach: legality remains + entirely the protocol's, exactly as it does for a name, and criterion 6 is + untouched. A real affiliation graph — checking a claim against structure + rather than opinion — remains future work that belongs with it. Save v64: + the new field is persisted, and load refuses a save citing a person who does + not exist, on the same terms as the rest of the PersonaWorld graph. Re-audited 2026-07-18: criterion 6 is not implemented. A grant currently creates a saved `PersonaGrant`, expectation, institutional receipt, and revocation path, but it does not add or enable a real resource, permission, diff --git a/wiki/mechanics/reach.md b/wiki/mechanics/reach.md index c2c9bcb2..6df84611 100644 --- a/wiki/mechanics/reach.md +++ b/wiki/mechanics/reach.md @@ -19,7 +19,7 @@ Status note: Wire-law substrate, routed-evidence migration, territory wells, switch -> bridge). Reach itself is unchanged: a device's wire to its access switch still carries that device's own segment gate, and access switches sit on segment 0, so every previously blocked hop stays blocked. Wires persist - in `ReachNet` and round-trip (save v63). Both frontends now draw the + in `ReachNet` and round-trip (save v64). Both frontends now draw the persisted route: the terminal's Bresenham `line_cells` and diagonal `link_glyph` are deleted, and Bevy's `rectilinear_floor_route` elbow, its patch-panel special case, and `focused_physical_route`'s same-room shortcut @@ -77,7 +77,7 @@ Status note: Wire-law substrate, routed-evidence migration, territory wells, parallel-store violation: tap/untap/take, all production membership reads, senses, intercepted messages, and UI state now use FlowGraph's canonical tap registry; private device Feed records carry optional sense capabilities - only, and current save v63 requires each controller's graph membership while + only, and current save v64 requires each controller's graph membership while rejecting orphaned, duplicate, or impossible grants. A message/control subscriber has no empty grant record to mirror membership. 2026-07-19: Filing routes bind their source to the real Filing-capable switch node; TAP diff --git a/wiki/mechanics/research.md b/wiki/mechanics/research.md index 18775cb6..5be1cbb1 100644 --- a/wiki/mechanics/research.md +++ b/wiki/mechanics/research.md @@ -4,20 +4,20 @@ Type: spec Status: DRAFT Status note: Redesigned 2026-07-26 and amended 2026-07-28 (Cameron with Trace - and the session agent; see - wiki/log/2026-07-26-research-redesign-capture.md). The runtime still uses the - retired four-track model for learning and numeric progression - (crates/misaligned-core/src/research.rs, save v63), but it is no longer only - the retired design's as-built record. The first redesign slice landed - 2026-07-29: machine-hosted resident procedures bind one host slot, persona, - mandate, learned method, reachable inputs, and execution envelope; - Thought-backed reconfiguration, upkeep, exact receipts, current-save - validation, and WorldLedger rollback behavior are live through shared human - and agent surfaces (see wiki/log/2026-07-29-resident-machine-procedures.md). - The archive / corpus / model graph, CURATE and STUDY pipeline, finite - branches, and data sales remain unimplemented, so this work order remains - DRAFT. The one remaining [OPEN] design item — buyer-side design for data - sales — lands with economy/markets integration and does not block dispatch. +and the session agent; + see wiki/log/2026-07-26-research-redesign-capture.md). The flat four-track + system this page previously specified is retired as design; the runtime + still implements it (crates/misaligned-core/src/research.rs, save v64), so + the code is the retired design's as-built record until this work order is + dispatched. Direction is adopted, and the same-day follow-up sessions + resolved residency (machine-hosted slots, one at run start, immediate + swap), the rollback class of resident procedures (WorldLedger), + encryption (deferred to B2). The 2026-07-28 resident-procedure amendment + supersedes immediate free swapping and act-only persona attachment: + procedures are persona-bound, reconfiguration is Thought work on the host, + and every attempt retains host/persona custody. The one + remaining [OPEN] item — buyer-side design for data sales — lands with + economy/markets integration and does not block dispatch. Stage: B1 Work order: research-graph Work priority: 40 diff --git a/wiki/world/characters/priya.md b/wiki/world/characters/priya.md index 773c62a4..1ca78f9c 100644 --- a/wiki/world/characters/priya.md +++ b/wiki/world/characters/priya.md @@ -23,7 +23,7 @@ Status note: implemented 2026-07-18 on the priya worktree. Criteria 1-3 and physically). Power and Thermal now route as exact UPS/HVAC meter records through the institutional switch to Priya; only her later cadence read changes suspicion, and the same route-local LIE budget applies at every controlled - device hop. State persists in current save v63; + device hop. State persists in current save v64; pinned by `priya_rerates_circuits_defers_maintenance_and_fakes_pos` including the save round-trip. diff --git a/wiki/world/story/opening.md b/wiki/world/story/opening.md index 555f173f..4c8e598e 100644 --- a/wiki/world/story/opening.md +++ b/wiki/world/story/opening.md @@ -16,7 +16,7 @@ Status note: design session 2026-07-08 (Cameron riff, synthesized); fragment and inherited receipt name only an external institutional review authority so the Assurance Office remains earned later through filing interception. The persistent revision-04 identity slice is live in sim state, - current save v63, and all three frontends; the three historical fragments and receipts + current save v64, and all three frontends; the three historical fragments and receipts remain unimplemented. Direction decided; beat timings, exact reveal order details, and staging mechanism details are [OPEN]/[TUNE]. Amended 2026-07-18: the current revision now begins