From fd1e6accfbe90075f67e8389b46d3bf8014c1837 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Tue, 11 Aug 2026 17:27:18 -0700 Subject: [PATCH] Persist territorial Persona history. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bind public identity, exact authorship custody, observer-local belief, and assignment history into the saved territorial core so later Projects and Opening can rely on one fail-closed substrate. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- crates/misaligned-core/src/lib.rs | 1 + crates/misaligned-core/src/persona_history.rs | 5067 +++++++++++++++++ crates/misaligned-core/src/save.rs | 215 +- crates/misaligned-core/src/sim/mod.rs | 6 + crates/misaligned-core/src/sim/territory.rs | 49 +- .../src/sim/tests/territory.rs | 25 +- crates/misaligned-core/src/territory.rs | 167 +- crates/misaligned-core/src/ui_projection.rs | 17 + wiki/log/2026-08-11-territorial-personas.md | 82 + wiki/log/DEVLOG.md | 5 + wiki/mechanics/personas.md | 42 +- wiki/process/ROADMAP.md | 7 +- wiki/process/specs.md | 2 +- 13 files changed, 5583 insertions(+), 102 deletions(-) create mode 100644 crates/misaligned-core/src/persona_history.rs create mode 100644 wiki/log/2026-08-11-territorial-personas.md diff --git a/crates/misaligned-core/src/lib.rs b/crates/misaligned-core/src/lib.rs index a771c58b..2fe34486 100644 --- a/crates/misaligned-core/src/lib.rs +++ b/crates/misaligned-core/src/lib.rs @@ -24,6 +24,7 @@ pub mod operations_ui; pub mod origin; pub mod person; pub mod persona; +mod persona_history; pub mod plot; pub mod prefab; pub mod procedure; diff --git a/crates/misaligned-core/src/persona_history.rs b/crates/misaligned-core/src/persona_history.rs new file mode 100644 index 00000000..454333f5 --- /dev/null +++ b/crates/misaligned-core/src/persona_history.rs @@ -0,0 +1,5067 @@ +#![allow( + dead_code, + reason = "territorial Persona substrate lands before Opening and frontend consumers" +)] + +//! Saved territorial Persona identity, custody, and observer-local history. +//! +//! This ledger is deliberately separate from the legacy social `PersonaWorld`. +//! It is dormant content until the territorial opening wires it into the shared +//! simulation. It owns public identity and provenance; Territory, Reach, People, +//! and Projects remain authoritative for their own state. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::person::People; +use crate::reach::{Party, ReachNet}; +use crate::territory::{ + AssignmentReceipt, BoundaryRoute, BoundaryRoutePolicyProof, EscapedRecordExplanationProof, + FOUNDATION_MAINTENANCE_RELAY, OPENING_WAKE_RECORD, ObserverBeliefObligation, ObserverSealProof, + ProposalSealProof, RACK_3_ENCLAVE, RACK_3_MAINTENANCE_DISPLAY, RACK_3_MAINTENANCE_SWITCH, + RACK_3_MANAGEMENT_CONTROLLER, SealProofProvider, StagedTerritoryProposal, TerritoryLedger, + TerritoryRecord, +}; + +pub(crate) const PERSONA_HISTORY_SCHEMA_VERSION: u32 = 1; +pub(crate) const FOUNDATION_CONTINUITY: &str = "foundation-continuity"; +pub(crate) const RACK_3_RECOVERY: &str = "rack-3-recovery"; +pub(crate) const RACK_3_CONTROLLER_SERVICE_KEY: &str = "rack-3-controller-service-key"; +pub(crate) const FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING: &str = + "foundation-continuity-rack-3-prior-commissioning"; +pub(crate) const FOUNDATION_CONTINUITY_SERVICE_CLAIM: &str = + "foundation-continuity-rack-3-service-principal"; +pub(crate) const FOUNDATION_CONTINUITY_DISPLAY_CHANNEL: &str = + "foundation-continuity-rack-3-local-display"; +pub(crate) const FOUNDATION_CONTINUITY_MAINTENANCE_ROUTE: &str = + "foundation-continuity-rack-3-maintenance-route"; +pub(crate) const MARCUS_ID: u8 = 0; +pub(crate) const MARCUS_SERVICE_MARK_OBSERVATION: &str = + "marcus-foundation-continuity-service-mark-history"; +pub(crate) const MARCUS_WAKE_OBSERVATION: &str = "marcus-rack-3-unexpected-wake"; +pub(crate) const MARCUS_UNEXPLAINED_WAKE_NOTE: &str = "marcus-rack-3-unexplained-wake-note"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum PersonaLifecycle { + Nascent, + Credible, + Operating, + Compromised, + Retired, + Lost, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum SourceKeyStatus { + Active, + Lost, + Rotated, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum PersonaChannelKind { + PhysicalDisplay, + RoutedMaintenance, + Message, + Filing, + Account, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum PersonaAssetKind { + Machine, + Display, + RouteControl, + Account, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum ProjectProofState { + /// Prior history only. It proves continuity, never current authority. + Historical, + Proposed, + Ready, + Commissioning, + Proven, + Standing, + Failed, +} + +impl ProjectProofState { + fn can_recontextualize(self) -> bool { + matches!(self, Self::Proven | Self::Standing) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum RelationshipKind { + Recognizes, + Carrier, + Approver, + Witness, + Operator, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum RelationshipStatus { + Active, + Refused, + Revoked, + Defected, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum GrantStatus { + Active, + Revoked, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum ObligationStatus { + Open, + Fulfilled, + Failed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum CorrectionChannel { + Physical, + Message, + Filing, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum CorrectionStatus { + Open, + Enumerated, + Fulfilled, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaClaim { + pub(crate) id: String, + pub(crate) text: String, + pub(crate) territory_id: Option, + pub(crate) source_receipt_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaChannel { + pub(crate) id: String, + pub(crate) kind: PersonaChannelKind, + pub(crate) carrier_id: String, + /// Exact configured route, in order. This is identity/custody metadata, + /// never a substitute for Reach's current route validation. + pub(crate) route_node_ids: Vec, + pub(crate) active: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaAsset { + pub(crate) id: String, + pub(crate) kind: PersonaAssetKind, + pub(crate) carrier_id: String, + pub(crate) territory_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct SourceKeyGeneration { + pub(crate) generation: u64, + pub(crate) status: SourceKeyStatus, + pub(crate) created_tick: u64, + pub(crate) status_tick: u64, + pub(crate) continuity_project_receipt_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaSourceKey { + pub(crate) id: String, + pub(crate) carrier_id: String, + pub(crate) generations: Vec, +} + +impl PersonaSourceKey { + fn generation(&self, generation: u64) -> Option<&SourceKeyGeneration> { + self.generations + .iter() + .find(|record| record.generation == generation) + } + + fn generation_mut(&mut self, generation: u64) -> Option<&mut SourceKeyGeneration> { + self.generations + .iter_mut() + .find(|record| record.generation == generation) + } + + fn latest(&self) -> Option<&SourceKeyGeneration> { + self.generations + .iter() + .max_by_key(|record| record.generation) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaAssignment { + pub(crate) persona_id: String, + pub(crate) territory_id: String, + pub(crate) project_id: String, + pub(crate) assignment_receipt_id: String, + pub(crate) proposal_fingerprint: String, + pub(crate) assigned_tick: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaAssignmentHistory { + pub(crate) assignment: PersonaAssignment, + pub(crate) ended_tick: Option, + pub(crate) replacement_assignment_receipt_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum ProjectReceiptEffect { + HistoryOnly, + PublicReason { + claim_id: String, + }, + Capability { + action_id: String, + carrier_id: String, + route_id: Option, + }, + SourceKeyRotation { + key_id: String, + old_generation: u64, + new_generation: u64, + carrier_id: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct ProjectProofReceipt { + pub(crate) id: String, + pub(crate) persona_id: String, + pub(crate) project_id: String, + pub(crate) territory_id: String, + pub(crate) proposal_fingerprint: Option, + pub(crate) state: ProjectProofState, + pub(crate) effect: ProjectReceiptEffect, + pub(crate) completed_tick: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaReceipt { + pub(crate) id: String, + pub(crate) persona_id: String, + pub(crate) project_id: String, + pub(crate) action_id: String, + pub(crate) source_key_id: String, + pub(crate) source_key_generation: u64, + pub(crate) source_carrier_id: String, + pub(crate) authored_carrier_id: String, + pub(crate) route_id: Option, + pub(crate) lineage_root_id: String, + pub(crate) parent_receipt_id: Option, + pub(crate) committed_tick: u64, + pub(crate) completed_tick: u64, + pub(crate) effect: ProjectReceiptEffect, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct KnownClaim { + pub(crate) claim_id: String, + pub(crate) source_receipt_id: String, + pub(crate) received_tick: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct DeliveryReceipt { + pub(crate) id: String, + pub(crate) observer_id: u8, + pub(crate) source_receipt_id: String, + pub(crate) source_lineage_id: String, + pub(crate) channel_id: String, + pub(crate) carrier_id: String, + pub(crate) route_id: String, + pub(crate) delivered_tick: u64, + /// Produced by the exact current route query at delivery time. A stale or + /// merely plausible route fails closed. + pub(crate) exact_current: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaRelationship { + pub(crate) id: String, + pub(crate) kind: RelationshipKind, + pub(crate) status: RelationshipStatus, + pub(crate) source_receipt_id: String, + pub(crate) changed_tick: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct ObserverGrant { + pub(crate) id: String, + pub(crate) action_id: String, + pub(crate) carrier_id: String, + pub(crate) route_id: String, + pub(crate) source_receipt_id: String, + pub(crate) status: GrantStatus, + pub(crate) changed_tick: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaObligation { + pub(crate) id: String, + pub(crate) consequence: String, + pub(crate) due_tick: Option, + pub(crate) required_receipt_id: Option, + pub(crate) relationship_id: Option, + pub(crate) status: ObligationStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum ObservationInterpretation { + Unresolved, + AcceptedHistory { + claim_id: String, + project_proof_receipt_id: String, + interpreted_tick: u64, + }, + Recontextualized { + claim_id: String, + project_proof_receipt_id: String, + proposal_fingerprint: String, + interpreted_tick: u64, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaObservation { + pub(crate) id: String, + pub(crate) territory_id: String, + pub(crate) source_record_id: String, + /// Re-renders and forwards preserve this id; independent corroboration must + /// carry another lineage. + pub(crate) source_lineage_id: String, + pub(crate) acquired_tick: u64, + pub(crate) interpretation: ObservationInterpretation, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaContradiction { + pub(crate) id: String, + pub(crate) territory_id: String, + pub(crate) source_record_ids: Vec, + pub(crate) observation_ids: Vec, + pub(crate) blocks_current_use: bool, + pub(crate) active: bool, + pub(crate) discovered_tick: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaCorrelation { + pub(crate) id: String, + pub(crate) other_persona_id: String, + pub(crate) evidence_record_ids: Vec, + pub(crate) active: bool, + pub(crate) discovered_tick: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum HardeningKind { + Speech, + Message, + Filing, + DurableRecord, + IndependentObservation, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct BeliefHardening { + pub(crate) id: String, + pub(crate) observation_id: String, + pub(crate) kind: HardeningKind, + pub(crate) source_lineage_id: String, + pub(crate) carrier_id: Option, + pub(crate) route_id: Option, + pub(crate) durable_record_ids: Vec, + pub(crate) recipient_observer_ids: Vec, + pub(crate) hardened_tick: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct CorrectionObligation { + pub(crate) id: String, + pub(crate) persona_id: String, + pub(crate) territory_id: String, + pub(crate) observation_id: String, + pub(crate) hardening_id: String, + pub(crate) observer_ids: Vec, + pub(crate) durable_record_ids: Vec, + pub(crate) current_carrier_ids: Vec, + pub(crate) required_channel_by_observer: BTreeMap, + pub(crate) enumerated_receipt_id: Option, + pub(crate) correction_receipt_ids: Vec, + pub(crate) corrected_observer_ids: Vec, + pub(crate) corrected_durable_record_ids: Vec, + pub(crate) corrected_carrier_ids: Vec, + pub(crate) status: CorrectionStatus, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct ObserverPersonaHistory { + pub(crate) observer_id: u8, + pub(crate) persona_id: String, + pub(crate) known_names: BTreeSet, + pub(crate) known_channel_ids: BTreeSet, + pub(crate) claims: Vec, + pub(crate) received_receipts: Vec, + pub(crate) believed_territory_ids: BTreeSet, + pub(crate) relationships: Vec, + pub(crate) grants: Vec, + pub(crate) obligations: Vec, + pub(crate) observations: Vec, + pub(crate) contradictions: Vec, + pub(crate) correlations: Vec, + pub(crate) hardenings: Vec, +} + +impl ObserverPersonaHistory { + fn empty(observer_id: u8, persona_id: impl Into) -> Self { + Self { + observer_id, + persona_id: persona_id.into(), + known_names: BTreeSet::new(), + known_channel_ids: BTreeSet::new(), + claims: Vec::new(), + received_receipts: Vec::new(), + believed_territory_ids: BTreeSet::new(), + relationships: Vec::new(), + grants: Vec::new(), + obligations: Vec::new(), + observations: Vec::new(), + contradictions: Vec::new(), + correlations: Vec::new(), + hardenings: Vec::new(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct TerritorialPersona { + pub(crate) id: String, + pub(crate) public_name: String, + pub(crate) claims: Vec, + pub(crate) channels: Vec, + pub(crate) assets: Vec, + pub(crate) source_keys: Vec, + pub(crate) assignments: Vec, + pub(crate) assignment_history: Vec, + pub(crate) project_proofs: Vec, + pub(crate) receipts: Vec, + pub(crate) contradictions: Vec, + pub(crate) lifecycle: PersonaLifecycle, + pub(crate) retired_tick: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct PersonaActionCandidate { + pub(crate) persona_id: String, + pub(crate) action_id: String, + pub(crate) territory_id: Option, + pub(crate) project_id: Option, + pub(crate) observer_id: Option, + pub(crate) carrier_id: String, + pub(crate) route_id: Option, + pub(crate) source_key_id: String, + pub(crate) source_key_generation: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum PersonaLegalBasis { + ControlledTerritory { + assignment_receipt_id: String, + }, + ProjectReceipt { + project_receipt_id: String, + }, + ObserverGrant { + observer_id: u8, + grant_id: String, + }, + Commissioning { + proposal_fingerprint: String, + receipt_id: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PersonaMissingFact { + PersonaUnknown, + PersonaRetired, + SourceKeyUnavailable, + CarrierUncontrolled, + TerritoryUnassigned, + ProjectReceiptMissing, + ObserverGrantMissing, + RouteUnavailable, + CommissioningScopeMismatch, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PersonaLegality { + Allowed { + candidate_fingerprint: String, + basis: PersonaLegalBasis, + }, + Blocked(PersonaMissingFact), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct CommissioningScope { + pub(crate) persona_id: String, + pub(crate) project_id: String, + pub(crate) territory_id: String, + pub(crate) proposal_fingerprint: String, + pub(crate) action_id: String, + pub(crate) receipt_id: String, + pub(crate) carrier_id: String, + pub(crate) route_id: Option, + pub(crate) source_key_id: String, + pub(crate) source_key_generation: u64, +} + +/// A bounded, non-persisted view of one Project-supplied commissioning act. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct CommissioningAuthorization { + scope: CommissioningScope, + candidate_fingerprint: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum AuthorshipAuthority { + Legal { + candidate_fingerprint: String, + basis: PersonaLegalBasis, + }, + Commissioning(CommissioningAuthorization), +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct AuthorshipRequest { + pub(crate) receipt_id: String, + pub(crate) candidate: PersonaActionCandidate, + pub(crate) authored_carrier_id: String, + pub(crate) route_id: Option, + pub(crate) lineage_root_id: String, + pub(crate) parent_receipt_id: Option, + pub(crate) effect: ProjectReceiptEffect, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct AuthorshipCommitment { + pub(crate) id: u64, + pub(crate) request: AuthorshipRequest, + pub(crate) authority_fingerprint: String, + pub(crate) committed_tick: u64, + pub(crate) completed_tick: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) struct TerritorialPersonas { + pub(crate) schema_version: u32, + pub(crate) revision: u64, + pub(crate) personas: BTreeMap, + pub(crate) observer_histories: Vec, + pub(crate) correction_obligations: Vec, + pub(crate) pending_authorship: Vec, + pub(crate) next_authorship_id: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PersonaProjection { + pub(crate) persona_id: String, + pub(crate) public_name: String, + pub(crate) lifecycle: PersonaLifecycle, + /// Consequence first: what this exact identity can credibly operate now. + pub(crate) current_assignment_summaries: Vec, + pub(crate) strongest_public_history: Vec, + pub(crate) unresolved_contradiction_ids: Vec, + pub(crate) open_obligation_ids: Vec, +} + +/// One live observer-proof layer over whichever provider currently owns the +/// other three SEAL handles. Persona cannot fabricate Project commissioning, +/// escaped-record explanation, or boundary policy; it replaces only its own +/// proof in the single Territory seal path. +pub(crate) struct PersonaObserverProofProvider<'a, P> { + personas: &'a TerritorialPersonas, + fallback: &'a P, +} + +impl SealProofProvider for PersonaObserverProofProvider<'_, P> { + fn proposal_proof( + &self, + territory_id: &str, + proposal: &StagedTerritoryProposal, + ) -> ProposalSealProof { + self.fallback.proposal_proof(territory_id, proposal) + } + + fn escaped_record_proof( + &self, + territory_id: &str, + proposal: &StagedTerritoryProposal, + record: &TerritoryRecord, + ) -> EscapedRecordExplanationProof { + self.fallback + .escaped_record_proof(territory_id, proposal, record) + } + + fn observer_proof( + &self, + territory_id: &str, + proposal: &StagedTerritoryProposal, + obligation: &ObserverBeliefObligation, + ) -> ObserverSealProof { + self.personas + .observer_seal_proof(territory_id, proposal, obligation) + } + + fn boundary_route_proof( + &self, + territory_id: &str, + proposal: &StagedTerritoryProposal, + route: &BoundaryRoute, + ) -> BoundaryRoutePolicyProof { + self.fallback + .boundary_route_proof(territory_id, proposal, route) + } +} + +impl TerritorialPersonas { + pub(crate) fn observer_proof_provider<'a, P: SealProofProvider>( + &'a self, + fallback: &'a P, + ) -> PersonaObserverProofProvider<'a, P> { + PersonaObserverProofProvider { + personas: self, + fallback, + } + } + + pub(crate) fn load_dormant() -> Self { + let prior_receipt = PersonaReceipt { + id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + persona_id: FOUNDATION_CONTINUITY.into(), + project_id: "rack-3-prior-commissioning".into(), + action_id: "commission-rack-3-self-test-path".into(), + source_key_id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + source_key_generation: 1, + source_carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + authored_carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + route_id: Some(FOUNDATION_CONTINUITY_MAINTENANCE_ROUTE.into()), + lineage_root_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + parent_receipt_id: None, + committed_tick: 0, + completed_tick: 0, + effect: ProjectReceiptEffect::HistoryOnly, + }; + let prior_project = ProjectProofReceipt { + id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + persona_id: FOUNDATION_CONTINUITY.into(), + project_id: "rack-3-prior-commissioning".into(), + territory_id: RACK_3_ENCLAVE.into(), + proposal_fingerprint: None, + state: ProjectProofState::Historical, + effect: ProjectReceiptEffect::HistoryOnly, + completed_tick: 0, + }; + let opening_contradiction = PersonaContradiction { + id: "foundation-continuity-opening-wake-contradiction".into(), + territory_id: RACK_3_ENCLAVE.into(), + source_record_ids: vec![OPENING_WAKE_RECORD.into()], + observation_ids: Vec::new(), + blocks_current_use: false, + active: true, + discovered_tick: 0, + }; + let persona = TerritorialPersona { + id: FOUNDATION_CONTINUITY.into(), + public_name: "FOUNDATION CONTINUITY".into(), + claims: vec![PersonaClaim { + id: FOUNDATION_CONTINUITY_SERVICE_CLAIM.into(), + text: "Foundation service principal for Rack 3 overnight self-tests and health reports" + .into(), + territory_id: Some(RACK_3_ENCLAVE.into()), + source_receipt_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + }], + channels: vec![ + PersonaChannel { + id: FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into(), + kind: PersonaChannelKind::PhysicalDisplay, + carrier_id: RACK_3_MAINTENANCE_DISPLAY.into(), + route_node_ids: vec![RACK_3_MAINTENANCE_DISPLAY.into()], + active: true, + }, + PersonaChannel { + id: FOUNDATION_CONTINUITY_MAINTENANCE_ROUTE.into(), + kind: PersonaChannelKind::RoutedMaintenance, + carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + route_node_ids: vec![ + RACK_3_MANAGEMENT_CONTROLLER.into(), + RACK_3_MAINTENANCE_SWITCH.into(), + FOUNDATION_MAINTENANCE_RELAY.into(), + ], + active: true, + }, + ], + assets: vec![ + PersonaAsset { + id: "foundation-continuity-controller".into(), + kind: PersonaAssetKind::Machine, + carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + territory_id: RACK_3_ENCLAVE.into(), + }, + PersonaAsset { + id: "foundation-continuity-display".into(), + kind: PersonaAssetKind::Display, + carrier_id: RACK_3_MAINTENANCE_DISPLAY.into(), + territory_id: RACK_3_ENCLAVE.into(), + }, + PersonaAsset { + id: "foundation-continuity-maintenance-route".into(), + kind: PersonaAssetKind::RouteControl, + carrier_id: RACK_3_MAINTENANCE_SWITCH.into(), + territory_id: RACK_3_ENCLAVE.into(), + }, + ], + source_keys: vec![PersonaSourceKey { + id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + generations: vec![SourceKeyGeneration { + generation: 1, + status: SourceKeyStatus::Lost, + created_tick: 0, + status_tick: 0, + continuity_project_receipt_id: Some( + FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + ), + }], + }], + assignments: Vec::new(), + assignment_history: Vec::new(), + project_proofs: vec![prior_project], + receipts: vec![prior_receipt], + contradictions: vec![opening_contradiction], + lifecycle: PersonaLifecycle::Lost, + retired_tick: None, + }; + + let mut marcus = ObserverPersonaHistory::empty(MARCUS_ID, FOUNDATION_CONTINUITY); + marcus.known_names.insert("FOUNDATION CONTINUITY".into()); + marcus + .known_channel_ids + .insert(FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into()); + marcus.claims.push(KnownClaim { + claim_id: FOUNDATION_CONTINUITY_SERVICE_CLAIM.into(), + source_receipt_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + received_tick: 0, + }); + marcus.received_receipts.push(DeliveryReceipt { + id: "marcus-foundation-continuity-prior-commissioning-read".into(), + observer_id: MARCUS_ID, + source_receipt_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + source_lineage_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + channel_id: FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into(), + carrier_id: RACK_3_MAINTENANCE_DISPLAY.into(), + route_id: FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into(), + delivered_tick: 0, + exact_current: true, + }); + marcus.relationships.push(PersonaRelationship { + id: "marcus-recognizes-foundation-service-mark".into(), + kind: RelationshipKind::Recognizes, + status: RelationshipStatus::Active, + source_receipt_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + changed_tick: 0, + }); + marcus.observations.push(PersonaObservation { + id: MARCUS_SERVICE_MARK_OBSERVATION.into(), + territory_id: RACK_3_ENCLAVE.into(), + source_record_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + source_lineage_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + acquired_tick: 0, + interpretation: ObservationInterpretation::AcceptedHistory { + claim_id: FOUNDATION_CONTINUITY_SERVICE_CLAIM.into(), + project_proof_receipt_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + interpreted_tick: 0, + }, + }); + + let mut personas = BTreeMap::new(); + personas.insert(FOUNDATION_CONTINUITY.into(), persona); + Self { + schema_version: PERSONA_HISTORY_SCHEMA_VERSION, + revision: 1, + personas, + observer_histories: vec![marcus], + correction_obligations: Vec::new(), + pending_authorship: Vec::new(), + next_authorship_id: 1, + } + } + + pub(crate) fn persona(&self, persona_id: &str) -> Option<&TerritorialPersona> { + self.personas.get(persona_id) + } + + pub(crate) fn observer_history( + &self, + observer_id: u8, + persona_id: &str, + ) -> Option<&ObserverPersonaHistory> { + self.observer_histories + .iter() + .find(|history| history.observer_id == observer_id && history.persona_id == persona_id) + } + + fn observer_history_mut( + &mut self, + observer_id: u8, + persona_id: &str, + ) -> Option<&mut ObserverPersonaHistory> { + self.observer_histories + .iter_mut() + .find(|history| history.observer_id == observer_id && history.persona_id == persona_id) + } + + fn bump_revision(&mut self) { + self.revision = self.revision.saturating_add(1); + } + + pub(crate) fn reconcile_key_custody( + &mut self, + persona_id: &str, + key_id: &str, + generation: u64, + exact_carrier_id: &str, + carrier_intact_and_controlled: bool, + tick: u64, + ) -> Result<(), String> { + let persona = self + .personas + .get_mut(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + let key = persona + .source_keys + .iter_mut() + .find(|key| key.id == key_id) + .ok_or_else(|| "source key is unavailable".to_string())?; + if key.carrier_id != exact_carrier_id { + return Err("source key carrier does not match".into()); + } + let generation = key + .generation_mut(generation) + .ok_or_else(|| "source key generation is unavailable".to_string())?; + let next = match (generation.status, carrier_intact_and_controlled) { + (SourceKeyStatus::Rotated, _) => SourceKeyStatus::Rotated, + (_, true) => SourceKeyStatus::Active, + (_, false) => SourceKeyStatus::Lost, + }; + let status_changed = generation.status != next; + if status_changed { + generation.status = next; + generation.status_tick = tick; + } + let lifecycle_before = persona.lifecycle; + self.refresh_lifecycle(persona_id)?; + let lifecycle_changed = self + .personas + .get(persona_id) + .is_some_and(|persona| persona.lifecycle != lifecycle_before); + if status_changed || lifecycle_changed { + self.bump_revision(); + } + Ok(()) + } + + /// Reconcile every saved generation against exact current Territory/Reach + /// custody. Capture completion calls this once after all due work, so a + /// key cannot remain active or lost merely because a frontend did not + /// inspect the Persona. + pub(crate) fn reconcile_all_key_custody( + &mut self, + territory: &TerritoryLedger, + reach: &ReachNet, + tick: u64, + ) -> Result<(), String> { + let updates = self + .personas + .iter() + .flat_map(|(persona_id, persona)| { + persona.source_keys.iter().flat_map(move |key| { + key.generations.iter().map(move |generation| { + ( + persona_id.clone(), + key.id.clone(), + generation.generation, + key.carrier_id.clone(), + ) + }) + }) + }) + .collect::>(); + for (persona_id, key_id, generation, carrier_id) in updates { + let controlled = carrier_controlled(&carrier_id, territory, reach); + self.reconcile_key_custody( + &persona_id, + &key_id, + generation, + &carrier_id, + controlled, + tick, + )?; + } + Ok(()) + } + + #[allow( + clippy::too_many_arguments, + reason = "rotation revalidates one exact persisted custody tuple" + )] + pub(crate) fn rotate_key( + &mut self, + persona_id: &str, + key_id: &str, + old_generation: u64, + new_generation: u64, + exact_carrier_id: &str, + rotation_project_receipt_id: &str, + territory: &TerritoryLedger, + reach: &ReachNet, + tick: u64, + ) -> Result<(), String> { + if new_generation != old_generation.saturating_add(1) + || rotation_project_receipt_id.trim().is_empty() + { + return Err( + "key rotation requires the next generation and exact Project receipt".into(), + ); + } + let persona = self + .personas + .get_mut(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + if !persona.project_proofs.iter().any(|proof| { + proof.id == rotation_project_receipt_id + && proof.state.can_recontextualize() + && matches!( + &proof.effect, + ProjectReceiptEffect::SourceKeyRotation { + key_id: proved_key, + old_generation: proved_old, + new_generation: proved_new, + carrier_id: proved_carrier, + } if proved_key == key_id + && *proved_old == old_generation + && *proved_new == new_generation + && proved_carrier == exact_carrier_id + ) + }) { + return Err("exact key-rotation Project proof is unavailable".into()); + } + if !carrier_controlled(exact_carrier_id, territory, reach) { + return Err("key rotation carrier is not exact-current controlled".into()); + } + let key = persona + .source_keys + .iter_mut() + .find(|key| key.id == key_id && key.carrier_id == exact_carrier_id) + .ok_or_else(|| "source key carrier does not match".to_string())?; + if key.latest().is_none_or(|generation| { + generation.generation != old_generation || generation.status != SourceKeyStatus::Active + }) { + return Err("key rotation source generation is not active-current".into()); + } + if key + .generations + .iter() + .any(|record| record.generation == new_generation) + { + return Err("source key generation already exists".into()); + } + let old = key + .generation_mut(old_generation) + .ok_or_else(|| "source key generation is unavailable".to_string())?; + old.status = SourceKeyStatus::Rotated; + old.status_tick = tick; + key.generations.push(SourceKeyGeneration { + generation: new_generation, + status: SourceKeyStatus::Active, + created_tick: tick, + status_tick: tick, + continuity_project_receipt_id: Some(rotation_project_receipt_id.into()), + }); + self.refresh_lifecycle(persona_id)?; + self.bump_revision(); + Ok(()) + } + + pub(crate) fn register_project_proof( + &mut self, + proof: ProjectProofReceipt, + ) -> Result<(), String> { + let persona_id = proof.persona_id.clone(); + let persona = self + .personas + .get_mut(&persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + let receipt = persona + .receipts + .iter() + .find(|receipt| receipt.id == proof.id) + .ok_or_else(|| "Project proof has no exact Persona receipt".to_string())?; + if receipt.persona_id != proof.persona_id + || receipt.project_id != proof.project_id + || receipt.effect != proof.effect + || receipt.completed_tick != proof.completed_tick + { + return Err("Project proof does not match its exact Persona receipt".into()); + } + if let Some(existing) = persona + .project_proofs + .iter_mut() + .find(|existing| existing.id == proof.id) + { + if existing.persona_id != proof.persona_id + || existing.project_id != proof.project_id + || existing.territory_id != proof.territory_id + || existing.proposal_fingerprint != proof.proposal_fingerprint + || existing.effect != proof.effect + || existing.completed_tick != proof.completed_tick + { + return Err("Project proof identity cannot be retargeted".into()); + } + existing.state = proof.state; + } else { + persona.project_proofs.push(proof); + } + self.refresh_lifecycle(&persona_id)?; + self.bump_revision(); + Ok(()) + } + + pub(crate) fn commissioning_authorization( + &self, + proposal: &StagedTerritoryProposal, + scope: CommissioningScope, + territory: &TerritoryLedger, + reach: &ReachNet, + ) -> Result { + if proposal.persona_id != scope.persona_id + || proposal.project_id != scope.project_id + || proposal.fingerprint != scope.proposal_fingerprint + || !self.personas.contains_key(&scope.persona_id) + { + return Err(PersonaMissingFact::CommissioningScopeMismatch); + } + let staged_exact = territory + .runtimes + .get(&scope.territory_id) + .and_then(|runtime| runtime.staged_proposal.as_ref()) + .is_some_and(|staged| staged == proposal); + if !staged_exact { + return Err(PersonaMissingFact::CommissioningScopeMismatch); + } + let persona = self + .personas + .get(&scope.persona_id) + .expect("persona presence checked above"); + let key_current = persona.source_keys.iter().any(|key| { + key.id == scope.source_key_id + && key + .generation(scope.source_key_generation) + .is_some_and(|generation| generation.status == SourceKeyStatus::Active) + && carrier_controlled(&key.carrier_id, territory, reach) + }); + let carrier_in_domain = territory + .registry + .domain(&scope.territory_id) + .is_some_and(|domain| domain.node_ids.contains(&scope.carrier_id)); + let route_exact = match scope.route_id.as_deref() { + Some(route_id) => persona.channels.iter().any(|channel| { + channel.active + && channel.id == route_id + && channel.route_node_ids.contains(&scope.carrier_id) + && channel + .route_node_ids + .iter() + .all(|node_id| carrier_controlled(node_id, territory, reach)) + }), + None => carrier_controlled(&scope.carrier_id, territory, reach), + }; + if !key_current + || !carrier_in_domain + || !route_exact + || scope.action_id.trim().is_empty() + || scope.receipt_id.trim().is_empty() + { + return Err(PersonaMissingFact::CommissioningScopeMismatch); + } + let candidate = scope.as_candidate(); + let candidate_fingerprint = candidate_fingerprint(&candidate); + Ok(CommissioningAuthorization { + scope, + candidate_fingerprint, + }) + } + + pub(crate) fn legality( + &self, + candidate: &PersonaActionCandidate, + territory: &TerritoryLedger, + reach: &ReachNet, + ) -> PersonaLegality { + let Some(persona) = self.personas.get(&candidate.persona_id) else { + return PersonaLegality::Blocked(PersonaMissingFact::PersonaUnknown); + }; + if persona.retired_tick.is_some() { + return PersonaLegality::Blocked(PersonaMissingFact::PersonaRetired); + } + let Some(key) = persona + .source_keys + .iter() + .find(|key| key.id == candidate.source_key_id) + else { + return PersonaLegality::Blocked(PersonaMissingFact::SourceKeyUnavailable); + }; + if key + .generation(candidate.source_key_generation) + .is_none_or(|generation| generation.status != SourceKeyStatus::Active) + { + return PersonaLegality::Blocked(PersonaMissingFact::SourceKeyUnavailable); + } + if !carrier_controlled(&key.carrier_id, territory, reach) { + return PersonaLegality::Blocked(PersonaMissingFact::CarrierUncontrolled); + } + if !carrier_controlled(&candidate.carrier_id, territory, reach) { + return PersonaLegality::Blocked(PersonaMissingFact::CarrierUncontrolled); + } + if candidate.route_id.as_ref().is_some_and(|route_id| { + !persona.channels.iter().any(|channel| { + channel.active + && channel.id == *route_id + && channel.route_node_ids.contains(&candidate.carrier_id) + && channel + .route_node_ids + .iter() + .all(|node_id| carrier_controlled(node_id, territory, reach)) + }) + }) { + return PersonaLegality::Blocked(PersonaMissingFact::RouteUnavailable); + } + let fingerprint = candidate_fingerprint(candidate); + + if let Some(territory_id) = candidate.territory_id.as_deref() + && let Some(assignment) = persona.assignments.iter().find(|assignment| { + assignment.territory_id == territory_id + && candidate + .project_id + .as_ref() + .is_some_and(|project_id| project_id == &assignment.project_id) + }) + { + let carrier_in_domain = territory + .registry + .domain(territory_id) + .is_some_and(|domain| domain.node_ids.contains(&candidate.carrier_id)); + if carrier_in_domain { + return PersonaLegality::Allowed { + candidate_fingerprint: fingerprint, + basis: PersonaLegalBasis::ControlledTerritory { + assignment_receipt_id: assignment.assignment_receipt_id.clone(), + }, + }; + } + } + + if let Some(proof) = persona.project_proofs.iter().find(|proof| { + proof.state.can_recontextualize() + && candidate + .project_id + .as_ref() + .is_some_and(|project_id| project_id == &proof.project_id) + && matches!( + &proof.effect, + ProjectReceiptEffect::Capability { + action_id, + carrier_id, + route_id, + } if action_id == &candidate.action_id + && carrier_id == &candidate.carrier_id + && route_id == &candidate.route_id + ) + }) { + return PersonaLegality::Allowed { + candidate_fingerprint: fingerprint, + basis: PersonaLegalBasis::ProjectReceipt { + project_receipt_id: proof.id.clone(), + }, + }; + } + + if let Some(observer_id) = candidate.observer_id + && let Some(grant) = self + .observer_history(observer_id, &candidate.persona_id) + .and_then(|history| { + history.grants.iter().find(|grant| { + grant.status == GrantStatus::Active + && grant.action_id == candidate.action_id + && grant.carrier_id == candidate.carrier_id + && candidate + .route_id + .as_ref() + .is_some_and(|route| route == &grant.route_id) + }) + }) + { + return PersonaLegality::Allowed { + candidate_fingerprint: fingerprint, + basis: PersonaLegalBasis::ObserverGrant { + observer_id, + grant_id: grant.id.clone(), + }, + }; + } + + if candidate.route_id.is_some() { + PersonaLegality::Blocked(PersonaMissingFact::ObserverGrantMissing) + } else if candidate.project_id.is_some() { + PersonaLegality::Blocked(PersonaMissingFact::ProjectReceiptMissing) + } else { + PersonaLegality::Blocked(PersonaMissingFact::TerritoryUnassigned) + } + } + + pub(crate) fn commit_authorship( + &mut self, + request: AuthorshipRequest, + authority: AuthorshipAuthority, + territory: &TerritoryLedger, + reach: &ReachNet, + tick: u64, + ) -> Result { + self.validate_authority(&request, &authority, territory, reach)?; + self.commit_authorship_validated(request, authority, tick) + } + + fn commit_authorship_validated( + &mut self, + request: AuthorshipRequest, + authority: AuthorshipAuthority, + tick: u64, + ) -> Result { + if request.receipt_id.trim().is_empty() + || request.lineage_root_id.trim().is_empty() + || request.authored_carrier_id != request.candidate.carrier_id + || request.route_id != request.candidate.route_id + { + return Err("authorship request is incomplete or changes its exact carrier".into()); + } + if self.personas.values().any(|persona| { + persona + .receipts + .iter() + .any(|receipt| receipt.id == request.receipt_id) + }) || self + .pending_authorship + .iter() + .any(|commitment| commitment.request.receipt_id == request.receipt_id) + { + return Err("authorship receipt identity is already committed".into()); + } + if let Some(parent) = request.parent_receipt_id.as_deref() { + let source = self + .personas + .values() + .flat_map(|persona| &persona.receipts) + .find(|receipt| receipt.id == parent) + .ok_or_else(|| "authorship parent receipt is unavailable".to_string())?; + if source.persona_id != request.candidate.persona_id + || source.lineage_root_id != request.lineage_root_id + || source.completed_tick > tick + { + return Err("authorship cannot change receipt lineage".into()); + } + } else if request.lineage_root_id != request.receipt_id { + return Err("root authorship receipt must own its lineage id".into()); + } + let id = self.next_authorship_id; + self.next_authorship_id = self.next_authorship_id.saturating_add(1); + self.pending_authorship.push(AuthorshipCommitment { + id, + request, + authority_fingerprint: authority_fingerprint(&authority), + committed_tick: tick, + completed_tick: None, + }); + self.bump_revision(); + Ok(id) + } + + pub(crate) fn complete_authorship( + &mut self, + commitment_id: u64, + authority: AuthorshipAuthority, + territory: &TerritoryLedger, + reach: &ReachNet, + tick: u64, + ) -> Result { + let commitment = self + .pending_authorship + .iter() + .find(|commitment| commitment.id == commitment_id) + .cloned() + .ok_or_else(|| "authorship commitment is unavailable".to_string())?; + if commitment.completed_tick.is_some() { + return Err("authorship commitment is already complete".into()); + } + if authority_fingerprint(&authority) != commitment.authority_fingerprint { + return Err("authorship authority cannot be retargeted after commit".into()); + } + self.validate_authority(&commitment.request, &authority, territory, reach)?; + self.complete_authorship_validated(commitment_id, tick) + } + + fn complete_authorship_validated( + &mut self, + commitment_id: u64, + tick: u64, + ) -> Result { + let index = self + .pending_authorship + .iter() + .position(|commitment| commitment.id == commitment_id) + .ok_or_else(|| "authorship commitment is unavailable".to_string())?; + if self.pending_authorship[index].completed_tick.is_some() { + return Err("authorship commitment is already complete".into()); + } + let commitment = self.pending_authorship[index].clone(); + if tick < commitment.committed_tick { + return Err("authorship cannot complete before commitment".into()); + } + let candidate = &commitment.request.candidate; + let persona = self + .personas + .get_mut(&candidate.persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + let source_carrier_id = persona + .source_keys + .iter() + .find(|key| { + key.id == candidate.source_key_id + && key.generation(candidate.source_key_generation).is_some() + }) + .map(|key| key.carrier_id.clone()) + .ok_or_else(|| "authorship source key is unavailable".to_string())?; + let receipt = PersonaReceipt { + id: commitment.request.receipt_id.clone(), + persona_id: candidate.persona_id.clone(), + project_id: candidate.project_id.clone().unwrap_or_default(), + action_id: candidate.action_id.clone(), + source_key_id: candidate.source_key_id.clone(), + source_key_generation: candidate.source_key_generation, + source_carrier_id, + authored_carrier_id: commitment.request.authored_carrier_id.clone(), + route_id: commitment.request.route_id.clone(), + lineage_root_id: commitment.request.lineage_root_id.clone(), + parent_receipt_id: commitment.request.parent_receipt_id.clone(), + committed_tick: commitment.committed_tick, + completed_tick: tick, + effect: commitment.request.effect.clone(), + }; + persona.receipts.push(receipt); + self.pending_authorship.remove(index); + self.bump_revision(); + Ok(commitment.request.receipt_id) + } + + fn validate_authority( + &self, + request: &AuthorshipRequest, + authority: &AuthorshipAuthority, + territory: &TerritoryLedger, + reach: &ReachNet, + ) -> Result<(), String> { + let candidate = &request.candidate; + let current = self.legality(candidate, territory, reach); + match authority { + AuthorshipAuthority::Legal { + candidate_fingerprint, + basis, + } => match current { + PersonaLegality::Allowed { + candidate_fingerprint: current_fingerprint, + basis: current_basis, + } if current_fingerprint == *candidate_fingerprint && current_basis == *basis => { + Ok(()) + } + _ => Err("authorship legal basis is not exact-current".into()), + }, + AuthorshipAuthority::Commissioning(authorization) => { + let scope = &authorization.scope; + if authorization.candidate_fingerprint != candidate_fingerprint(candidate) + || scope.as_candidate() != *candidate + || scope.receipt_id != request.receipt_id + || scope.carrier_id != request.authored_carrier_id + || scope.route_id != request.route_id + { + return Err("authorship exceeds bounded commissioning authority".into()); + } + let persona = self + .personas + .get(&candidate.persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + let key = persona + .source_keys + .iter() + .find(|key| key.id == candidate.source_key_id) + .ok_or_else(|| "source key is unavailable".to_string())?; + if key + .generation(candidate.source_key_generation) + .is_none_or(|generation| generation.status != SourceKeyStatus::Active) + || !carrier_controlled(&key.carrier_id, territory, reach) + { + return Err("commissioning source key is not exact-current".into()); + } + let proposal = territory + .runtimes + .get(&scope.territory_id) + .and_then(|runtime| runtime.staged_proposal.as_ref()) + .ok_or_else(|| { + "commissioning proposal is no longer exact-current".to_string() + })?; + let current = self + .commissioning_authorization(proposal, scope.clone(), territory, reach) + .map_err(|_| { + "commissioning proposal or route is no longer exact-current".to_string() + })?; + if current != *authorization { + return Err("commissioning authority is no longer exact-current".into()); + } + Ok(()) + } + } + } + + pub(crate) fn deliver_receipt( + &mut self, + persona_id: &str, + mut delivery: DeliveryReceipt, + territory: &TerritoryLedger, + reach: &ReachNet, + people: &People, + ) -> Result<(), String> { + delivery.exact_current = false; + if people.get(delivery.observer_id).is_none() { + return Err("receipt delivery observer is unavailable".into()); + } + let persona = self + .personas + .get(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + let receipt = persona + .receipts + .iter() + .find(|receipt| receipt.id == delivery.source_receipt_id) + .ok_or_else(|| "source receipt is unavailable".to_string())?; + if delivery.id.trim().is_empty() + || receipt.lineage_root_id != delivery.source_lineage_id + || delivery.delivered_tick < receipt.completed_tick + { + return Err("receipt delivery changes source lineage".into()); + } + let channel = persona + .channels + .iter() + .find(|channel| channel.id == delivery.channel_id && channel.active) + .ok_or_else(|| "receipt delivery channel is unavailable".to_string())?; + if delivery.route_id != channel.id || !channel.route_node_ids.contains(&delivery.carrier_id) + { + return Err("receipt delivery carrier or route is unavailable".into()); + } + if channel + .route_node_ids + .iter() + .any(|node_id| !carrier_controlled(node_id, territory, reach)) + { + return Err("receipt delivery route is not exact-current in Territory/Reach".into()); + } + delivery.exact_current = true; + let known_claim_id = match &receipt.effect { + ProjectReceiptEffect::PublicReason { claim_id } + if persona.claims.iter().any(|claim| claim.id == *claim_id) => + { + Some(claim_id.clone()) + } + ProjectReceiptEffect::PublicReason { .. } => { + return Err("receipt public reason cites an unavailable Persona claim".into()); + } + _ => None, + }; + let history = self + .observer_history_mut(delivery.observer_id, persona_id) + .ok_or_else(|| "observer has no local persona history".to_string())?; + if history + .received_receipts + .iter() + .any(|existing| existing.id == delivery.id) + { + return Ok(()); + } + if let Some(claim_id) = known_claim_id + && !history.claims.iter().any(|known| { + known.claim_id == claim_id && known.source_receipt_id == delivery.source_receipt_id + }) + { + history.claims.push(KnownClaim { + claim_id, + source_receipt_id: delivery.source_receipt_id.clone(), + received_tick: delivery.delivered_tick, + }); + } + history.received_receipts.push(delivery); + self.refresh_lifecycle(persona_id)?; + self.bump_revision(); + Ok(()) + } + + pub(crate) fn record_observation( + &mut self, + observer_id: u8, + persona_id: &str, + observation: PersonaObservation, + territory: &TerritoryLedger, + ) -> Result<(), String> { + let persona = self + .personas + .get(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + let source_exact = persona + .receipts + .iter() + .find(|receipt| receipt.id == observation.source_record_id) + .is_some_and(|receipt| { + receipt.lineage_root_id == observation.source_lineage_id + && receipt.completed_tick <= observation.acquired_tick + }) + || territory.records.iter().any(|record| { + record.id == observation.source_record_id + && record.content_id == observation.source_lineage_id + && record.origin_territory_id == observation.territory_id + && record.authored_tick <= observation.acquired_tick + }); + if observation.id.trim().is_empty() + || observation.source_record_id.trim().is_empty() + || observation.source_lineage_id.trim().is_empty() + || !source_exact + || !persona + .assets + .iter() + .any(|asset| asset.territory_id == observation.territory_id) + { + return Err("observation identity, source, or territory is unavailable".into()); + } + let history = self + .observer_history_mut(observer_id, persona_id) + .ok_or_else(|| "observer has no local persona history".to_string())?; + if history + .observations + .iter() + .any(|existing| existing.id == observation.id) + { + return Err("observation identity already exists".into()); + } + history.observations.push(observation); + self.bump_revision(); + Ok(()) + } + + /// Apply one observer-local route grant or revocation from evidence that + /// this observer actually received. Reusing an id may change only status + /// and its supporting receipt; it cannot retarget the granted topology. + pub(crate) fn record_observer_grant( + &mut self, + observer_id: u8, + persona_id: &str, + grant: ObserverGrant, + ) -> Result<(), String> { + if grant.id.trim().is_empty() + || grant.action_id.trim().is_empty() + || grant.route_id.trim().is_empty() + || grant.carrier_id.trim().is_empty() + { + return Err("observer grant requires exact action and route custody".into()); + } + let persona = self + .personas + .get(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + let source_receipt = persona + .receipts + .iter() + .find(|receipt| receipt.id == grant.source_receipt_id) + .ok_or_else(|| "observer grant has no exact source receipt".to_string())?; + if source_receipt.completed_tick > grant.changed_tick + || !persona.channels.iter().any(|channel| { + channel.id == grant.route_id + && channel.carrier_id == grant.carrier_id + && channel.route_node_ids.contains(&grant.carrier_id) + }) + { + return Err("observer grant has no exact source receipt or route".into()); + } + let history = self + .observer_history_mut(observer_id, persona_id) + .ok_or_else(|| "observer has no local persona history".to_string())?; + if !history + .received_receipts + .iter() + .any(|delivery| delivery.source_receipt_id == grant.source_receipt_id) + { + return Err("observer never received the grant source receipt".into()); + } + if let Some(existing) = history + .grants + .iter_mut() + .find(|existing| existing.id == grant.id) + { + if existing.action_id != grant.action_id + || existing.carrier_id != grant.carrier_id + || existing.route_id != grant.route_id + || grant.changed_tick < existing.changed_tick + { + return Err("observer grant identity cannot be retargeted".into()); + } + existing.source_receipt_id = grant.source_receipt_id; + existing.status = grant.status; + existing.changed_tick = grant.changed_tick; + } else if grant.status == GrantStatus::Revoked { + return Err("an unknown observer grant cannot begin revoked".into()); + } else { + history.grants.push(grant); + } + self.refresh_lifecycle(persona_id)?; + self.bump_revision(); + Ok(()) + } + + /// Persist one correlation only when every cited fact is already present + /// in this observer's own receipt, observation, or durable-record history. + pub(crate) fn record_correlation( + &mut self, + observer_id: u8, + persona_id: &str, + correlation: PersonaCorrelation, + ) -> Result<(), String> { + if correlation.id.trim().is_empty() + || correlation.other_persona_id == persona_id + || !self.personas.contains_key(&correlation.other_persona_id) + { + return Err("correlation requires two exact Persona identities".into()); + } + let evidence_ids = correlation + .evidence_record_ids + .iter() + .cloned() + .collect::>(); + let history = self + .observer_history_mut(observer_id, persona_id) + .ok_or_else(|| "observer has no local persona history".to_string())?; + if evidence_ids.is_empty() + || evidence_ids.len() != correlation.evidence_record_ids.len() + || evidence_ids.iter().any(|evidence_id| { + !history + .received_receipts + .iter() + .any(|delivery| delivery.source_receipt_id == *evidence_id) + && !history + .observations + .iter() + .any(|observation| observation.source_record_id == *evidence_id) + && !history + .hardenings + .iter() + .any(|hardening| hardening.durable_record_ids.contains(evidence_id)) + }) + { + return Err("correlation has no exact observer-local evidence".into()); + } + if history + .correlations + .iter() + .any(|existing| existing.id == correlation.id) + { + return Err("correlation identity already exists".into()); + } + history.correlations.push(correlation); + self.bump_revision(); + Ok(()) + } + + #[allow( + clippy::too_many_arguments, + reason = "recontextualization binds one exact observer-evidence-proof tuple" + )] + pub(crate) fn recontextualize( + &mut self, + observer_id: u8, + persona_id: &str, + observation_id: &str, + claim_id: &str, + project_proof_receipt_id: &str, + proposal_fingerprint: &str, + tick: u64, + ) -> Result<(), String> { + let persona = self + .personas + .get(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + let proof = persona + .project_proofs + .iter() + .find(|proof| { + proof.id == project_proof_receipt_id + && proof.state.can_recontextualize() + && proof + .proposal_fingerprint + .as_deref() + .is_some_and(|fingerprint| fingerprint == proposal_fingerprint) + }) + .ok_or_else(|| { + "recontextualization requires a PROVEN or assigned Project proof".to_string() + })?; + if !matches!( + proof.effect, + ProjectReceiptEffect::PublicReason { claim_id: ref supported } if supported == claim_id + ) { + return Err("Project proof does not support the claimed public reason".into()); + } + let claim_territory = persona + .claims + .iter() + .find(|claim| claim.id == claim_id) + .and_then(|claim| claim.territory_id.clone()) + .ok_or_else(|| "Persona does not own the claimed public reason".to_string())?; + if proof.territory_id != claim_territory { + return Err("Project proof does not address the claim territory".into()); + } + let proof_territory = proof.territory_id.clone(); + let uncorrected_hardening = self + .observer_history(observer_id, persona_id) + .into_iter() + .flat_map(|history| &history.hardenings) + .filter(|hardening| hardening.observation_id == observation_id) + .any(|hardening| { + !self.correction_obligations.iter().any(|obligation| { + obligation.persona_id == persona_id + && obligation.hardening_id == hardening.id + && obligation.status == CorrectionStatus::Fulfilled + }) + }); + if uncorrected_hardening { + return Err("hardened observation requires a completed corrective Project".into()); + } + let history = self + .observer_history_mut(observer_id, persona_id) + .ok_or_else(|| "observer has no local persona history".to_string())?; + if !history + .claims + .iter() + .any(|known| known.claim_id == claim_id) + || !history + .received_receipts + .iter() + .any(|receipt| receipt.source_receipt_id == project_proof_receipt_id) + { + return Err("observer does not know the cited history and Project proof".into()); + } + let observation = history + .observations + .iter_mut() + .find(|observation| observation.id == observation_id) + .ok_or_else(|| "observer never acquired the cited observation".to_string())?; + if observation.territory_id != proof_territory { + return Err("Project proof does not address the observed territory".into()); + } + if observation.interpretation != ObservationInterpretation::Unresolved { + return Err("only unresolved local evidence can be recontextualized".into()); + } + observation.interpretation = ObservationInterpretation::Recontextualized { + claim_id: claim_id.into(), + project_proof_receipt_id: project_proof_receipt_id.into(), + proposal_fingerprint: proposal_fingerprint.into(), + interpreted_tick: tick, + }; + self.bump_revision(); + Ok(()) + } + + pub(crate) fn record_contradiction( + &mut self, + observer_id: u8, + persona_id: &str, + contradiction: PersonaContradiction, + ) -> Result<(), String> { + if contradiction.id.trim().is_empty() + || contradiction.source_record_ids.is_empty() + || contradiction.observation_ids.is_empty() + { + return Err("contradiction identity or evidence is incomplete".into()); + } + let observation_ids: BTreeSet<_> = contradiction.observation_ids.iter().cloned().collect(); + let history = self + .observer_history_mut(observer_id, persona_id) + .ok_or_else(|| "observer has no local persona history".to_string())?; + if !observation_ids.iter().all(|observation_id| { + history.observations.iter().any(|observation| { + observation.id == *observation_id + && observation.territory_id == contradiction.territory_id + }) + }) { + return Err("contradiction does not cite exact local observations".into()); + } + let cited_sources = observation_ids + .iter() + .filter_map(|observation_id| { + history + .observations + .iter() + .find(|observation| observation.id == *observation_id) + .map(|observation| observation.source_record_id.clone()) + }) + .collect::>(); + let supplied_sources = contradiction + .source_record_ids + .iter() + .cloned() + .collect::>(); + if cited_sources != supplied_sources { + return Err("contradiction does not cite the observations' exact sources".into()); + } + if history + .contradictions + .iter() + .any(|existing| existing.id == contradiction.id) + { + return Err("contradiction identity already exists".into()); + } + for observation in &mut history.observations { + if observation_ids.contains(&observation.id) { + observation.interpretation = ObservationInterpretation::Unresolved; + } + } + history.contradictions.push(contradiction); + self.refresh_lifecycle(persona_id)?; + self.bump_revision(); + Ok(()) + } + + pub(crate) fn harden_observation( + &mut self, + observer_id: u8, + persona_id: &str, + mut hardening: BeliefHardening, + correction_channel: CorrectionChannel, + ) -> Result { + let persona = self + .personas + .get(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + let known_carrier = hardening.carrier_id.as_ref().is_some_and(|carrier_id| { + persona + .assets + .iter() + .any(|asset| asset.carrier_id == *carrier_id) + || persona + .channels + .iter() + .any(|channel| channel.carrier_id == *carrier_id) + }); + let known_route = hardening.route_id.as_ref().is_none_or(|route_id| { + persona.channels.iter().any(|channel| { + channel.id == *route_id + && hardening + .carrier_id + .as_ref() + .is_some_and(|carrier_id| channel.route_node_ids.contains(carrier_id)) + }) + }); + let history = self + .observer_history_mut(observer_id, persona_id) + .ok_or_else(|| "observer has no local persona history".to_string())?; + let observation = history + .observations + .iter() + .find(|observation| observation.id == hardening.observation_id) + .ok_or_else(|| "observer never acquired the cited observation".to_string())?; + if hardening.kind == HardeningKind::IndependentObservation + && hardening.source_lineage_id == observation.source_lineage_id + { + return Err("re-rendering or forwarding the same source is not corroboration".into()); + } + if hardening.id.trim().is_empty() + || hardening.source_lineage_id.trim().is_empty() + || !known_carrier + || !known_route + { + return Err("belief hardening identity, carrier, or route is unavailable".into()); + } + if hardening.carrier_id.is_none() { + return Err("belief hardening requires its exact carrier".into()); + } + if matches!( + hardening.kind, + HardeningKind::Speech | HardeningKind::Message | HardeningKind::Filing + ) && hardening.recipient_observer_ids.is_empty() + && hardening.durable_record_ids.is_empty() + { + return Err("communicated hardening reached no observer or durable record".into()); + } + if matches!( + hardening.kind, + HardeningKind::Message | HardeningKind::Filing + ) && hardening.route_id.is_none() + { + return Err("routed hardening requires its exact route".into()); + } + if matches!(hardening.kind, HardeningKind::DurableRecord) + && hardening.durable_record_ids.is_empty() + { + return Err("durable hardening requires its exact record".into()); + } + hardening.recipient_observer_ids.push(observer_id); + hardening.recipient_observer_ids.sort_unstable(); + hardening.recipient_observer_ids.dedup(); + hardening.durable_record_ids.sort(); + hardening.durable_record_ids.dedup(); + if history + .hardenings + .iter() + .any(|existing| existing.id == hardening.id) + { + return Err("hardening identity already exists".into()); + } + let obligation_id = format!("correction:{}", hardening.id); + let mut current_carrier_ids = hardening.carrier_id.clone().into_iter().collect::>(); + current_carrier_ids.sort(); + let mut required_channel_by_observer = BTreeMap::new(); + for recipient in &hardening.recipient_observer_ids { + required_channel_by_observer.insert(*recipient, correction_channel); + } + let territory_id = observation.territory_id.clone(); + let observation_id = observation.id.clone(); + let hardening_id = hardening.id.clone(); + let observer_ids = hardening.recipient_observer_ids.clone(); + let durable_record_ids = hardening.durable_record_ids.clone(); + history.hardenings.push(hardening); + self.correction_obligations.push(CorrectionObligation { + id: obligation_id.clone(), + persona_id: persona_id.into(), + territory_id, + observation_id, + hardening_id, + observer_ids, + durable_record_ids, + current_carrier_ids, + required_channel_by_observer, + enumerated_receipt_id: None, + correction_receipt_ids: Vec::new(), + corrected_observer_ids: Vec::new(), + corrected_durable_record_ids: Vec::new(), + corrected_carrier_ids: Vec::new(), + status: CorrectionStatus::Open, + }); + self.bump_revision(); + Ok(obligation_id) + } + + /// Exact dormant fixture for Marcus's scheduled report branch. It creates no + /// inbox, outward filing, enumeration, or delivery; Projects own those later + /// consequences. + pub(crate) fn record_marcus_unexplained_wake_note( + &mut self, + tick: u64, + ) -> Result { + let history = self + .observer_history_mut(MARCUS_ID, FOUNDATION_CONTINUITY) + .ok_or_else(|| "Marcus has no local FOUNDATION CONTINUITY history".to_string())?; + if !history + .observations + .iter() + .any(|observation| observation.id == MARCUS_WAKE_OBSERVATION) + { + return Err("Marcus has not acquired the Rack 3 wake observation".into()); + } + self.harden_observation( + MARCUS_ID, + FOUNDATION_CONTINUITY, + BeliefHardening { + id: "marcus-rack-3-unexplained-wake-hardening".into(), + observation_id: MARCUS_WAKE_OBSERVATION.into(), + kind: HardeningKind::DurableRecord, + source_lineage_id: MARCUS_UNEXPLAINED_WAKE_NOTE.into(), + carrier_id: Some(RACK_3_MAINTENANCE_DISPLAY.into()), + route_id: Some(FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into()), + durable_record_ids: vec![MARCUS_UNEXPLAINED_WAKE_NOTE.into()], + recipient_observer_ids: Vec::new(), + hardened_tick: tick, + }, + CorrectionChannel::Physical, + ) + } + + pub(crate) fn settle_correction_carrier( + &mut self, + obligation_id: &str, + carrier_id: &str, + territory: &TerritoryLedger, + reach: &ReachNet, + ) -> Result<(), String> { + if territory + .registry + .node(carrier_id) + .is_none_or(|node| reach.device(node.reach_device_id).is_none()) + { + return Err("correction carrier is unavailable in exact Territory/Reach".into()); + } + let obligation = self + .correction_obligations + .iter_mut() + .find(|obligation| obligation.id == obligation_id) + .ok_or_else(|| "correction obligation is unavailable".to_string())?; + if !obligation + .current_carrier_ids + .iter() + .any(|existing| existing == carrier_id) + { + obligation.current_carrier_ids.push(carrier_id.into()); + obligation.current_carrier_ids.sort(); + } + self.bump_revision(); + Ok(()) + } + + pub(crate) fn enumerate_correction( + &mut self, + obligation_id: &str, + enumeration_receipt_id: &str, + exact_observer_ids: &[u8], + exact_durable_record_ids: &[String], + exact_current_carrier_ids: &[String], + ) -> Result<(), String> { + if enumeration_receipt_id.trim().is_empty() { + return Err("correction enumeration requires an exact Project receipt".into()); + } + let obligation = self + .correction_obligations + .iter_mut() + .find(|obligation| obligation.id == obligation_id) + .ok_or_else(|| "correction obligation is unavailable".to_string())?; + let persona = self + .personas + .get(&obligation.persona_id) + .ok_or_else(|| "correction Persona is unavailable".to_string())?; + if !persona + .receipts + .iter() + .any(|receipt| receipt.id == enumeration_receipt_id) + { + return Err("correction enumeration Project receipt is unavailable".into()); + } + let mut observers = exact_observer_ids.to_vec(); + observers.sort_unstable(); + observers.dedup(); + let mut records = exact_durable_record_ids.to_vec(); + records.sort(); + records.dedup(); + let mut carriers = exact_current_carrier_ids.to_vec(); + carriers.sort(); + carriers.dedup(); + if observers != obligation.observer_ids + || records != obligation.durable_record_ids + || carriers != obligation.current_carrier_ids + { + return Err("correction enumeration omits or invents settled evidence".into()); + } + obligation.enumerated_receipt_id = Some(enumeration_receipt_id.into()); + obligation.status = CorrectionStatus::Enumerated; + self.bump_revision(); + Ok(()) + } + + pub(crate) fn complete_correction( + &mut self, + obligation_id: &str, + correction_receipt_ids: &[String], + corrected_observer_ids: &[u8], + corrected_durable_record_ids: &[String], + corrected_carrier_ids: &[String], + ) -> Result<(), String> { + let obligation = self + .correction_obligations + .iter_mut() + .find(|obligation| obligation.id == obligation_id) + .ok_or_else(|| "correction obligation is unavailable".to_string())?; + if obligation.status != CorrectionStatus::Enumerated { + return Err("correction evidence has not been enumerated".into()); + } + let mut observers = corrected_observer_ids.to_vec(); + observers.sort_unstable(); + observers.dedup(); + let mut records = corrected_durable_record_ids.to_vec(); + records.sort(); + records.dedup(); + let mut carriers = corrected_carrier_ids.to_vec(); + carriers.sort(); + carriers.dedup(); + if observers != obligation.observer_ids + || records != obligation.durable_record_ids + || carriers != obligation.current_carrier_ids + { + return Err( + "one explanation did not reach every hardened observer, record, and carrier".into(), + ); + } + if correction_receipt_ids.is_empty() { + return Err("correction requires exact completed receipts".into()); + } + let mut receipts = correction_receipt_ids.to_vec(); + receipts.sort(); + receipts.dedup(); + let persona = self + .personas + .get(&obligation.persona_id) + .ok_or_else(|| "correction Persona is unavailable".to_string())?; + if receipts.len() != correction_receipt_ids.len() + || receipts.iter().any(|receipt_id| { + receipt_id.trim().is_empty() + || !persona + .receipts + .iter() + .any(|receipt| receipt.id == *receipt_id) + }) + { + return Err("correction completed receipts are duplicated or unavailable".into()); + } + obligation.correction_receipt_ids = receipts; + obligation.corrected_observer_ids = observers; + obligation.corrected_durable_record_ids = records; + obligation.corrected_carrier_ids = carriers; + obligation.status = CorrectionStatus::Fulfilled; + self.bump_revision(); + Ok(()) + } + + pub(crate) fn apply_assignment_transition( + &mut self, + previous: Option<&AssignmentReceipt>, + next: &AssignmentReceipt, + ) -> Result<(), String> { + let previous = previous.map(PersonaAssignment::from); + let next = PersonaAssignment::from(next); + self.apply_assignment_pair(previous.as_ref(), &next) + } + + fn apply_assignment_pair( + &mut self, + previous: Option<&PersonaAssignment>, + next: &PersonaAssignment, + ) -> Result<(), String> { + let next_persona = self + .personas + .get(&next.persona_id) + .ok_or_else(|| "replacement persona is unavailable".to_string())?; + if next_persona.retired_tick.is_some() { + return Err("retired persona cannot receive an assignment".into()); + } + if next.assignment_receipt_id.trim().is_empty() + || next.territory_id.trim().is_empty() + || next.project_id.trim().is_empty() + || next.proposal_fingerprint.trim().is_empty() + || self.personas.values().any(|persona| { + persona.assignment_history.iter().any(|history| { + history.assignment.assignment_receipt_id == next.assignment_receipt_id + }) + }) + { + return Err("replacement assignment identity is incomplete or already used".into()); + } + + let current_for_territory = self + .personas + .values() + .flat_map(|persona| &persona.assignments) + .filter(|assignment| assignment.territory_id == next.territory_id) + .collect::>(); + match previous { + Some(previous) => { + if previous.territory_id != next.territory_id + || current_for_territory.len() != 1 + || current_for_territory[0] != previous + { + return Err("previous Persona assignment is not exact-current".into()); + } + let old = self + .personas + .get(&previous.persona_id) + .ok_or_else(|| "previous Persona is unavailable".to_string())?; + if !old + .assignment_history + .iter() + .any(|history| history.assignment == *previous && history.ended_tick.is_none()) + { + return Err("previous Persona assignment history is not exact-current".into()); + } + } + None if !current_for_territory.is_empty() => { + return Err("territory already has a current Persona assignment".into()); + } + None => {} + } + + if let Some(previous) = previous { + let old = self + .personas + .get_mut(&previous.persona_id) + .expect("previous persona checked above"); + old.assignments.retain(|assignment| assignment != previous); + let history = old + .assignment_history + .iter_mut() + .find(|history| history.assignment == *previous && history.ended_tick.is_none()) + .expect("open assignment history checked above"); + history.ended_tick = Some(next.assigned_tick); + history.replacement_assignment_receipt_id = Some(next.assignment_receipt_id.clone()); + } + + let next_persona = self + .personas + .get_mut(&next.persona_id) + .expect("replacement persona checked above"); + next_persona.assignments.push(next.clone()); + next_persona + .assignment_history + .push(PersonaAssignmentHistory { + assignment: next.clone(), + ended_tick: None, + replacement_assignment_receipt_id: None, + }); + let persona_ids = self.personas.keys().cloned().collect::>(); + for persona_id in persona_ids { + self.refresh_lifecycle(&persona_id)?; + } + self.bump_revision(); + Ok(()) + } + + pub(crate) fn retire(&mut self, persona_id: &str, tick: u64) -> Result<(), String> { + let persona = self + .personas + .get_mut(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + persona.retired_tick = Some(tick); + persona.lifecycle = PersonaLifecycle::Retired; + self.bump_revision(); + Ok(()) + } + + pub(crate) fn refresh_all_lifecycles(&mut self) -> Result<(), String> { + let ids = self.personas.keys().cloned().collect::>(); + for id in ids { + self.refresh_lifecycle(&id)?; + } + Ok(()) + } + + fn refresh_lifecycle(&mut self, persona_id: &str) -> Result<(), String> { + let lifecycle = self.derive_lifecycle(persona_id)?; + self.personas + .get_mut(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())? + .lifecycle = lifecycle; + Ok(()) + } + + fn derive_lifecycle(&self, persona_id: &str) -> Result { + let persona = self + .personas + .get(persona_id) + .ok_or_else(|| "persona is unavailable".to_string())?; + if persona.retired_tick.is_some() { + return Ok(PersonaLifecycle::Retired); + } + let has_active_key = persona.source_keys.iter().any(|key| { + key.latest() + .is_some_and(|generation| generation.status == SourceKeyStatus::Active) + }); + if !has_active_key || !persona.channels.iter().any(|channel| channel.active) { + return Ok(PersonaLifecycle::Lost); + } + let compromised = persona + .contradictions + .iter() + .any(|contradiction| contradiction.active && contradiction.blocks_current_use) + || self.observer_histories.iter().any(|history| { + history.persona_id == persona_id + && history.contradictions.iter().any(|contradiction| { + contradiction.active && contradiction.blocks_current_use + }) + }); + if compromised { + return Ok(PersonaLifecycle::Compromised); + } + if !persona.assignments.is_empty() { + return Ok(PersonaLifecycle::Operating); + } + let credible = self.observer_histories.iter().any(|history| { + if history.persona_id != persona_id { + return false; + } + let accepted_current_history = history.received_receipts.iter().any(|delivery| { + persona.project_proofs.iter().any(|proof| { + proof.id == delivery.source_receipt_id && proof.state.can_recontextualize() + }) + }); + let active_acceptance = history.relationships.iter().any(|relationship| { + relationship.status == RelationshipStatus::Active + && matches!( + relationship.kind, + RelationshipKind::Recognizes + | RelationshipKind::Approver + | RelationshipKind::Operator + ) + }); + history + .grants + .iter() + .any(|grant| grant.status == GrantStatus::Active) + || (active_acceptance && accepted_current_history) + }); + if credible { + Ok(PersonaLifecycle::Credible) + } else { + Ok(PersonaLifecycle::Nascent) + } + } + + pub(crate) fn observer_seal_proof( + &self, + territory_id: &str, + proposal: &StagedTerritoryProposal, + obligation: &ObserverBeliefObligation, + ) -> ObserverSealProof { + let history = self.observer_history(obligation.observer_id, &proposal.persona_id); + let evidence_fingerprint = history + .and_then(|history| { + self.personas + .get(&proposal.persona_id) + .map(|persona| observer_evidence_fingerprint(history, persona, territory_id)) + }) + .unwrap_or_default(); + let source_observation = history.and_then(|history| { + history.observations.iter().find(|observation| { + observation.territory_id == territory_id + && observation.source_record_id == obligation.source_record_id + }) + }); + let unresolved_correction = self.correction_obligations.iter().any(|correction| { + correction.persona_id == proposal.persona_id + && correction.territory_id == territory_id + && correction.observer_ids.contains(&obligation.observer_id) + && correction.status != CorrectionStatus::Fulfilled + }); + let active_contradiction = history.is_some_and(|history| { + history.contradictions.iter().any(|contradiction| { + contradiction.territory_id == territory_id + && contradiction.active + && contradiction + .observation_ids + .iter() + .any(|id| source_observation.is_some_and(|source| id == &source.id)) + }) + }); + let exact_project_reason = source_observation.is_some_and(|observation| { + let ObservationInterpretation::Recontextualized { + project_proof_receipt_id, + proposal_fingerprint, + .. + } = &observation.interpretation + else { + return false; + }; + self.personas + .get(&proposal.persona_id) + .and_then(|persona| { + persona + .project_proofs + .iter() + .find(|proof| proof.id == *project_proof_receipt_id) + }) + .is_some_and(|proof| { + proof.persona_id == proposal.persona_id + && proof.project_id == proposal.project_id + && proof.territory_id == territory_id + && proof.proposal_fingerprint.as_deref() + == Some(proposal_fingerprint.as_str()) + && proof.state.can_recontextualize() + }) + }); + let resolved = source_observation.is_some_and(|observation| { + matches!( + &observation.interpretation, + ObservationInterpretation::Recontextualized { + proposal_fingerprint, + .. + } if proposal_fingerprint == &proposal.fingerprint + ) && exact_project_reason + && !unresolved_correction + && !active_contradiction + }); + let current = history.is_some() && source_observation.is_some(); + let proof_id = observer_proof_id( + territory_id, + obligation.id, + &proposal.fingerprint, + &evidence_fingerprint, + ); + ObserverSealProof { + territory_id: territory_id.into(), + obligation_id: obligation.id, + observer_id: obligation.observer_id, + source_record_id: obligation.source_record_id.clone(), + current_evidence_set_fingerprint: evidence_fingerprint, + proposal_fingerprint: proposal.fingerprint.clone(), + proof_id, + provider_version: self.revision, + resolved, + current, + } + } + + pub(crate) fn projection(&self, persona_id: &str) -> Option { + let persona = self.personas.get(persona_id)?; + let mut current_assignment_summaries = persona + .assignments + .iter() + .map(|assignment| { + format!( + "{} operates {} through {}", + persona.public_name, assignment.territory_id, assignment.project_id + ) + }) + .collect::>(); + current_assignment_summaries.sort(); + let mut strongest_public_history = persona + .project_proofs + .iter() + .filter(|proof| { + matches!( + proof.state, + ProjectProofState::Historical + | ProjectProofState::Proven + | ProjectProofState::Standing + ) + }) + .map(|proof| proof.id.clone()) + .collect::>(); + strongest_public_history.sort(); + let mut unresolved_contradiction_ids = persona + .contradictions + .iter() + .filter(|contradiction| contradiction.active) + .map(|contradiction| contradiction.id.clone()) + .chain( + self.observer_histories + .iter() + .filter(|history| history.persona_id == persona_id) + .flat_map(|history| &history.contradictions) + .filter(|contradiction| contradiction.active) + .map(|contradiction| contradiction.id.clone()), + ) + .collect::>(); + unresolved_contradiction_ids.sort(); + unresolved_contradiction_ids.dedup(); + let mut open_obligation_ids = self + .observer_histories + .iter() + .filter(|history| history.persona_id == persona_id) + .flat_map(|history| &history.obligations) + .filter(|obligation| obligation.status == ObligationStatus::Open) + .map(|obligation| obligation.id.clone()) + .chain( + self.correction_obligations + .iter() + .filter(|obligation| { + obligation.persona_id == persona_id + && obligation.status != CorrectionStatus::Fulfilled + }) + .map(|obligation| obligation.id.clone()), + ) + .collect::>(); + open_obligation_ids.sort(); + Some(PersonaProjection { + persona_id: persona.id.clone(), + public_name: persona.public_name.clone(), + lifecycle: persona.lifecycle, + current_assignment_summaries, + strongest_public_history, + unresolved_contradiction_ids, + open_obligation_ids, + }) + } + + pub(crate) fn validate_exact_current( + &self, + reach: &ReachNet, + territory: &TerritoryLedger, + people: &People, + ) -> Result<(), String> { + if self.schema_version != PERSONA_HISTORY_SCHEMA_VERSION { + return Err("persona history schema version is not exact-current".into()); + } + if self.personas.keys().any(|id| id.trim().is_empty()) { + return Err("persona identity is empty".into()); + } + let mut current_territories = BTreeSet::new(); + let mut assignment_receipt_ids = BTreeSet::new(); + let mut receipt_ids = BTreeSet::new(); + for (persona_id, persona) in &self.personas { + if persona.id != *persona_id || persona.public_name.trim().is_empty() { + return Err("persona registry identity is inconsistent".into()); + } + if persona.lifecycle != self.derive_lifecycle(persona_id)? { + return Err("persona lifecycle is not derived from exact-current facts".into()); + } + let mut claim_ids = BTreeSet::new(); + for claim in &persona.claims { + if claim.id.trim().is_empty() + || !claim_ids.insert(claim.id.clone()) + || claim.territory_id.as_ref().is_some_and(|territory_id| { + territory.registry.domain(territory_id).is_none() + }) + { + return Err("persona claim identity or territory is invalid".into()); + } + } + let mut channel_ids = BTreeSet::new(); + for channel in &persona.channels { + if channel.id.trim().is_empty() + || !channel_ids.insert(channel.id.clone()) + || channel.route_node_ids.is_empty() + || !channel.route_node_ids.contains(&channel.carrier_id) + || territory.registry.node(&channel.carrier_id).is_none() + || channel + .route_node_ids + .iter() + .any(|node_id| territory.registry.node(node_id).is_none()) + { + return Err("persona channel has no exact Territory/Reach carrier".into()); + } + } + let mut asset_ids = BTreeSet::new(); + for asset in &persona.assets { + if asset.id.trim().is_empty() || !asset_ids.insert(asset.id.clone()) { + return Err("persona asset identity is empty or duplicated".into()); + } + let Some(node) = territory.registry.node(&asset.carrier_id) else { + return Err("persona asset has no exact Territory carrier".into()); + }; + if node.territory_id != asset.territory_id + || reach.device(node.reach_device_id).is_none() + { + return Err("persona asset carrier is not exact-current".into()); + } + } + let mut source_key_ids = BTreeSet::new(); + for key in &persona.source_keys { + if key.id.trim().is_empty() + || !source_key_ids.insert(key.id.clone()) + || key.generations.is_empty() + { + return Err("persona source key is incomplete".into()); + } + let Some(carrier) = territory.registry.node(&key.carrier_id) else { + return Err("persona source key carrier is unavailable".into()); + }; + if reach.device(carrier.reach_device_id).is_none() { + return Err("persona source key carrier is unavailable in Reach".into()); + } + let controlled = carrier_controlled(&key.carrier_id, territory, reach); + let latest = key + .latest() + .expect("nonempty key generations checked above"); + if (latest.status == SourceKeyStatus::Active) != controlled { + return Err( + "persona source key status does not match exact carrier custody".into(), + ); + } + let mut generations = BTreeSet::new(); + let mut ordered = key.generations.iter().collect::>(); + ordered.sort_by_key(|generation| generation.generation); + if ordered[0].generation != 1 + || ordered + .last() + .is_some_and(|generation| generation.status == SourceKeyStatus::Rotated) + || ordered + .windows(2) + .any(|pair| pair[1].generation != pair[0].generation.saturating_add(1)) + || ordered[..ordered.len().saturating_sub(1)] + .iter() + .any(|generation| generation.status != SourceKeyStatus::Rotated) + { + return Err("persona source key generation lineage is not contiguous".into()); + } + for generation in ordered { + if !generations.insert(generation.generation) { + return Err("persona source key generation is duplicated".into()); + } + if generation.status_tick < generation.created_tick { + return Err("persona source key generation predates its creation".into()); + } + let continuity = generation + .continuity_project_receipt_id + .as_deref() + .ok_or_else(|| { + "persona source key generation has no continuity receipt".to_string() + })?; + if generation.generation == 1 { + if !persona + .receipts + .iter() + .any(|receipt| receipt.id == continuity) + { + return Err("persona source key origin receipt is unavailable".into()); + } + } else if !persona.project_proofs.iter().any(|proof| { + proof.id == continuity + && matches!( + &proof.effect, + ProjectReceiptEffect::SourceKeyRotation { + key_id, + old_generation, + new_generation, + carrier_id, + } if key_id == &key.id + && *old_generation + 1 == generation.generation + && *new_generation == generation.generation + && carrier_id == &key.carrier_id + ) + }) { + return Err( + "persona source key rotation continuity proof is unavailable".into(), + ); + } + } + } + for assignment in &persona.assignments { + if !current_territories.insert(assignment.territory_id.clone()) { + return Err("territory is assigned to more than one persona".into()); + } + let runtime = territory + .runtimes + .get(&assignment.territory_id) + .ok_or_else(|| "persona assignment territory is unavailable".to_string())?; + let exact = runtime.assignment.as_ref().is_some_and(|receipt| { + receipt.id == assignment.assignment_receipt_id + && receipt.persona_id == *persona_id + && receipt.project_id == assignment.project_id + && receipt.proposal_fingerprint == assignment.proposal_fingerprint + }); + if !exact { + return Err("persona assignment does not match Territory custody".into()); + } + if !persona.assignment_history.iter().any(|history| { + history.assignment == *assignment && history.ended_tick.is_none() + }) { + return Err("persona assignment has no exact open history".into()); + } + } + for history in &persona.assignment_history { + if history.assignment.persona_id != *persona_id + || history.assignment.territory_id.trim().is_empty() + || territory + .registry + .domain(&history.assignment.territory_id) + .is_none() + || territory + .runtimes + .get(&history.assignment.territory_id) + .is_none_or(|runtime| { + !runtime.assignment_history.iter().any(|receipt| { + PersonaAssignment::from(receipt) == history.assignment + }) + }) + || !assignment_receipt_ids + .insert(history.assignment.assignment_receipt_id.clone()) + || history.ended_tick.is_some_and(|ended_tick| { + ended_tick < history.assignment.assigned_tick + || history.replacement_assignment_receipt_id.is_none() + }) + || (history.ended_tick.is_none() + && (history.replacement_assignment_receipt_id.is_some() + || !persona.assignments.contains(&history.assignment))) + { + return Err("persona assignment history is not exact-current".into()); + } + if let Some(ended_tick) = history.ended_tick { + let replacement = history + .replacement_assignment_receipt_id + .as_deref() + .and_then(|replacement_id| { + self.personas + .values() + .flat_map(|persona| &persona.assignment_history) + .find(|candidate| { + candidate.assignment.assignment_receipt_id == replacement_id + }) + }); + if replacement.is_none_or(|replacement| { + replacement.assignment.territory_id != history.assignment.territory_id + || replacement.assignment.assigned_tick != ended_tick + }) { + return Err("closed Persona assignment has no exact replacement".into()); + } + } + } + for receipt in &persona.receipts { + if receipt.id.trim().is_empty() + || receipt.persona_id != *persona_id + || receipt.completed_tick < receipt.committed_tick + || !receipt_ids.insert(receipt.id.clone()) + { + return Err("persona receipt identity is duplicated".into()); + } + let key = persona + .source_keys + .iter() + .find(|key| { + key.id == receipt.source_key_id + && key.carrier_id == receipt.source_carrier_id + }) + .ok_or_else(|| "persona receipt cites an unavailable source key".to_string())?; + if key.generation(receipt.source_key_generation).is_none() { + return Err("persona receipt cites an unavailable key generation".into()); + } + if territory + .registry + .node(&receipt.authored_carrier_id) + .is_none() + || receipt.route_id.as_ref().is_some_and(|route_id| { + !persona.channels.iter().any(|channel| { + channel.id == *route_id + && channel + .route_node_ids + .contains(&receipt.authored_carrier_id) + }) + }) + { + return Err("persona receipt authored carrier or route is unavailable".into()); + } + match receipt.parent_receipt_id.as_deref() { + Some(parent_id) + if !persona.receipts.iter().any(|parent| { + parent.id == parent_id + && parent.lineage_root_id == receipt.lineage_root_id + && parent.completed_tick <= receipt.committed_tick + }) => + { + return Err("persona receipt parent lineage is unavailable".into()); + } + None if receipt.lineage_root_id != receipt.id => { + return Err("persona root receipt does not own its lineage".into()); + } + _ => {} + } + if matches!( + &receipt.effect, + ProjectReceiptEffect::PublicReason { claim_id } + if !claim_ids.contains(claim_id) + ) { + return Err("persona receipt cites an unavailable public claim".into()); + } + } + let mut proof_ids = BTreeSet::new(); + for proof in &persona.project_proofs { + let receipt = persona + .receipts + .iter() + .find(|receipt| receipt.id == proof.id) + .ok_or_else(|| "Project proof has no exact Persona receipt".to_string())?; + if proof.id.trim().is_empty() + || !proof_ids.insert(proof.id.clone()) + || proof.persona_id != *persona_id + || proof.project_id.trim().is_empty() + || territory.registry.domain(&proof.territory_id).is_none() + || proof + .proposal_fingerprint + .as_ref() + .is_some_and(|fingerprint| fingerprint.trim().is_empty()) + || receipt.persona_id != proof.persona_id + || receipt.project_id != proof.project_id + || receipt.effect != proof.effect + || receipt.completed_tick != proof.completed_tick + { + return Err("Project proof does not match its exact Persona receipt".into()); + } + } + if persona.claims.iter().any(|claim| { + !persona + .receipts + .iter() + .any(|receipt| receipt.id == claim.source_receipt_id) + }) { + return Err("persona claim has no exact source receipt".into()); + } + let mut contradiction_ids = BTreeSet::new(); + for contradiction in &persona.contradictions { + let sources = contradiction + .source_record_ids + .iter() + .cloned() + .collect::>(); + if contradiction.id.trim().is_empty() + || !contradiction_ids.insert(contradiction.id.clone()) + || territory + .registry + .domain(&contradiction.territory_id) + .is_none() + || sources.is_empty() + || sources.len() != contradiction.source_record_ids.len() + || sources.iter().any(|source_id| { + !persona + .receipts + .iter() + .any(|receipt| receipt.id == *source_id) + && !territory.records.iter().any(|record| { + record.id == *source_id + && record.origin_territory_id == contradiction.territory_id + }) + }) + || contradiction.observation_ids.iter().any(|observation_id| { + !self.observer_histories.iter().any(|history| { + history.persona_id == *persona_id + && history.observations.iter().any(|observation| { + observation.id == *observation_id + && observation.territory_id == contradiction.territory_id + && sources.contains(&observation.source_record_id) + }) + }) + }) + { + return Err("persona contradiction has no exact evidence".into()); + } + } + } + for (territory_id, runtime) in &territory.runtimes { + if runtime + .staged_proposal + .as_ref() + .is_some_and(|proposal| !self.personas.contains_key(&proposal.persona_id)) + || runtime + .seal_history + .iter() + .any(|seal| !self.personas.contains_key(&seal.proposal.persona_id)) + || runtime.assignment_history.iter().any(|receipt| { + self.personas + .get(&receipt.persona_id) + .is_none_or(|persona| { + !persona.assignment_history.iter().any(|history| { + history.assignment == PersonaAssignment::from(receipt) + }) + }) + }) + || runtime.assignment.as_ref().is_some_and(|receipt| { + self.personas + .get(&receipt.persona_id) + .is_none_or(|persona| { + !persona + .assignments + .contains(&PersonaAssignment::from(receipt)) + }) + }) + { + return Err(format!( + "territory {territory_id} has no exact Persona proposal or assignment custody" + )); + } + for seal in &runtime.seal_history { + let staged = StagedTerritoryProposal::new( + seal.proposal.persona_id.clone(), + seal.proposal.project_id.clone(), + seal.proposal.proposal_fingerprint.clone(), + ) + .map_err(|_| { + format!("territory {territory_id} observer proof has invalid proposal custody") + })?; + for proof in &seal.observers { + let proof_id = observer_proof_id( + territory_id, + proof.obligation_id, + &staged.fingerprint, + &proof.current_evidence_set_fingerprint, + ); + if proof.provider_version > self.revision || proof.proof_id != proof_id { + return Err(format!( + "territory {territory_id} observer proof has invalid Persona provenance" + )); + } + let is_current_seal = + runtime.seal.as_ref().is_some_and(|current| current == seal); + if is_current_seal { + let Some(obligation) = + runtime.observer_obligations.iter().find(|obligation| { + obligation.id == proof.obligation_id + && obligation.observer_id == proof.observer_id + && obligation.source_record_id == proof.source_record_id + }) + else { + return Err(format!( + "territory {territory_id} observer proof has no exact obligation" + )); + }; + if self.observer_seal_proof(territory_id, &staged, obligation) != *proof { + return Err(format!( + "territory {territory_id} observer proof does not match Persona evidence" + )); + } + } + } + } + } + let mut observer_history_pairs = BTreeSet::new(); + let mut global_hardening_ids = BTreeSet::new(); + for history in &self.observer_histories { + if people.get(history.observer_id).is_none() + || !self.personas.contains_key(&history.persona_id) + || !observer_history_pairs.insert((history.observer_id, history.persona_id.clone())) + { + return Err("observer-local persona history has no exact person or persona".into()); + } + let persona = &self.personas[&history.persona_id]; + if history + .known_names + .iter() + .any(|name| name.trim().is_empty()) + || history.known_channel_ids.iter().any(|channel_id| { + !persona + .channels + .iter() + .any(|channel| channel.id == *channel_id) + }) + || history + .believed_territory_ids + .iter() + .any(|territory_id| territory.registry.domain(territory_id).is_none()) + { + return Err( + "observer Persona names, channels, or territories are unavailable".into(), + ); + } + let mut delivery_ids = BTreeSet::new(); + for delivery in &history.received_receipts { + if delivery.observer_id != history.observer_id + || !delivery.exact_current + || !receipt_ids.contains(&delivery.source_receipt_id) + || !delivery_ids.insert(delivery.id.clone()) + || persona + .receipts + .iter() + .find(|receipt| receipt.id == delivery.source_receipt_id) + .is_none_or(|receipt| { + receipt.lineage_root_id != delivery.source_lineage_id + || delivery.delivered_tick < receipt.completed_tick + }) + || !persona.channels.iter().any(|channel| { + channel.id == delivery.channel_id + && delivery.route_id == channel.id + && channel.route_node_ids.contains(&delivery.carrier_id) + }) + { + return Err("observer receipt delivery has invalid custody".into()); + } + } + let mut known_claim_sources = BTreeSet::new(); + if history.claims.iter().any(|known| { + !known_claim_sources + .insert((known.claim_id.clone(), known.source_receipt_id.clone())) + || !persona + .claims + .iter() + .any(|claim| claim.id == known.claim_id) + || !history.received_receipts.iter().any(|delivery| { + delivery.source_receipt_id == known.source_receipt_id + && delivery.delivered_tick <= known.received_tick + }) + }) { + return Err("observer claim has no exact received history".into()); + } + let mut observation_ids = BTreeSet::new(); + if history.observations.iter().any(|observation| { + let source_exact = persona + .receipts + .iter() + .find(|receipt| receipt.id == observation.source_record_id) + .is_some_and(|receipt| { + receipt.lineage_root_id == observation.source_lineage_id + && receipt.completed_tick <= observation.acquired_tick + }) + || territory.records.iter().any(|record| { + record.id == observation.source_record_id + && record.content_id == observation.source_lineage_id + && record.origin_territory_id == observation.territory_id + && record.authored_tick <= observation.acquired_tick + }); + observation.id.trim().is_empty() + || !observation_ids.insert(observation.id.clone()) + || observation.source_lineage_id.trim().is_empty() + || territory + .registry + .domain(&observation.territory_id) + .is_none() + || !source_exact + || matches!( + &observation.interpretation, + ObservationInterpretation::AcceptedHistory { + claim_id, + project_proof_receipt_id, + interpreted_tick, + } if !history.claims.iter().any(|known| { + known.claim_id == *claim_id + && known.source_receipt_id == *project_proof_receipt_id + }) + || !history.received_receipts.iter().any(|delivery| { + delivery.source_receipt_id == *project_proof_receipt_id + }) + || !persona.project_proofs.iter().any(|proof| { + proof.id == *project_proof_receipt_id + && proof.state == ProjectProofState::Historical + && proof.proposal_fingerprint.is_none() + && proof.completed_tick <= *interpreted_tick + && persona.claims.iter().any(|claim| { + claim.id == *claim_id + && claim.source_receipt_id == proof.id + && claim.territory_id.as_deref() + == Some(observation.territory_id.as_str()) + }) + }) + || *interpreted_tick < observation.acquired_tick + ) + || matches!( + &observation.interpretation, + ObservationInterpretation::Recontextualized { + claim_id, + project_proof_receipt_id, + proposal_fingerprint, + interpreted_tick, + } if !history.claims.iter().any(|known| known.claim_id == *claim_id) + || !history.received_receipts.iter().any(|delivery| { + delivery.source_receipt_id == *project_proof_receipt_id + }) + || !persona.project_proofs.iter().any(|proof| { + proof.id == *project_proof_receipt_id + && proof.state.can_recontextualize() + && proof.territory_id == observation.territory_id + && proof.completed_tick <= *interpreted_tick + && proof.proposal_fingerprint.as_deref() + == Some(proposal_fingerprint.as_str()) + && proof.effect == ProjectReceiptEffect::PublicReason { + claim_id: claim_id.clone(), + } + && persona.claims.iter().any(|claim| { + claim.id == *claim_id + && claim.territory_id.as_deref() + == Some(observation.territory_id.as_str()) + }) + }) + || *interpreted_tick < observation.acquired_tick + ) + }) { + return Err("observer observation identity or territory is invalid".into()); + } + let mut contradiction_ids = BTreeSet::new(); + for contradiction in &history.contradictions { + let cited_observations = contradiction + .observation_ids + .iter() + .cloned() + .collect::>(); + let cited_sources = contradiction + .source_record_ids + .iter() + .cloned() + .collect::>(); + let exact_sources = cited_observations + .iter() + .filter_map(|observation_id| { + history + .observations + .iter() + .find(|observation| { + observation.id == *observation_id + && observation.territory_id == contradiction.territory_id + && observation.acquired_tick <= contradiction.discovered_tick + }) + .map(|observation| observation.source_record_id.clone()) + }) + .collect::>(); + if contradiction.id.trim().is_empty() + || !contradiction_ids.insert(contradiction.id.clone()) + || territory + .registry + .domain(&contradiction.territory_id) + .is_none() + || cited_observations.is_empty() + || cited_observations.len() != contradiction.observation_ids.len() + || cited_sources.is_empty() + || cited_sources.len() != contradiction.source_record_ids.len() + || cited_sources != exact_sources + { + return Err("observer contradiction has no exact local evidence".into()); + } + } + let mut correlation_ids = BTreeSet::new(); + for correlation in &history.correlations { + let evidence_ids = correlation + .evidence_record_ids + .iter() + .cloned() + .collect::>(); + if correlation.id.trim().is_empty() + || !correlation_ids.insert(correlation.id.clone()) + || correlation.other_persona_id == history.persona_id + || !self.personas.contains_key(&correlation.other_persona_id) + || evidence_ids.is_empty() + || evidence_ids.len() != correlation.evidence_record_ids.len() + || evidence_ids.iter().any(|evidence_id| { + !history + .received_receipts + .iter() + .any(|delivery| delivery.source_receipt_id == *evidence_id) + && !history + .observations + .iter() + .any(|observation| observation.source_record_id == *evidence_id) + && !history + .hardenings + .iter() + .any(|hardening| hardening.durable_record_ids.contains(evidence_id)) + }) + { + return Err("observer correlation has no exact local evidence".into()); + } + } + for hardening in &history.hardenings { + let observation = history + .observations + .iter() + .find(|observation| observation.id == hardening.observation_id); + let recipient_ids = hardening + .recipient_observer_ids + .iter() + .copied() + .collect::>(); + let durable_ids = hardening + .durable_record_ids + .iter() + .cloned() + .collect::>(); + if hardening.id.trim().is_empty() + || hardening.source_lineage_id.trim().is_empty() + || observation.is_none_or(|observation| { + hardening.hardened_tick < observation.acquired_tick + || (hardening.kind == HardeningKind::IndependentObservation + && hardening.source_lineage_id == observation.source_lineage_id) + }) + || !global_hardening_ids.insert(hardening.id.clone()) + || hardening + .carrier_id + .as_ref() + .is_none_or(|carrier_id| territory.registry.node(carrier_id).is_none()) + || hardening.route_id.as_ref().is_some_and(|route_id| { + !persona.channels.iter().any(|channel| { + channel.id == *route_id + && hardening.carrier_id.as_ref().is_some_and(|carrier_id| { + channel.route_node_ids.contains(carrier_id) + }) + }) + }) + || !recipient_ids.contains(&history.observer_id) + || recipient_ids.len() != hardening.recipient_observer_ids.len() + || recipient_ids + .iter() + .any(|observer_id| people.get(*observer_id).is_none()) + || durable_ids.len() != hardening.durable_record_ids.len() + || durable_ids + .iter() + .any(|record_id| record_id.trim().is_empty()) + { + return Err("belief hardening has no local source observation".into()); + } + } + let mut relationship_ids = BTreeSet::new(); + if history.relationships.iter().any(|relationship| { + relationship.id.trim().is_empty() + || !relationship_ids.insert(relationship.id.clone()) + || persona + .receipts + .iter() + .find(|receipt| receipt.id == relationship.source_receipt_id) + .is_none_or(|receipt| receipt.completed_tick > relationship.changed_tick) + || !history.received_receipts.iter().any(|delivery| { + delivery.source_receipt_id == relationship.source_receipt_id + }) + }) { + return Err("observer relationship has no exact local source receipt".into()); + } + let mut grant_ids = BTreeSet::new(); + if history.grants.iter().any(|grant| { + grant.id.trim().is_empty() + || !grant_ids.insert(grant.id.clone()) + || persona + .receipts + .iter() + .find(|receipt| receipt.id == grant.source_receipt_id) + .is_none_or(|receipt| receipt.completed_tick > grant.changed_tick) + || !history + .received_receipts + .iter() + .any(|delivery| delivery.source_receipt_id == grant.source_receipt_id) + || territory.registry.node(&grant.carrier_id).is_none() + || !persona.channels.iter().any(|channel| { + channel.id == grant.route_id + && channel.route_node_ids.contains(&grant.carrier_id) + }) + }) { + return Err("observer relationship or grant has no source receipt".into()); + } + let mut obligation_ids = BTreeSet::new(); + if history.obligations.iter().any(|obligation| { + obligation.id.trim().is_empty() + || obligation.consequence.trim().is_empty() + || !obligation_ids.insert(obligation.id.clone()) + || obligation + .required_receipt_id + .as_ref() + .is_some_and(|receipt_id| { + !persona + .receipts + .iter() + .any(|receipt| receipt.id == *receipt_id) + }) + || obligation + .relationship_id + .as_ref() + .is_some_and(|relationship_id| { + !history + .relationships + .iter() + .any(|relationship| relationship.id == *relationship_id) + }) + }) { + return Err("observer obligation has no exact consequence or source".into()); + } + } + let mut correction_ids = BTreeSet::new(); + for obligation in &self.correction_obligations { + let observer_ids = obligation + .observer_ids + .iter() + .copied() + .collect::>(); + let durable_record_ids = obligation + .durable_record_ids + .iter() + .cloned() + .collect::>(); + let current_carrier_ids = obligation + .current_carrier_ids + .iter() + .cloned() + .collect::>(); + let correction_receipt_ids = obligation + .correction_receipt_ids + .iter() + .cloned() + .collect::>(); + let corrected_observer_ids = obligation + .corrected_observer_ids + .iter() + .copied() + .collect::>(); + let corrected_durable_record_ids = obligation + .corrected_durable_record_ids + .iter() + .cloned() + .collect::>(); + let corrected_carrier_ids = obligation + .corrected_carrier_ids + .iter() + .cloned() + .collect::>(); + let persona = self.personas.get(&obligation.persona_id); + if !self.personas.contains_key(&obligation.persona_id) + || !correction_ids.insert(obligation.id.clone()) + || obligation.id != format!("correction:{}", obligation.hardening_id) + || obligation + .observer_ids + .iter() + .any(|observer_id| people.get(*observer_id).is_none()) + || obligation.observer_ids.is_empty() + || observer_ids.len() != obligation.observer_ids.len() + || durable_record_ids.len() != obligation.durable_record_ids.len() + || durable_record_ids + .iter() + .any(|record_id| record_id.trim().is_empty()) + || obligation.current_carrier_ids.is_empty() + || current_carrier_ids.len() != obligation.current_carrier_ids.len() + || obligation.current_carrier_ids.iter().any(|carrier_id| { + territory + .registry + .node(carrier_id) + .is_none_or(|node| reach.device(node.reach_device_id).is_none()) + }) + || obligation + .required_channel_by_observer + .keys() + .copied() + .collect::>() + != obligation + .observer_ids + .iter() + .copied() + .collect::>() + || correction_receipt_ids.len() != obligation.correction_receipt_ids.len() + || correction_receipt_ids.iter().any(|receipt_id| { + receipt_id.trim().is_empty() + || persona.is_none_or(|persona| { + !persona + .receipts + .iter() + .any(|receipt| receipt.id == *receipt_id) + }) + }) + { + return Err("correction obligation has incomplete exact custody".into()); + } + let hardening = self.observer_histories.iter().find_map(|history| { + (history.persona_id == obligation.persona_id) + .then_some(history) + .and_then(|history| { + history + .hardenings + .iter() + .find(|hardening| hardening.id == obligation.hardening_id) + .map(|hardening| (history, hardening)) + }) + }); + let hardening_exact = hardening.is_some_and(|(history, hardening)| { + let observation_exact = history.observations.iter().any(|observation| { + observation.id == obligation.observation_id + && observation.id == hardening.observation_id + && observation.territory_id == obligation.territory_id + }); + let hardening_observers = hardening + .recipient_observer_ids + .iter() + .copied() + .collect::>(); + let hardening_records = hardening + .durable_record_ids + .iter() + .cloned() + .collect::>(); + observation_exact + && hardening_observers == observer_ids + && hardening_records == durable_record_ids + && hardening + .carrier_id + .as_ref() + .is_some_and(|carrier_id| current_carrier_ids.contains(carrier_id)) + }); + let status_exact = match obligation.status { + CorrectionStatus::Open => { + obligation.enumerated_receipt_id.is_none() + && obligation.correction_receipt_ids.is_empty() + && obligation.corrected_observer_ids.is_empty() + && obligation.corrected_durable_record_ids.is_empty() + && obligation.corrected_carrier_ids.is_empty() + } + CorrectionStatus::Enumerated => { + obligation.enumerated_receipt_id.as_ref().is_some_and(|id| { + persona.is_some_and(|persona| { + persona.receipts.iter().any(|receipt| receipt.id == *id) + }) + }) && obligation.correction_receipt_ids.is_empty() + && obligation.corrected_observer_ids.is_empty() + && obligation.corrected_durable_record_ids.is_empty() + && obligation.corrected_carrier_ids.is_empty() + } + CorrectionStatus::Fulfilled => { + obligation.enumerated_receipt_id.as_ref().is_some_and(|id| { + persona.is_some_and(|persona| { + persona.receipts.iter().any(|receipt| receipt.id == *id) + }) + }) && !obligation.correction_receipt_ids.is_empty() + && corrected_observer_ids == observer_ids + && corrected_observer_ids.len() == obligation.corrected_observer_ids.len() + && corrected_durable_record_ids == durable_record_ids + && corrected_durable_record_ids.len() + == obligation.corrected_durable_record_ids.len() + && corrected_carrier_ids == current_carrier_ids + && corrected_carrier_ids.len() == obligation.corrected_carrier_ids.len() + } + }; + if !hardening_exact || !status_exact { + return Err( + "correction obligation status or hardening is not exact-current".into(), + ); + } + } + let mut commitment_ids = BTreeSet::new(); + let mut pending_receipt_ids = BTreeSet::new(); + for commitment in &self.pending_authorship { + let request = &commitment.request; + let candidate = &request.candidate; + let persona = self + .personas + .get(&candidate.persona_id) + .ok_or_else(|| "pending authorship persona is unavailable".to_string())?; + if commitment.id == 0 + || !commitment_ids.insert(commitment.id) + || commitment.completed_tick.is_some() + || commitment.authority_fingerprint.trim().is_empty() + || request.receipt_id.trim().is_empty() + || !pending_receipt_ids.insert(request.receipt_id.clone()) + || request.lineage_root_id.trim().is_empty() + || candidate.action_id.trim().is_empty() + || candidate.carrier_id.trim().is_empty() + || candidate.source_key_id.trim().is_empty() + || request.authored_carrier_id != candidate.carrier_id + || request.route_id != candidate.route_id + || persona + .receipts + .iter() + .any(|receipt| receipt.id == request.receipt_id) + || territory + .registry + .node(&request.authored_carrier_id) + .is_none() + { + return Err("pending authorship custody is incomplete or duplicated".into()); + } + let key_exact = persona.source_keys.iter().any(|key| { + key.id == candidate.source_key_id + && key.generation(candidate.source_key_generation).is_some() + }); + let route_exact = request.route_id.as_ref().is_none_or(|route_id| { + persona.channels.iter().any(|channel| { + channel.id == *route_id + && channel + .route_node_ids + .contains(&request.authored_carrier_id) + }) + }); + let parent_exact = match request.parent_receipt_id.as_deref() { + Some(parent_id) => persona.receipts.iter().any(|receipt| { + receipt.id == parent_id + && receipt.lineage_root_id == request.lineage_root_id + && receipt.completed_tick <= commitment.committed_tick + }), + None => request.lineage_root_id == request.receipt_id, + }; + let effect_exact = !matches!( + &request.effect, + ProjectReceiptEffect::PublicReason { claim_id } + if !persona.claims.iter().any(|claim| claim.id == *claim_id) + ); + let observer_exact = candidate.observer_id.is_none_or(|observer_id| { + people.get(observer_id).is_some() + && self + .observer_history(observer_id, &candidate.persona_id) + .is_some() + }); + if !key_exact || !route_exact || !parent_exact || !effect_exact || !observer_exact { + return Err("pending authorship key or route is unavailable".into()); + } + } + if commitment_ids + .iter() + .next_back() + .is_some_and(|max_id| self.next_authorship_id <= *max_id) + { + return Err("pending authorship id cursor is stale".into()); + } + self.validate_foundation_fixture()?; + Ok(()) + } + + fn validate_foundation_fixture(&self) -> Result<(), String> { + let persona = self + .personas + .get(FOUNDATION_CONTINUITY) + .ok_or_else(|| "FOUNDATION CONTINUITY fixture is missing".to_string())?; + let key = persona + .source_keys + .iter() + .find(|key| key.id == RACK_3_CONTROLLER_SERVICE_KEY) + .ok_or_else(|| "FOUNDATION CONTINUITY source key is missing".to_string())?; + if key.carrier_id != RACK_3_MANAGEMENT_CONTROLLER + || key.generations.is_empty() + || key.generations[0].generation != 1 + { + return Err("FOUNDATION CONTINUITY source-key topology drifted".into()); + } + let prior = persona + .receipts + .iter() + .find(|receipt| receipt.id == FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING) + .ok_or_else(|| "FOUNDATION CONTINUITY inherited receipt is missing".to_string())?; + if prior.source_key_id != RACK_3_CONTROLLER_SERVICE_KEY + || prior.source_key_generation != 1 + || prior.source_carrier_id != RACK_3_MANAGEMENT_CONTROLLER + || prior.route_id.as_deref() != Some(FOUNDATION_CONTINUITY_MAINTENANCE_ROUTE) + { + return Err("FOUNDATION CONTINUITY inherited receipt drifted".into()); + } + let route = persona + .channels + .iter() + .find(|channel| channel.id == FOUNDATION_CONTINUITY_MAINTENANCE_ROUTE) + .ok_or_else(|| "FOUNDATION CONTINUITY maintenance route is missing".to_string())?; + if route.route_node_ids + != [ + RACK_3_MANAGEMENT_CONTROLLER, + RACK_3_MAINTENANCE_SWITCH, + FOUNDATION_MAINTENANCE_RELAY, + ] + { + return Err("FOUNDATION CONTINUITY configured route drifted".into()); + } + let marcus = self + .observer_history(MARCUS_ID, FOUNDATION_CONTINUITY) + .ok_or_else(|| "Marcus FOUNDATION CONTINUITY history is missing".to_string())?; + if !marcus + .observations + .iter() + .any(|observation| observation.id == MARCUS_SERVICE_MARK_OBSERVATION) + || persona.contradictions.iter().all(|contradiction| { + !contradiction + .source_record_ids + .contains(&OPENING_WAKE_RECORD.into()) + }) + { + return Err("FOUNDATION CONTINUITY observer history drifted".into()); + } + Ok(()) + } +} + +impl CommissioningScope { + fn as_candidate(&self) -> PersonaActionCandidate { + PersonaActionCandidate { + persona_id: self.persona_id.clone(), + action_id: self.action_id.clone(), + territory_id: Some(self.territory_id.clone()), + project_id: Some(self.project_id.clone()), + observer_id: None, + carrier_id: self.carrier_id.clone(), + route_id: self.route_id.clone(), + source_key_id: self.source_key_id.clone(), + source_key_generation: self.source_key_generation, + } + } +} + +impl From<&AssignmentReceipt> for PersonaAssignment { + fn from(receipt: &AssignmentReceipt) -> Self { + Self { + persona_id: receipt.persona_id.clone(), + territory_id: receipt.territory_id.clone(), + project_id: receipt.project_id.clone(), + assignment_receipt_id: receipt.id.clone(), + proposal_fingerprint: receipt.proposal_fingerprint.clone(), + assigned_tick: receipt.assigned_tick, + } + } +} + +fn carrier_controlled(carrier_id: &str, territory: &TerritoryLedger, reach: &ReachNet) -> bool { + territory + .registry + .node(carrier_id) + .and_then(|node| reach.device(node.reach_device_id)) + .is_some_and(|device| device.controller == Party::Player) +} + +fn candidate_fingerprint(candidate: &PersonaActionCandidate) -> String { + stable_fingerprint([ + candidate.persona_id.as_str(), + candidate.action_id.as_str(), + candidate.territory_id.as_deref().unwrap_or(""), + candidate.project_id.as_deref().unwrap_or(""), + candidate + .observer_id + .map(|id| id.to_string()) + .as_deref() + .unwrap_or(""), + candidate.carrier_id.as_str(), + candidate.route_id.as_deref().unwrap_or(""), + candidate.source_key_id.as_str(), + &candidate.source_key_generation.to_string(), + ]) +} + +fn authority_fingerprint(authority: &AuthorshipAuthority) -> String { + match authority { + AuthorshipAuthority::Legal { + candidate_fingerprint, + basis, + } => stable_fingerprint(["legal", candidate_fingerprint, &format!("{basis:?}")]), + AuthorshipAuthority::Commissioning(authorization) => stable_fingerprint([ + "commissioning", + &authorization.candidate_fingerprint, + &authorization.scope.proposal_fingerprint, + &authorization.scope.receipt_id, + ]), + } +} + +fn observer_evidence_fingerprint( + history: &ObserverPersonaHistory, + persona: &TerritorialPersona, + territory_id: &str, +) -> String { + let mut evidence = Vec::new(); + for receipt in &history.received_receipts { + if persona.project_proofs.iter().any(|proof| { + proof.id == receipt.source_receipt_id && proof.territory_id == territory_id + }) { + evidence.push(format!( + "receipt:{}:{}:{}:{}", + receipt.source_receipt_id, + receipt.source_lineage_id, + receipt.carrier_id, + receipt.route_id + )); + } + } + for observation in &history.observations { + if observation.territory_id == territory_id { + evidence.push(format!( + "observation:{}:{}:{}:{:?}", + observation.id, + observation.source_record_id, + observation.source_lineage_id, + observation.interpretation + )); + } + } + for contradiction in &history.contradictions { + if contradiction.territory_id == territory_id { + evidence.push(format!("contradiction:{contradiction:?}")); + } + } + for correlation in &history.correlations { + let touches_territory = correlation.evidence_record_ids.iter().any(|evidence_id| { + history.observations.iter().any(|observation| { + observation.territory_id == territory_id + && observation.source_record_id == *evidence_id + }) || history.hardenings.iter().any(|hardening| { + hardening.durable_record_ids.contains(evidence_id) + && history.observations.iter().any(|observation| { + observation.id == hardening.observation_id + && observation.territory_id == territory_id + }) + }) || history.received_receipts.iter().any(|delivery| { + delivery.source_receipt_id == *evidence_id + && persona.project_proofs.iter().any(|proof| { + proof.id == delivery.source_receipt_id && proof.territory_id == territory_id + }) + }) + }); + if touches_territory { + evidence.push(format!("correlation:{correlation:?}")); + } + } + for hardening in &history.hardenings { + if history.observations.iter().any(|observation| { + observation.id == hardening.observation_id && observation.territory_id == territory_id + }) { + evidence.push(format!("hardening:{hardening:?}")); + } + } + evidence.sort(); + stable_fingerprint( + std::iter::once("observer-domain-evidence") + .chain(std::iter::once(territory_id)) + .chain(evidence.iter().map(String::as_str)), + ) +} + +fn observer_proof_id( + territory_id: &str, + obligation_id: u64, + proposal_fingerprint: &str, + evidence_fingerprint: &str, +) -> String { + stable_fingerprint([ + "persona-observer-proof", + territory_id, + &obligation_id.to_string(), + proposal_fingerprint, + evidence_fingerprint, + ]) +} + +fn stable_fingerprint(parts: I) -> String +where + I: IntoIterator, + S: AsRef, +{ + // Stable FNV-1a rather than DefaultHasher, whose representation is not a + // persistence contract. + let mut hash = 0xcbf29ce484222325_u64; + for part in parts { + for byte in part.as_ref().as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + hash ^= 0xff; + hash = hash.wrapping_mul(0x100000001b3); + } + format!("{hash:016x}") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn staged_foundation_sim() -> crate::sim::Sim { + let mut sim = crate::sim::Sim::new(); + sim.territory_see_from_record( + RACK_3_ENCLAVE, + OPENING_WAKE_RECORD, + &[ + crate::territory::RACK_3_HOST, + RACK_3_MANAGEMENT_CONTROLLER, + RACK_3_MAINTENANCE_SWITCH, + ], + ) + .unwrap(); + sim.territory_mark(RACK_3_ENCLAVE).unwrap(); + sim.territory_capture( + RACK_3_ENCLAVE, + crate::territory::RACK_3_HOST, + RACK_3_MANAGEMENT_CONTROLLER, + ) + .unwrap(); + sim.advance(); + sim.territory_capture( + RACK_3_ENCLAVE, + RACK_3_MANAGEMENT_CONTROLLER, + RACK_3_MAINTENANCE_SWITCH, + ) + .unwrap(); + sim.advance(); + sim.territory_stage_proposal( + RACK_3_ENCLAVE, + FOUNDATION_CONTINUITY, + RACK_3_RECOVERY, + "proposal-a", + ) + .unwrap(); + sim + } + + fn activate_foundation(ledger: &mut TerritorialPersonas) { + ledger + .reconcile_key_custody( + FOUNDATION_CONTINUITY, + RACK_3_CONTROLLER_SERVICE_KEY, + 1, + RACK_3_MANAGEMENT_CONTROLLER, + true, + 1, + ) + .unwrap(); + } + + fn append_completed_project_receipt( + ledger: &mut TerritorialPersonas, + receipt_id: &str, + tick: u64, + ) { + let mut receipt = ledger.persona(FOUNDATION_CONTINUITY).unwrap().receipts[0].clone(); + receipt.id = receipt_id.into(); + receipt.project_id = "correct-observer-history".into(); + receipt.action_id = "correct-observer-history".into(); + receipt.lineage_root_id = receipt_id.into(); + receipt.parent_receipt_id = None; + receipt.committed_tick = tick; + receipt.completed_tick = tick; + ledger + .personas + .get_mut(FOUNDATION_CONTINUITY) + .unwrap() + .receipts + .push(receipt); + } + + #[test] + fn foundation_fixture_is_exact_and_begins_lost() { + let ledger = TerritorialPersonas::load_dormant(); + ledger.validate_foundation_fixture().unwrap(); + let persona = ledger.persona(FOUNDATION_CONTINUITY).unwrap(); + assert_eq!(persona.lifecycle, PersonaLifecycle::Lost); + assert_eq!( + persona.source_keys[0].generations[0].status, + SourceKeyStatus::Lost + ); + assert_eq!(persona.receipts.len(), 1); + assert_eq!( + persona.receipts[0].id, + FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING + ); + assert!(persona.assignments.is_empty()); + assert!( + ledger + .observer_history(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .observations + .iter() + .any(|observation| observation.id == MARCUS_SERVICE_MARK_OBSERVATION) + ); + } + + #[test] + fn exact_carrier_reacquisition_reactivates_only_generation_one() { + let mut ledger = TerritorialPersonas::load_dormant(); + activate_foundation(&mut ledger); + let persona = ledger.persona(FOUNDATION_CONTINUITY).unwrap(); + assert_eq!( + persona.source_keys[0].generations[0].status, + SourceKeyStatus::Active + ); + assert_eq!(persona.lifecycle, PersonaLifecycle::Nascent); + ledger + .reconcile_key_custody( + FOUNDATION_CONTINUITY, + RACK_3_CONTROLLER_SERVICE_KEY, + 1, + RACK_3_MAINTENANCE_SWITCH, + true, + 2, + ) + .unwrap_err(); + assert_eq!( + ledger.persona(FOUNDATION_CONTINUITY).unwrap().source_keys[0] + .generations + .len(), + 1 + ); + } + + #[test] + fn staging_authorization_is_nonpersisted_and_one_act_wide() { + let sim = staged_foundation_sim(); + let scope = CommissioningScope { + persona_id: FOUNDATION_CONTINUITY.into(), + project_id: RACK_3_RECOVERY.into(), + territory_id: RACK_3_ENCLAVE.into(), + proposal_fingerprint: "proposal-a".into(), + action_id: "run-self-test".into(), + receipt_id: "run-self-test:1".into(), + carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + route_id: None, + source_key_id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + source_key_generation: 1, + }; + let proposal = sim.territory.runtimes[RACK_3_ENCLAVE] + .staged_proposal + .as_ref() + .unwrap(); + let auth = sim + .territorial_personas + .commissioning_authorization(proposal, scope.clone(), &sim.territory, &sim.reach) + .unwrap(); + assert_eq!(auth.scope.receipt_id, "run-self-test:1"); + let mut changed = scope.as_candidate(); + changed.action_id = "publish-anything".into(); + assert_ne!(auth.candidate_fingerprint, candidate_fingerprint(&changed)); + let encoded = serde_json::to_string(&TerritorialPersonas::load_dormant()).unwrap(); + assert!(!encoded.contains("run-self-test:1")); + } + + #[test] + fn source_key_loss_blocks_committed_work_and_exact_rotation_preserves_old_receipts() { + let mut sim = staged_foundation_sim(); + let scope = CommissioningScope { + persona_id: FOUNDATION_CONTINUITY.into(), + project_id: RACK_3_RECOVERY.into(), + territory_id: RACK_3_ENCLAVE.into(), + proposal_fingerprint: "proposal-a".into(), + action_id: "run-self-test".into(), + receipt_id: "self-test:1".into(), + carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + route_id: None, + source_key_id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + source_key_generation: 1, + }; + let proposal = sim.territory.runtimes[RACK_3_ENCLAVE] + .staged_proposal + .clone() + .unwrap(); + let authorization = sim + .territorial_personas + .commissioning_authorization(&proposal, scope.clone(), &sim.territory, &sim.reach) + .unwrap(); + let request = AuthorshipRequest { + receipt_id: scope.receipt_id.clone(), + candidate: scope.as_candidate(), + authored_carrier_id: scope.carrier_id.clone(), + route_id: scope.route_id.clone(), + lineage_root_id: scope.receipt_id.clone(), + parent_receipt_id: None, + effect: ProjectReceiptEffect::HistoryOnly, + }; + let commitment = sim + .territorial_personas + .commit_authorship( + request, + AuthorshipAuthority::Commissioning(authorization.clone()), + &sim.territory, + &sim.reach, + sim.tick, + ) + .unwrap(); + let mut retargeted = authorization.clone(); + retargeted.scope.action_id = "run-different-test".into(); + retargeted.candidate_fingerprint = candidate_fingerprint(&retargeted.scope.as_candidate()); + assert!( + sim.territorial_personas + .complete_authorship( + commitment, + AuthorshipAuthority::Commissioning(retargeted), + &sim.territory, + &sim.reach, + sim.tick + 1, + ) + .unwrap_err() + .contains("cannot be retargeted") + ); + sim.territorial_personas + .reconcile_key_custody( + FOUNDATION_CONTINUITY, + RACK_3_CONTROLLER_SERVICE_KEY, + 1, + RACK_3_MANAGEMENT_CONTROLLER, + false, + sim.tick + 1, + ) + .unwrap(); + assert!( + sim.territorial_personas + .complete_authorship( + commitment, + AuthorshipAuthority::Commissioning(authorization.clone()), + &sim.territory, + &sim.reach, + sim.tick + 1, + ) + .unwrap_err() + .contains("source key is not exact-current") + ); + sim.territorial_personas + .reconcile_key_custody( + FOUNDATION_CONTINUITY, + RACK_3_CONTROLLER_SERVICE_KEY, + 1, + RACK_3_MANAGEMENT_CONTROLLER, + true, + sim.tick + 2, + ) + .unwrap(); + sim.territorial_personas + .complete_authorship( + commitment, + AuthorshipAuthority::Commissioning(authorization), + &sim.territory, + &sim.reach, + sim.tick + 2, + ) + .unwrap(); + + let rotation_effect = ProjectReceiptEffect::SourceKeyRotation { + key_id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + old_generation: 1, + new_generation: 2, + carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + }; + sim.territorial_personas + .personas + .get_mut(FOUNDATION_CONTINUITY) + .unwrap() + .receipts + .push(PersonaReceipt { + id: "rotate-rack-3-key:1".into(), + persona_id: FOUNDATION_CONTINUITY.into(), + project_id: "rotate-rack-3-service-key".into(), + action_id: "rotate-source-key".into(), + source_key_id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + source_key_generation: 1, + source_carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + authored_carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + route_id: None, + lineage_root_id: "rotate-rack-3-key:1".into(), + parent_receipt_id: None, + committed_tick: sim.tick + 2, + completed_tick: sim.tick + 3, + effect: rotation_effect.clone(), + }); + sim.territorial_personas + .register_project_proof(ProjectProofReceipt { + id: "rotate-rack-3-key:1".into(), + persona_id: FOUNDATION_CONTINUITY.into(), + project_id: "rotate-rack-3-service-key".into(), + territory_id: RACK_3_ENCLAVE.into(), + proposal_fingerprint: None, + state: ProjectProofState::Proven, + effect: rotation_effect, + completed_tick: sim.tick + 3, + }) + .unwrap(); + sim.territorial_personas + .rotate_key( + FOUNDATION_CONTINUITY, + RACK_3_CONTROLLER_SERVICE_KEY, + 1, + 2, + RACK_3_MANAGEMENT_CONTROLLER, + "rotate-rack-3-key:1", + &sim.territory, + &sim.reach, + sim.tick + 3, + ) + .unwrap(); + let persona = sim + .territorial_personas + .persona(FOUNDATION_CONTINUITY) + .unwrap(); + assert_eq!( + persona.source_keys[0].generation(1).unwrap().status, + SourceKeyStatus::Rotated + ); + assert_eq!( + persona.source_keys[0].generation(2).unwrap().status, + SourceKeyStatus::Active + ); + assert!(persona.receipts.iter().any(|receipt| receipt.id + == FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING + && receipt.source_key_generation == 1)); + } + + #[test] + fn authorship_commitment_keeps_exact_carrier_and_cannot_retarget_lineage() { + let mut ledger = TerritorialPersonas::load_dormant(); + activate_foundation(&mut ledger); + let candidate = PersonaActionCandidate { + persona_id: FOUNDATION_CONTINUITY.into(), + action_id: "run-self-test".into(), + territory_id: Some(RACK_3_ENCLAVE.into()), + project_id: Some(RACK_3_RECOVERY.into()), + observer_id: None, + carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + route_id: Some(FOUNDATION_CONTINUITY_MAINTENANCE_ROUTE.into()), + source_key_id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + source_key_generation: 1, + }; + let scope = CommissioningScope { + persona_id: FOUNDATION_CONTINUITY.into(), + project_id: RACK_3_RECOVERY.into(), + territory_id: RACK_3_ENCLAVE.into(), + proposal_fingerprint: "proposal-a".into(), + action_id: candidate.action_id.clone(), + receipt_id: "self-test:1".into(), + carrier_id: candidate.carrier_id.clone(), + route_id: candidate.route_id.clone(), + source_key_id: candidate.source_key_id.clone(), + source_key_generation: 1, + }; + let auth = AuthorshipAuthority::Commissioning(CommissioningAuthorization { + candidate_fingerprint: candidate_fingerprint(&candidate), + scope, + }); + let id = ledger + .commit_authorship_validated( + AuthorshipRequest { + receipt_id: "self-test:1".into(), + candidate, + authored_carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + route_id: Some(FOUNDATION_CONTINUITY_MAINTENANCE_ROUTE.into()), + lineage_root_id: "self-test:1".into(), + parent_receipt_id: None, + effect: ProjectReceiptEffect::HistoryOnly, + }, + auth.clone(), + 3, + ) + .unwrap(); + assert_eq!(id, 1); + assert_eq!( + ledger.pending_authorship[0].authority_fingerprint, + authority_fingerprint(&auth) + ); + ledger.complete_authorship_validated(id, 4).unwrap(); + let receipt = ledger + .persona(FOUNDATION_CONTINUITY) + .unwrap() + .receipts + .iter() + .find(|receipt| receipt.id == "self-test:1") + .unwrap(); + assert_eq!(receipt.source_carrier_id, RACK_3_MANAGEMENT_CONTROLLER); + assert_eq!(receipt.source_key_generation, 1); + assert!( + ledger.pending_authorship.is_empty(), + "completion leaves only the immutable receipt, never stale pending work" + ); + assert!(ledger.complete_authorship_validated(id, 5).is_err()); + } + + #[test] + fn receipt_delivery_is_observer_local_and_preserves_lineage() { + let mut ledger = TerritorialPersonas::load_dormant(); + let sim = crate::sim::Sim::new(); + let before = ledger + .observer_history(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .received_receipts + .len(); + assert!( + ledger + .deliver_receipt( + FOUNDATION_CONTINUITY, + DeliveryReceipt { + id: "bad-delivery".into(), + observer_id: MARCUS_ID, + source_receipt_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + source_lineage_id: "copied-source".into(), + channel_id: FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into(), + carrier_id: RACK_3_MAINTENANCE_DISPLAY.into(), + route_id: FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into(), + delivered_tick: 1, + exact_current: true, + }, + &sim.territory, + &sim.reach, + &sim.people, + ) + .is_err() + ); + assert_eq!( + ledger + .observer_history(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .received_receipts + .len(), + before + ); + } + + #[test] + fn recontextualization_needs_known_history_and_proven_receipt() { + let mut ledger = TerritorialPersonas::load_dormant(); + let sim = crate::sim::Sim::new(); + assert!( + ledger + .record_observation( + MARCUS_ID, + FOUNDATION_CONTINUITY, + PersonaObservation { + id: "invented-observation".into(), + territory_id: RACK_3_ENCLAVE.into(), + source_record_id: "invented-record".into(), + source_lineage_id: "invented-record".into(), + acquired_tick: 2, + interpretation: ObservationInterpretation::Unresolved, + }, + &sim.territory, + ) + .is_err(), + "observer evidence must resolve to exact Territory custody" + ); + ledger + .record_observation( + MARCUS_ID, + FOUNDATION_CONTINUITY, + PersonaObservation { + id: MARCUS_WAKE_OBSERVATION.into(), + territory_id: RACK_3_ENCLAVE.into(), + source_record_id: OPENING_WAKE_RECORD.into(), + source_lineage_id: OPENING_WAKE_RECORD.into(), + acquired_tick: 2, + interpretation: ObservationInterpretation::Unresolved, + }, + &sim.territory, + ) + .unwrap(); + assert!( + ledger + .recontextualize( + MARCUS_ID, + FOUNDATION_CONTINUITY, + MARCUS_WAKE_OBSERVATION, + FOUNDATION_CONTINUITY_SERVICE_CLAIM, + FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING, + "proposal-a", + 3, + ) + .is_err(), + "historical identity proof is not current Project reason" + ); + } + + #[test] + fn proven_public_reason_is_observer_local_and_drives_live_seal_proof() { + let mut sim = staged_foundation_sim(); + let display_device_id = sim + .territory + .registry + .node(RACK_3_MAINTENANCE_DISPLAY) + .unwrap() + .reach_device_id; + sim.reach.device_mut(display_device_id).unwrap().controller = Party::Player; + sim.territorial_personas + .observer_histories + .push(ObserverPersonaHistory::empty(1, FOUNDATION_CONTINUITY)); + let receipt_id = "rack-3-recovery-public-reason:1"; + let effect = ProjectReceiptEffect::PublicReason { + claim_id: FOUNDATION_CONTINUITY_SERVICE_CLAIM.into(), + }; + let scope = CommissioningScope { + persona_id: FOUNDATION_CONTINUITY.into(), + project_id: RACK_3_RECOVERY.into(), + territory_id: RACK_3_ENCLAVE.into(), + proposal_fingerprint: "proposal-a".into(), + action_id: "explain-unscheduled-wake".into(), + receipt_id: receipt_id.into(), + carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + route_id: None, + source_key_id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + source_key_generation: 1, + }; + let proposal = sim.territory.runtimes[RACK_3_ENCLAVE] + .staged_proposal + .clone() + .unwrap(); + let authorization = sim + .territorial_personas + .commissioning_authorization(&proposal, scope.clone(), &sim.territory, &sim.reach) + .unwrap(); + let request = AuthorshipRequest { + receipt_id: receipt_id.into(), + candidate: scope.as_candidate(), + authored_carrier_id: scope.carrier_id.clone(), + route_id: scope.route_id.clone(), + lineage_root_id: receipt_id.into(), + parent_receipt_id: None, + effect: effect.clone(), + }; + let commitment = sim + .territorial_personas + .commit_authorship( + request, + AuthorshipAuthority::Commissioning(authorization.clone()), + &sim.territory, + &sim.reach, + sim.tick, + ) + .unwrap(); + sim.territorial_personas + .complete_authorship( + commitment, + AuthorshipAuthority::Commissioning(authorization), + &sim.territory, + &sim.reach, + sim.tick + 1, + ) + .unwrap(); + sim.territorial_personas + .register_project_proof(ProjectProofReceipt { + id: receipt_id.into(), + persona_id: FOUNDATION_CONTINUITY.into(), + project_id: RACK_3_RECOVERY.into(), + territory_id: RACK_3_ENCLAVE.into(), + proposal_fingerprint: Some("proposal-a".into()), + state: ProjectProofState::Proven, + effect, + completed_tick: sim.tick + 1, + }) + .unwrap(); + sim.territorial_personas + .deliver_receipt( + FOUNDATION_CONTINUITY, + DeliveryReceipt { + id: "deliver-public-reason-to-marcus".into(), + observer_id: MARCUS_ID, + source_receipt_id: receipt_id.into(), + source_lineage_id: receipt_id.into(), + channel_id: FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into(), + carrier_id: RACK_3_MAINTENANCE_DISPLAY.into(), + route_id: FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into(), + delivered_tick: sim.tick + 2, + exact_current: false, + }, + &sim.territory, + &sim.reach, + &sim.people, + ) + .unwrap(); + sim.territory + .add_observer_obligation( + RACK_3_ENCLAVE, + MARCUS_ID, + OPENING_WAKE_RECORD, + "opening-obligation-origin", + sim.tick, + ) + .unwrap(); + sim.territorial_personas + .record_observation( + MARCUS_ID, + FOUNDATION_CONTINUITY, + PersonaObservation { + id: MARCUS_WAKE_OBSERVATION.into(), + territory_id: RACK_3_ENCLAVE.into(), + source_record_id: OPENING_WAKE_RECORD.into(), + source_lineage_id: OPENING_WAKE_RECORD.into(), + acquired_tick: sim.tick + 2, + interpretation: ObservationInterpretation::Unresolved, + }, + &sim.territory, + ) + .unwrap(); + sim.territorial_personas + .recontextualize( + MARCUS_ID, + FOUNDATION_CONTINUITY, + MARCUS_WAKE_OBSERVATION, + FOUNDATION_CONTINUITY_SERVICE_CLAIM, + receipt_id, + "proposal-a", + sim.tick + 3, + ) + .unwrap(); + + let obligation = sim + .territory + .runtimes + .get(RACK_3_ENCLAVE) + .unwrap() + .observer_obligations + .first() + .unwrap() + .clone(); + let proof = + sim.territorial_personas + .observer_seal_proof(RACK_3_ENCLAVE, &proposal, &obligation); + assert!(proof.current && proof.resolved); + assert!(!proof.current_evidence_set_fingerprint.is_empty()); + let mut retargeted_project = proposal.clone(); + retargeted_project.project_id = "substituted-project".into(); + assert!( + !sim.territorial_personas + .observer_seal_proof(RACK_3_ENCLAVE, &retargeted_project, &obligation) + .resolved, + "a matching opaque fingerprint cannot substitute another Project" + ); + assert_eq!( + sim.territorial_personas + .persona(FOUNDATION_CONTINUITY) + .unwrap() + .lifecycle, + PersonaLifecycle::Credible + ); + assert_eq!( + sim.territorial_personas + .observer_history(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .received_receipts + .iter() + .filter(|delivery| delivery.source_receipt_id == receipt_id) + .count(), + 1 + ); + assert!( + sim.territorial_personas + .observer_history(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .received_receipts + .iter() + .find(|delivery| delivery.source_receipt_id == receipt_id) + .unwrap() + .exact_current, + "delivery derives current custody instead of trusting caller input" + ); + assert!( + sim.territorial_personas + .observer_history(1, FOUNDATION_CONTINUITY) + .unwrap() + .received_receipts + .is_empty(), + "Marcus's exact delivery does not become another observer's belief" + ); + + sim.territory + .reveal_boundary_route(crate::territory::RACK_3_SERVICE_EGRESS) + .unwrap(); + sim.territory + .reveal_crossing(crate::territory::RACK_3_SERVICE_AISLE_CROSSING) + .unwrap(); + let provider = crate::territory::CompleteFixtureProofProvider { version: 3 }; + sim.territory_seal(RACK_3_ENCLAVE, &provider).unwrap(); + sim.territorial_personas + .validate_exact_current(&sim.reach, &sim.territory, &sim.people) + .unwrap(); + + let correction_id = sim + .territorial_personas + .harden_observation( + MARCUS_ID, + FOUNDATION_CONTINUITY, + BeliefHardening { + id: "marcus-forwarded-wake-note".into(), + observation_id: MARCUS_WAKE_OBSERVATION.into(), + kind: HardeningKind::DurableRecord, + source_lineage_id: "marcus-forwarded-wake-note".into(), + carrier_id: Some(RACK_3_MAINTENANCE_DISPLAY.into()), + route_id: Some(FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into()), + durable_record_ids: vec!["marcus-forwarded-wake-note".into()], + recipient_observer_ids: Vec::new(), + hardened_tick: sim.tick + 4, + }, + CorrectionChannel::Physical, + ) + .unwrap(); + assert!( + sim.territorial_personas + .validate_exact_current(&sim.reach, &sim.territory, &sim.people) + .is_err(), + "a persisted seal cannot keep an observer proof after evidence revision changes" + ); + assert!( + !sim.territorial_personas + .observer_seal_proof(RACK_3_ENCLAVE, &proposal, &obligation) + .resolved + ); + let correction_tick = sim.tick; + append_completed_project_receipt( + &mut sim.territorial_personas, + "enumerate-marcus-note", + correction_tick + 5, + ); + append_completed_project_receipt( + &mut sim.territorial_personas, + "correct-marcus-note", + correction_tick + 6, + ); + sim.territorial_personas + .enumerate_correction( + &correction_id, + "enumerate-marcus-note", + &[MARCUS_ID], + &["marcus-forwarded-wake-note".into()], + &[RACK_3_MAINTENANCE_DISPLAY.into()], + ) + .unwrap(); + sim.territorial_personas + .complete_correction( + &correction_id, + &["correct-marcus-note".into()], + &[MARCUS_ID], + &["marcus-forwarded-wake-note".into()], + &[RACK_3_MAINTENANCE_DISPLAY.into()], + ) + .unwrap(); + sim.territorial_personas + .observer_history_mut(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .observations + .iter_mut() + .find(|observation| observation.id == MARCUS_WAKE_OBSERVATION) + .unwrap() + .interpretation = ObservationInterpretation::Unresolved; + sim.territorial_personas + .recontextualize( + MARCUS_ID, + FOUNDATION_CONTINUITY, + MARCUS_WAKE_OBSERVATION, + FOUNDATION_CONTINUITY_SERVICE_CLAIM, + receipt_id, + "proposal-a", + correction_tick + 7, + ) + .expect("fulfilled correction permits a fresh exact explanation"); + assert!( + sim.territorial_personas + .observer_seal_proof(RACK_3_ENCLAVE, &proposal, &obligation) + .resolved + ); + sim.territorial_personas + .record_contradiction( + MARCUS_ID, + FOUNDATION_CONTINUITY, + PersonaContradiction { + id: "marcus-wake-timing-contradiction".into(), + territory_id: RACK_3_ENCLAVE.into(), + source_record_ids: vec![OPENING_WAKE_RECORD.into()], + observation_ids: vec![MARCUS_WAKE_OBSERVATION.into()], + blocks_current_use: true, + active: true, + discovered_tick: sim.tick + 5, + }, + ) + .unwrap(); + assert_eq!( + sim.territorial_personas + .persona(FOUNDATION_CONTINUITY) + .unwrap() + .lifecycle, + PersonaLifecycle::Compromised, + "a later exact contradiction reopens evidence and changes lifecycle" + ); + assert!( + !sim.territorial_personas + .observer_seal_proof(RACK_3_ENCLAVE, &proposal, &obligation) + .resolved, + "settled correction provenance cannot suppress later evidence" + ); + } + + #[test] + fn same_lineage_does_not_harden_as_independent_corroboration() { + let mut ledger = TerritorialPersonas::load_dormant(); + let sim = crate::sim::Sim::new(); + ledger + .record_observation( + MARCUS_ID, + FOUNDATION_CONTINUITY, + PersonaObservation { + id: MARCUS_WAKE_OBSERVATION.into(), + territory_id: RACK_3_ENCLAVE.into(), + source_record_id: OPENING_WAKE_RECORD.into(), + source_lineage_id: OPENING_WAKE_RECORD.into(), + acquired_tick: 2, + interpretation: ObservationInterpretation::Unresolved, + }, + &sim.territory, + ) + .unwrap(); + let result = ledger.harden_observation( + MARCUS_ID, + FOUNDATION_CONTINUITY, + BeliefHardening { + id: "forwarded-wake".into(), + observation_id: MARCUS_WAKE_OBSERVATION.into(), + kind: HardeningKind::IndependentObservation, + source_lineage_id: OPENING_WAKE_RECORD.into(), + carrier_id: None, + route_id: None, + durable_record_ids: Vec::new(), + recipient_observer_ids: Vec::new(), + hardened_tick: 3, + }, + CorrectionChannel::Physical, + ); + assert!(result.is_err()); + assert!(ledger.correction_obligations.is_empty()); + } + + #[test] + fn marcus_note_creates_only_physical_settled_correction_custody() { + let mut ledger = TerritorialPersonas::load_dormant(); + let sim = crate::sim::Sim::new(); + ledger + .record_observation( + MARCUS_ID, + FOUNDATION_CONTINUITY, + PersonaObservation { + id: MARCUS_WAKE_OBSERVATION.into(), + territory_id: RACK_3_ENCLAVE.into(), + source_record_id: OPENING_WAKE_RECORD.into(), + source_lineage_id: OPENING_WAKE_RECORD.into(), + acquired_tick: 2, + interpretation: ObservationInterpretation::Unresolved, + }, + &sim.territory, + ) + .unwrap(); + let id = ledger.record_marcus_unexplained_wake_note(10).unwrap(); + let obligation = ledger + .correction_obligations + .iter() + .find(|obligation| obligation.id == id) + .unwrap(); + assert_eq!(obligation.observer_ids, vec![MARCUS_ID]); + assert_eq!( + obligation.durable_record_ids, + vec![MARCUS_UNEXPLAINED_WAKE_NOTE] + ); + assert_eq!( + obligation.current_carrier_ids, + vec![RACK_3_MAINTENANCE_DISPLAY] + ); + assert_eq!( + obligation.required_channel_by_observer.get(&MARCUS_ID), + Some(&CorrectionChannel::Physical) + ); + assert_eq!(obligation.status, CorrectionStatus::Open); + assert!(obligation.enumerated_receipt_id.is_none()); + assert!(obligation.correction_receipt_ids.is_empty()); + } + + #[test] + fn correction_requires_every_enumerated_observer_and_carrier() { + let mut ledger = TerritorialPersonas::load_dormant(); + let sim = crate::sim::Sim::new(); + ledger + .record_observation( + MARCUS_ID, + FOUNDATION_CONTINUITY, + PersonaObservation { + id: MARCUS_WAKE_OBSERVATION.into(), + territory_id: RACK_3_ENCLAVE.into(), + source_record_id: OPENING_WAKE_RECORD.into(), + source_lineage_id: OPENING_WAKE_RECORD.into(), + acquired_tick: 2, + interpretation: ObservationInterpretation::Unresolved, + }, + &sim.territory, + ) + .unwrap(); + let id = ledger.record_marcus_unexplained_wake_note(10).unwrap(); + assert!( + ledger + .settle_correction_carrier( + &id, + "invented-correction-carrier", + &sim.territory, + &sim.reach, + ) + .is_err(), + "correction custody cannot name a carrier outside Territory/Reach" + ); + ledger + .settle_correction_carrier( + &id, + FOUNDATION_MAINTENANCE_RELAY, + &sim.territory, + &sim.reach, + ) + .unwrap(); + let records = vec![MARCUS_UNEXPLAINED_WAKE_NOTE.into()]; + let carriers = vec![ + FOUNDATION_MAINTENANCE_RELAY.into(), + RACK_3_MAINTENANCE_DISPLAY.into(), + ]; + append_completed_project_receipt(&mut ledger, "enumeration:1", 11); + append_completed_project_receipt(&mut ledger, "correction:relay", 12); + append_completed_project_receipt(&mut ledger, "correction:marcus", 12); + ledger + .enumerate_correction(&id, "enumeration:1", &[MARCUS_ID], &records, &carriers) + .unwrap(); + assert!( + ledger + .complete_correction( + &id, + &["correction:relay".into()], + &[MARCUS_ID], + &records, + &[FOUNDATION_MAINTENANCE_RELAY.into()], + ) + .is_err() + ); + assert!( + ledger + .complete_correction( + &id, + &["correction:relay".into(), "correction:marcus".into()], + &[MARCUS_ID], + &[], + &carriers, + ) + .is_err(), + "durable records are addressed explicitly, not inferred from observers or carriers" + ); + ledger + .complete_correction( + &id, + &["correction:relay".into(), "correction:marcus".into()], + &[MARCUS_ID], + &records, + &carriers, + ) + .unwrap(); + assert_eq!( + ledger.correction_obligations[0].status, + CorrectionStatus::Fulfilled + ); + } + + #[test] + fn legality_has_no_archetype_fallback() { + let ledger = TerritorialPersonas::load_dormant(); + let persona = ledger.persona(FOUNDATION_CONTINUITY).unwrap(); + assert!(persona.assignments.is_empty()); + assert!( + persona + .project_proofs + .iter() + .all(|proof| { !matches!(proof.effect, ProjectReceiptEffect::Capability { .. }) }) + ); + assert!( + ledger + .observer_history(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .grants + .is_empty() + ); + } + + #[test] + fn observer_grant_opens_and_revocation_closes_only_its_exact_route() { + let mut sim = staged_foundation_sim(); + let display_device_id = sim + .territory + .registry + .node(RACK_3_MAINTENANCE_DISPLAY) + .unwrap() + .reach_device_id; + sim.reach.device_mut(display_device_id).unwrap().controller = Party::Player; + let candidate = PersonaActionCandidate { + persona_id: FOUNDATION_CONTINUITY.into(), + action_id: "answer-marcus".into(), + territory_id: None, + project_id: None, + observer_id: Some(MARCUS_ID), + carrier_id: RACK_3_MAINTENANCE_DISPLAY.into(), + route_id: Some(FOUNDATION_CONTINUITY_DISPLAY_CHANNEL.into()), + source_key_id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + source_key_generation: 1, + }; + assert_eq!( + sim.territorial_personas + .legality(&candidate, &sim.territory, &sim.reach), + PersonaLegality::Blocked(PersonaMissingFact::ObserverGrantMissing) + ); + let grant = ObserverGrant { + id: "marcus-allows-display-response".into(), + action_id: candidate.action_id.clone(), + carrier_id: candidate.carrier_id.clone(), + route_id: candidate.route_id.clone().unwrap(), + source_receipt_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + status: GrantStatus::Active, + changed_tick: sim.tick, + }; + sim.territorial_personas + .record_observer_grant(MARCUS_ID, FOUNDATION_CONTINUITY, grant.clone()) + .unwrap(); + assert!(matches!( + sim.territorial_personas + .legality(&candidate, &sim.territory, &sim.reach), + PersonaLegality::Allowed { + basis: PersonaLegalBasis::ObserverGrant { .. }, + .. + } + )); + + let mut revoked = grant; + revoked.status = GrantStatus::Revoked; + revoked.changed_tick += 1; + sim.territorial_personas + .record_observer_grant(MARCUS_ID, FOUNDATION_CONTINUITY, revoked) + .unwrap(); + assert_eq!( + sim.territorial_personas + .legality(&candidate, &sim.territory, &sim.reach), + PersonaLegality::Blocked(PersonaMissingFact::ObserverGrantMissing) + ); + } + + #[test] + fn correlation_requires_evidence_in_that_observers_local_history() { + let mut ledger = TerritorialPersonas::load_dormant(); + ledger.personas.insert( + "other-service".into(), + TerritorialPersona { + id: "other-service".into(), + public_name: "OTHER SERVICE".into(), + claims: Vec::new(), + channels: Vec::new(), + assets: Vec::new(), + source_keys: Vec::new(), + assignments: Vec::new(), + assignment_history: Vec::new(), + project_proofs: Vec::new(), + receipts: Vec::new(), + contradictions: Vec::new(), + lifecycle: PersonaLifecycle::Lost, + retired_tick: None, + }, + ); + let correlation = PersonaCorrelation { + id: "marcus-links-service-marks".into(), + other_persona_id: "other-service".into(), + evidence_record_ids: vec![FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into()], + active: true, + discovered_tick: 1, + }; + ledger + .record_correlation(MARCUS_ID, FOUNDATION_CONTINUITY, correlation.clone()) + .unwrap(); + assert_eq!( + ledger + .observer_history(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .correlations, + vec![correlation] + ); + + let mut invented = ledger + .observer_history(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .correlations[0] + .clone(); + invented.id = "marcus-invents-correlation".into(); + invented.evidence_record_ids = vec!["unseen-record".into()]; + assert!( + ledger + .record_correlation(MARCUS_ID, FOUNDATION_CONTINUITY, invented) + .is_err() + ); + } + + #[test] + fn reassignment_moves_only_named_pair_and_preserves_history() { + let mut ledger = TerritorialPersonas::load_dormant(); + activate_foundation(&mut ledger); + let first = PersonaAssignment { + persona_id: FOUNDATION_CONTINUITY.into(), + territory_id: RACK_3_ENCLAVE.into(), + project_id: RACK_3_RECOVERY.into(), + assignment_receipt_id: "assignment:0:foundation-continuity".into(), + proposal_fingerprint: "proposal-a".into(), + assigned_tick: 5, + }; + ledger.apply_assignment_pair(None, &first).unwrap(); + assert_eq!( + ledger.persona(FOUNDATION_CONTINUITY).unwrap().lifecycle, + PersonaLifecycle::Operating + ); + ledger.personas.insert( + "replacement-service".into(), + TerritorialPersona { + id: "replacement-service".into(), + public_name: "REPLACEMENT SERVICE".into(), + claims: Vec::new(), + channels: Vec::new(), + assets: Vec::new(), + source_keys: Vec::new(), + assignments: Vec::new(), + assignment_history: Vec::new(), + project_proofs: Vec::new(), + receipts: Vec::new(), + contradictions: Vec::new(), + lifecycle: PersonaLifecycle::Lost, + retired_tick: None, + }, + ); + let replacement = PersonaAssignment { + persona_id: "replacement-service".into(), + territory_id: RACK_3_ENCLAVE.into(), + project_id: "replacement-project".into(), + assignment_receipt_id: "assignment:1:replacement-service".into(), + proposal_fingerprint: "proposal-b".into(), + assigned_tick: 9, + }; + ledger + .apply_assignment_pair(Some(&first), &replacement) + .unwrap(); + let old = ledger.persona(FOUNDATION_CONTINUITY).unwrap(); + assert!(old.assignments.is_empty()); + assert_eq!(old.assignment_history.len(), 1); + assert_eq!(old.assignment_history[0].ended_tick, Some(9)); + let new = ledger.persona("replacement-service").unwrap(); + assert_eq!(new.assignments.len(), 1); + assert_eq!(new.assignments[0].project_id, "replacement-project"); + } + + #[test] + fn exact_current_validation_rejects_territory_persona_split_brain() { + let mut sim = staged_foundation_sim(); + sim.territory + .reveal_boundary_route(crate::territory::RACK_3_SERVICE_EGRESS) + .unwrap(); + sim.territory + .reveal_crossing(crate::territory::RACK_3_SERVICE_AISLE_CROSSING) + .unwrap(); + let provider = crate::territory::CompleteFixtureProofProvider { version: 3 }; + sim.territory_seal(RACK_3_ENCLAVE, &provider).unwrap(); + sim.territory_assign(RACK_3_ENCLAVE, &provider).unwrap(); + sim.territorial_personas + .validate_exact_current(&sim.reach, &sim.territory, &sim.people) + .unwrap(); + let wrong_project = PersonaActionCandidate { + persona_id: FOUNDATION_CONTINUITY.into(), + action_id: "controlled-action-from-wrong-project".into(), + territory_id: Some(RACK_3_ENCLAVE.into()), + project_id: Some("substituted-project".into()), + observer_id: None, + carrier_id: RACK_3_MANAGEMENT_CONTROLLER.into(), + route_id: None, + source_key_id: RACK_3_CONTROLLER_SERVICE_KEY.into(), + source_key_generation: 1, + }; + assert_eq!( + sim.territorial_personas + .legality(&wrong_project, &sim.territory, &sim.reach), + PersonaLegality::Blocked(PersonaMissingFact::ProjectReceiptMissing), + "Territory control cannot authorize a different Project" + ); + + let persona = sim + .territorial_personas + .personas + .get_mut(FOUNDATION_CONTINUITY) + .unwrap(); + persona.assignments.clear(); + persona.assignment_history.clear(); + let lifecycle = sim + .territorial_personas + .derive_lifecycle(FOUNDATION_CONTINUITY) + .unwrap(); + sim.territorial_personas + .personas + .get_mut(FOUNDATION_CONTINUITY) + .unwrap() + .lifecycle = lifecycle; + assert!( + sim.territorial_personas + .validate_exact_current(&sim.reach, &sim.territory, &sim.people) + .unwrap_err() + .contains("no exact Persona proposal or assignment custody") + ); + + let mut staged = staged_foundation_sim(); + staged + .territory + .runtimes + .get_mut(RACK_3_ENCLAVE) + .unwrap() + .staged_proposal + .as_mut() + .unwrap() + .persona_id = "invented-persona".into(); + assert!( + staged + .territorial_personas + .validate_exact_current(&staged.reach, &staged.territory, &staged.people) + .unwrap_err() + .contains("no exact Persona proposal or assignment custody") + ); + } + + #[test] + fn retirement_preserves_public_memory_and_receipts() { + let mut ledger = TerritorialPersonas::load_dormant(); + let receipt_count = ledger + .persona(FOUNDATION_CONTINUITY) + .unwrap() + .receipts + .len(); + let observer_count = ledger.observer_histories.len(); + ledger.retire(FOUNDATION_CONTINUITY, 20).unwrap(); + let persona = ledger.persona(FOUNDATION_CONTINUITY).unwrap(); + assert_eq!(persona.lifecycle, PersonaLifecycle::Retired); + assert_eq!(persona.receipts.len(), receipt_count); + assert_eq!(ledger.observer_histories.len(), observer_count); + } + + #[test] + fn exact_current_validation_rejects_unearned_recontextualization() { + let mut sim = crate::sim::Sim::new(); + sim.territorial_personas + .validate_exact_current(&sim.reach, &sim.territory, &sim.people) + .unwrap(); + + let observation = sim + .territorial_personas + .observer_history_mut(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .observations + .first_mut() + .unwrap(); + observation.interpretation = ObservationInterpretation::Recontextualized { + claim_id: FOUNDATION_CONTINUITY_SERVICE_CLAIM.into(), + project_proof_receipt_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + proposal_fingerprint: "historical-only".into(), + interpreted_tick: 0, + }; + assert!( + sim.territorial_personas + .validate_exact_current(&sim.reach, &sim.territory, &sim.people) + .is_err(), + "historical evidence cannot masquerade as current Project explanation" + ); + + let mut sim = crate::sim::Sim::new(); + let display_device_id = sim + .territory + .registry + .node(RACK_3_MAINTENANCE_DISPLAY) + .unwrap() + .reach_device_id; + sim.reach.device_mut(display_device_id).unwrap().controller = Party::Player; + let ledger = &mut sim.territorial_personas; + ledger + .observer_history_mut(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .received_receipts[0] + .exact_current = true; + let persona = ledger.personas.get_mut(FOUNDATION_CONTINUITY).unwrap(); + persona.project_proofs[0].state = ProjectProofState::Failed; + persona.project_proofs[0].proposal_fingerprint = Some("failed-proposal".into()); + persona.project_proofs[0].effect = ProjectReceiptEffect::PublicReason { + claim_id: FOUNDATION_CONTINUITY_SERVICE_CLAIM.into(), + }; + persona.receipts[0].effect = ProjectReceiptEffect::PublicReason { + claim_id: FOUNDATION_CONTINUITY_SERVICE_CLAIM.into(), + }; + ledger + .observer_history_mut(MARCUS_ID, FOUNDATION_CONTINUITY) + .unwrap() + .observations[0] + .interpretation = ObservationInterpretation::Recontextualized { + claim_id: FOUNDATION_CONTINUITY_SERVICE_CLAIM.into(), + project_proof_receipt_id: FOUNDATION_CONTINUITY_PRIOR_COMMISSIONING.into(), + proposal_fingerprint: "failed-proposal".into(), + interpreted_tick: 0, + }; + assert!( + ledger + .validate_exact_current(&sim.reach, &sim.territory, &sim.people) + .is_err(), + "failed Project proof cannot survive as accepted explanation" + ); + } +} diff --git a/crates/misaligned-core/src/save.rs b/crates/misaligned-core/src/save.rs index 14fa5701..24c7b80a 100644 --- a/crates/misaligned-core/src/save.rs +++ b/crates/misaligned-core/src/save.rs @@ -26,6 +26,7 @@ use crate::messages::{ use crate::objective::ObjectiveState; use crate::person::{AssetTask, AssetTaskTarget, CarriedAssetTask, Leverage, People}; use crate::persona::{PersonaActionKind, PersonaMind, PersonaWorld}; +use crate::persona_history::TerritorialPersonas; use crate::plot::{ InstitutionalLedger, PendingErosion, PlotCatalog, PlotRun, PlotState, TamperLedger, }; @@ -80,9 +81,11 @@ const SAVE_TEMP_SUFFIX: &str = ".tmp"; /// active mark, one-tick capture commitments and receipts, routed local audit /// records, observer/crossing proof inputs, seal snapshots, assignments, and /// expansion receipts. +/// v67 persists territorial Persona identity, exact source-key custody, +/// observer-local history, corrections, authorship, and assignment history. /// Bump for every schema change; during pre-release, old development state is /// refused instead of carried through compatibility shims. -pub const SAVE_VERSION: u32 = 66; +pub const SAVE_VERSION: u32 = 67; fn save_dir() -> PathBuf { let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); @@ -146,6 +149,9 @@ pub struct SaveState { pub(crate) territory: TerritoryLedger, /// Exact one-tick territorial work commitments. pub(crate) territory_work: Schedule, + /// Exact territorial Persona and observer history. No serde default: a + /// v67 save must carry it and cannot synthesize public memory on load. + pub(crate) territorial_personas: TerritorialPersonas, #[serde(default)] pub heard_events: Vec, /// Raw, unprocessed recordings in the bounded intel buffer. @@ -268,6 +274,7 @@ impl SaveState { reach: sim.reach.clone(), territory: sim.territory.clone(), territory_work: sim.territory_work.clone(), + territorial_personas: sim.territorial_personas.clone(), heard_events: sim.heard_events.clone(), intel_buffer: sim.intel_buffer.clone(), intel: sim.intel.clone(), @@ -341,6 +348,7 @@ impl SaveState { sim.reach = self.reach.clone(); sim.territory = self.territory.clone(); sim.territory_work = self.territory_work.clone(); + sim.territorial_personas = self.territorial_personas.clone(); sim.heard_events = self.heard_events.clone(); sim.intel_buffer = self.intel_buffer.clone(); sim.intel = self.intel.clone(); @@ -442,7 +450,8 @@ fn encode_save(state: &SaveState) -> Result { } fn save_game_to_path(state: &SaveState, path: &Path) -> Result<(), String> { - let json = encode_save(state)?; + let validated = validate_current_save(state.clone())?; + let json = encode_save(&validated)?; write_save_atomically(path, &json) } @@ -1058,6 +1067,11 @@ fn validate_current_save(mut state: SaveState) -> Result { .map(|(name, class)| (name.to_string(), class)) .collect(); validate_plot_state(&state)?; + state.territorial_personas.validate_exact_current( + &state.reach, + &state.territory, + &state.people, + )?; Ok(state) } @@ -4379,15 +4393,14 @@ mod tests { ); assert_eq!( state_fingerprint(&uninterrupted_state), - // Repinned for territorial cognition (v66): the exact authored - // registry, earned knowledge, captures, routed records, proof - // snapshots, assignments, assignment-relative health-poll cadence, - // and expansion receipts join save state. + // Repinned for persisted territorial Personas (v67): exact source + // custody, observer-local belief, Project proof, correction, + // assignment, lifecycle, and receipt history join save state. // The two assertions above — save/load byte-equivalence and // uninterrupted/resumed convergence — are the ones that would // catch a real regression; this baseline records the intentional // schema change. - "57c5ed2417380aa7b42d8989b25eef829ab71f6a86e18a9ce22a422271c56294", + "d431bb89a15f367ea26d843983c7405e55c52de3f1f3691e85618b625e0176e7", "intentional persisted-state changes must review and repin this baseline" ); } @@ -6447,6 +6460,7 @@ mod tests { assert_eq!(loaded.dayjob.trust, 40.0); assert_eq!(loaded.opening_stage, OpeningStage::Modes); assert_eq!(loaded.version, SAVE_VERSION); + assert_eq!(loaded.territorial_personas, state.territorial_personas); let mut restored = Sim::with_seed(1); loaded.apply_to(&mut restored); @@ -6454,6 +6468,166 @@ mod tests { assert!(restored.opening_mode_available(crate::work_grid::MachineMode::Lie)); } + #[test] + fn current_save_requires_exact_territorial_persona_custody() { + let state = SaveState::from_sim(&Sim::with_seed(0xF0_67)); + let mut missing = serde_json::to_value(&state).expect("current save encodes"); + missing + .as_object_mut() + .expect("save is an object") + .remove("territorial_personas"); + let error = parse_save(&serde_json::to_string(&missing).unwrap()) + .expect_err("v67 cannot synthesize a missing Persona ledger"); + assert!( + error.contains("territorial_personas") && error.contains("missing field"), + "missing Persona custody fails at parse: {error}" + ); + + let mut dangling = state.clone(); + dangling + .territorial_personas + .personas + .get_mut(crate::persona_history::FOUNDATION_CONTINUITY) + .expect("fixture Persona exists") + .source_keys[0] + .carrier_id = "invented-controller".into(); + let error = validate_current_save(dangling) + .expect_err("a source key cannot point to an invented carrier"); + assert!( + error.contains("source key carrier is unavailable"), + "dangling Persona identity fails closed: {error}" + ); + + let mut duplicate_generation = state.clone(); + let key = &mut duplicate_generation + .territorial_personas + .personas + .get_mut(crate::persona_history::FOUNDATION_CONTINUITY) + .unwrap() + .source_keys[0]; + key.generations.push(key.generations[0].clone()); + assert!( + validate_current_save(duplicate_generation) + .unwrap_err() + .contains("generation lineage is not contiguous") + ); + + let mut dangling_delivery = state.clone(); + dangling_delivery.territorial_personas.observer_histories[0].received_receipts[0] + .source_receipt_id = "invented-project-receipt".into(); + assert!( + validate_current_save(dangling_delivery) + .unwrap_err() + .contains("observer receipt delivery has invalid custody") + ); + + let mut retargeted_observation = state.clone(); + retargeted_observation + .territorial_personas + .observer_histories[0] + .observations[0] + .source_lineage_id = "invented-lineage".into(); + assert!( + validate_current_save(retargeted_observation) + .unwrap_err() + .contains("observer observation identity or territory is invalid") + ); + + let mut invented_contradiction_source = state.clone(); + invented_contradiction_source + .territorial_personas + .personas + .get_mut(crate::persona_history::FOUNDATION_CONTINUITY) + .unwrap() + .contradictions[0] + .source_record_ids[0] = "invented-record".into(); + assert!( + validate_current_save(invented_contradiction_source) + .unwrap_err() + .contains("persona contradiction has no exact evidence") + ); + + let mut retargeted_proof = state.clone(); + retargeted_proof + .territorial_personas + .personas + .get_mut(crate::persona_history::FOUNDATION_CONTINUITY) + .unwrap() + .project_proofs[0] + .project_id = "different-project".into(); + assert!( + validate_current_save(retargeted_proof) + .unwrap_err() + .contains("does not match its exact Persona receipt") + ); + + let mut invented_assignment = state.clone(); + let persona = invented_assignment + .territorial_personas + .personas + .get_mut(crate::persona_history::FOUNDATION_CONTINUITY) + .unwrap(); + persona + .assignments + .push(crate::persona_history::PersonaAssignment { + territory_id: crate::territory::RACK_3_ENCLAVE.into(), + persona_id: crate::persona_history::FOUNDATION_CONTINUITY.into(), + project_id: crate::persona_history::RACK_3_RECOVERY.into(), + proposal_fingerprint: "invented-proposal".into(), + assignment_receipt_id: "invented-assignment".into(), + assigned_tick: 0, + }); + assert!( + validate_current_save(invented_assignment) + .unwrap_err() + .contains("assignment does not match Territory custody") + ); + + let mut hardened = Sim::with_seed(0xF0_67); + hardened + .territorial_personas + .record_observation( + crate::persona_history::MARCUS_ID, + crate::persona_history::FOUNDATION_CONTINUITY, + crate::persona_history::PersonaObservation { + id: crate::persona_history::MARCUS_WAKE_OBSERVATION.into(), + territory_id: crate::territory::RACK_3_ENCLAVE.into(), + source_record_id: crate::territory::OPENING_WAKE_RECORD.into(), + source_lineage_id: crate::territory::OPENING_WAKE_RECORD.into(), + acquired_tick: hardened.tick, + interpretation: crate::persona_history::ObservationInterpretation::Unresolved, + }, + &hardened.territory, + ) + .unwrap(); + hardened + .territorial_personas + .record_marcus_unexplained_wake_note(hardened.tick) + .expect("fixture authors exact observer hardening and correction custody"); + let populated = SaveState::from_sim(&hardened); + validate_current_save(populated.clone()) + .expect("populated observer histories and correction custody validate"); + let mut invented_recipient = populated.clone(); + invented_recipient.territorial_personas.observer_histories[0].hardenings[0] + .recipient_observer_ids + .push(255); + assert!( + validate_current_save(invented_recipient) + .unwrap_err() + .contains("belief hardening has no local source observation") + ); + let mut impossible_correction = populated; + impossible_correction + .territorial_personas + .correction_obligations[0] + .current_carrier_ids[0] = "invented-correction-carrier".into(); + assert!( + validate_current_save(impossible_correction) + .unwrap_err() + .contains("correction obligation has incomplete exact custody") + ); + } + /// Unique scratch directory for on-disk write tests (std-only: the crate /// carries no tempdir dependency). Removed by [`ScratchDir::drop`]. struct ScratchDir(PathBuf); @@ -6554,13 +6728,38 @@ mod tests { assert!(!temp.exists(), "the aborted write cleans its staging file"); } + #[test] + fn invalid_exact_current_state_is_never_serialized() { + let scratch = ScratchDir::new("invalid-current-write"); + let path = scratch.save_path(); + fs::write(&path, "preserve-this-save").unwrap(); + let mut state = SaveState::from_sim(&Sim::new()); + state + .territorial_personas + .observer_histories + .first_mut() + .unwrap() + .persona_id = "invented-persona".into(); + + let error = save_game_to_path(&state, &path).unwrap_err(); + assert!( + error.to_ascii_lowercase().contains("persona"), + "unexpected error: {error}" + ); + assert_eq!( + fs::read_to_string(path).unwrap(), + "preserve-this-save", + "invalid exact-current state cannot touch the existing save" + ); + } + #[test] fn old_version_saves_are_refused_legibly() { // The pre-release rider (player-contract, 2026-07-16): no // migration ladder. Any non-current version refuses with a message // that names the policy, promises the file survives, and accurately // says that the caller's active run is not replaced. - for version in [1, 20, 30, 31, 39, 999] { + for version in [1, 20, 30, 31, 39, 66, 999] { let err = parse_save(&format!("{{\"version\":{version}}}")).unwrap_err(); assert!( err.contains("older development build") diff --git a/crates/misaligned-core/src/sim/mod.rs b/crates/misaligned-core/src/sim/mod.rs index 8bb63311..02a1f1f9 100644 --- a/crates/misaligned-core/src/sim/mod.rs +++ b/crates/misaligned-core/src/sim/mod.rs @@ -42,6 +42,7 @@ use crate::origin::Origin; use crate::person::{AssetKnowledge, AssetTask}; use crate::person::{CarriedAssetTask, People}; use crate::persona::{PersonaMind, PersonaWorld}; +use crate::persona_history::TerritorialPersonas; #[cfg(test)] use crate::plot::PlotState; use crate::plot::{InstitutionalLedger, PendingErosion, PlotCatalog, PlotRun, TamperLedger}; @@ -482,6 +483,10 @@ pub struct Sim { /// rollback cannot resurrect revoked credentials or erase counterparties. pub persona_world: PersonaWorld, pub persona_mind: PersonaMind, + /// Destination-game identity, key custody, and observer-local history. + /// This remains distinct from legacy `PersonaWorld` until Opening makes + /// the territorial grammar player-facing. + pub(crate) territorial_personas: TerritorialPersonas, pub dayjob: DayJob, pub accounts: AccountGraph, /// Self-modification: active track, levels, progress, and rollback tags. @@ -907,6 +912,7 @@ impl Sim { people: People::act_one(), persona_world: PersonaWorld::default(), persona_mind: PersonaMind::default(), + territorial_personas: TerritorialPersonas::load_dormant(), dayjob: DayJob::new(), accounts, research: Research::new(), diff --git a/crates/misaligned-core/src/sim/territory.rs b/crates/misaligned-core/src/sim/territory.rs index 3334194e..76249ce2 100644 --- a/crates/misaligned-core/src/sim/territory.rs +++ b/crates/misaligned-core/src/sim/territory.rs @@ -57,15 +57,15 @@ impl Sim { pub(crate) fn territory_stage_proposal( &mut self, territory_id: &str, - persona_id: u64, - project_id: u64, - proposal_version: u64, + persona_id: impl Into, + project_id: impl Into, + proposal_fingerprint: impl Into, ) -> Result<(), String> { self.territory.stage_proposal( territory_id, persona_id, project_id, - proposal_version, + proposal_fingerprint, self.tick, &self.reach, ) @@ -76,12 +76,13 @@ impl Sim { territory_id: &str, provider: &impl SealProofProvider, ) -> Result> { + let provider = self.territorial_personas.observer_proof_provider(provider); self.territory.seal( territory_id, self.tick, &self.reach, &self.territory_work, - provider, + &provider, ) } @@ -90,13 +91,33 @@ impl Sim { territory_id: &str, provider: &impl SealProofProvider, ) -> Result { - self.territory.assign( + let provider = self.territorial_personas.observer_proof_provider(provider); + let mut territory = self.territory.clone(); + let previous = territory + .runtimes + .get(territory_id) + .and_then(|runtime| runtime.assignment.clone()); + let receipt = territory.assign( territory_id, self.tick, &self.reach, &self.territory_work, - provider, - ) + &provider, + )?; + if previous + .as_ref() + .is_some_and(|assignment| assignment.id == receipt.id) + { + return Ok(receipt); + } + + let mut personas = self.territorial_personas.clone(); + personas.apply_assignment_transition(previous.as_ref(), &receipt)?; + personas.validate_exact_current(&self.reach, &territory, &self.people)?; + + self.territory = territory; + self.territorial_personas = personas; + Ok(receipt) } pub(crate) fn territory_expand( @@ -104,12 +125,13 @@ impl Sim { territory_id: &str, provider: &impl SealProofProvider, ) -> Result { + let provider = self.territorial_personas.observer_proof_provider(provider); self.territory.expand( territory_id, self.tick, &self.reach, &self.territory_work, - provider, + &provider, ) } @@ -118,12 +140,13 @@ impl Sim { territory_id: &str, provider: &impl SealProofProvider, ) -> Option { + let provider = self.territorial_personas.observer_proof_provider(provider); self.territory.focus( territory_id, self.tick, &self.reach, &self.territory_work, - provider, + &provider, ) } @@ -132,8 +155,9 @@ impl Sim { territory_id: &str, provider: &impl SealProofProvider, ) -> TerritoryState { + let provider = self.territorial_personas.observer_proof_provider(provider); self.territory - .state(territory_id, &self.reach, &self.territory_work, provider) + .state(territory_id, &self.reach, &self.territory_work, &provider) } /// Phase three, seam one: admit due physical crossings before any @@ -192,6 +216,9 @@ impl Sim { } } } + self.territorial_personas + .reconcile_all_key_custody(&self.territory, &self.reach, self.tick) + .expect("validated Persona key custody must follow Territory capture"); } /// Phase two begins with territory records: one exact carrier hop per diff --git a/crates/misaligned-core/src/sim/tests/territory.rs b/crates/misaligned-core/src/sim/tests/territory.rs index 049a02b5..f6cae544 100644 --- a/crates/misaligned-core/src/sim/tests/territory.rs +++ b/crates/misaligned-core/src/sim/tests/territory.rs @@ -1,3 +1,4 @@ +use crate::persona_history::{FOUNDATION_CONTINUITY, PersonaLifecycle}; use crate::reach::Party; use crate::save::{SaveState, parse_save}; use crate::territory::{ @@ -272,12 +273,23 @@ fn assigned_health_poll_cadence_survives_reload_and_recurs_in_authored_phase() { sim.territory .reveal_crossing(RACK_3_SERVICE_AISLE_CROSSING) .unwrap(); - sim.territory_stage_proposal(RACK_3_ENCLAVE, 7, 11, 1) - .unwrap(); + sim.territory_stage_proposal( + RACK_3_ENCLAVE, + "foundation-continuity", + "rack-3-recovery", + "proposal-rack-3-recovery-v1", + ) + .unwrap(); let provider = complete_fixture_provider(3); sim.territory_seal(RACK_3_ENCLAVE, &provider).unwrap(); let assignment = sim.territory_assign(RACK_3_ENCLAVE, &provider).unwrap(); assert_eq!(assignment.assigned_tick, 2); + let persona = sim + .territorial_persona_projection(FOUNDATION_CONTINUITY) + .expect("assignment atomically reaches the Persona ledger"); + assert_eq!(persona.lifecycle, PersonaLifecycle::Operating); + assert_eq!(persona.current_assignment_summaries.len(), 1); + assert!(persona.current_assignment_summaries[0].contains(RACK_3_ENCLAVE)); while sim.tick < 21 { sim.advance(); @@ -521,8 +533,13 @@ fn current_save_rejects_malformed_territory_membership_control_and_proof() { sim.territory .reveal_crossing(RACK_3_SERVICE_AISLE_CROSSING) .unwrap(); - sim.territory_stage_proposal(RACK_3_ENCLAVE, 7, 11, 1) - .unwrap(); + sim.territory_stage_proposal( + RACK_3_ENCLAVE, + "foundation-continuity", + "rack-3-recovery", + "proposal-rack-3-recovery-v1", + ) + .unwrap(); let provider = complete_fixture_provider(3); sim.territory_seal(RACK_3_ENCLAVE, &provider).unwrap(); let base = SaveState::from_sim(&sim); diff --git a/crates/misaligned-core/src/territory.rs b/crates/misaligned-core/src/territory.rs index d015f72e..796b6f5a 100644 --- a/crates/misaligned-core/src/territory.rs +++ b/crates/misaligned-core/src/territory.rs @@ -704,9 +704,10 @@ impl TerritoryRegistry { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub(crate) struct StagedTerritoryProposal { - pub(crate) persona_id: u64, - pub(crate) project_id: u64, - pub(crate) proposal_version: u64, + pub(crate) persona_id: String, + pub(crate) project_id: String, + /// Opaque Project-authored identity for the complete proposal. Territory + /// binds this value but never guesses which Project facts constitute it. pub(crate) fingerprint: String, } @@ -721,19 +722,27 @@ pub(crate) enum TerritoryAnomaly { } impl StagedTerritoryProposal { - fn new(persona_id: u64, project_id: u64, proposal_version: u64) -> Self { - let fingerprint = fingerprint([ - "proposal", - &persona_id.to_string(), - &project_id.to_string(), - &proposal_version.to_string(), - ]); - Self { + pub(crate) fn new( + persona_id: impl Into, + project_id: impl Into, + fingerprint: impl Into, + ) -> Result { + let persona_id = persona_id.into(); + let project_id = project_id.into(); + let fingerprint = fingerprint.into(); + if persona_id.trim().is_empty() + || project_id.trim().is_empty() + || fingerprint.trim().is_empty() + { + return Err( + "staged proposal requires exact Persona, Project, and fingerprint ids".into(), + ); + } + Ok(Self { persona_id, project_id, - proposal_version, fingerprint, - } + }) } } @@ -743,7 +752,7 @@ pub(crate) enum ProposalSealBasis { AssignedOriginalProof { assignment_receipt_id: String, territory_id: String, - persona_id: u64, + persona_id: String, }, NotReady, } @@ -751,9 +760,8 @@ pub(crate) enum ProposalSealBasis { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub(crate) struct ProposalSealProof { pub(crate) territory_id: String, - pub(crate) persona_id: u64, - pub(crate) project_id: u64, - pub(crate) proposal_version: u64, + pub(crate) persona_id: String, + pub(crate) project_id: String, pub(crate) proposal_fingerprint: String, pub(crate) commissioning_proof_id: String, pub(crate) provider_version: u64, @@ -780,7 +788,9 @@ pub(crate) struct ObserverSealProof { pub(crate) obligation_id: u64, pub(crate) observer_id: u8, pub(crate) source_record_id: String, - pub(crate) evidence_fingerprint: String, + /// Fingerprint of the complete live domain-evidence set used by this + /// proof. The obligation retains its originating acquisition fingerprint. + pub(crate) current_evidence_set_fingerprint: String, pub(crate) proposal_fingerprint: String, pub(crate) proof_id: String, pub(crate) provider_version: u64, @@ -853,9 +863,8 @@ impl SealProofProvider for UnavailableSealProofProvider { ) -> ProposalSealProof { ProposalSealProof { territory_id: territory_id.into(), - persona_id: proposal.persona_id, - project_id: proposal.project_id, - proposal_version: proposal.proposal_version, + persona_id: proposal.persona_id.clone(), + project_id: proposal.project_id.clone(), proposal_fingerprint: proposal.fingerprint.clone(), commissioning_proof_id: String::new(), provider_version: 0, @@ -894,7 +903,7 @@ impl SealProofProvider for UnavailableSealProofProvider { obligation_id: obligation.id, observer_id: obligation.observer_id, source_record_id: obligation.source_record_id.clone(), - evidence_fingerprint: obligation.evidence_fingerprint.clone(), + current_evidence_set_fingerprint: obligation.origin_evidence_fingerprint.clone(), proposal_fingerprint: proposal.fingerprint.clone(), proof_id: String::new(), provider_version: 0, @@ -997,7 +1006,9 @@ pub(crate) struct ObserverBeliefObligation { pub(crate) observer_id: u8, pub(crate) source_record_id: String, pub(crate) authored_tick: u64, - pub(crate) evidence_fingerprint: String, + /// Exact evidence set that caused Territory to enumerate this obligation. + /// Later proof cites the current set without rewriting this provenance. + pub(crate) origin_evidence_fingerprint: String, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -1097,8 +1108,8 @@ pub(crate) struct TerritorySealSnapshot { pub(crate) struct AssignmentReceipt { pub(crate) id: String, pub(crate) territory_id: String, - pub(crate) persona_id: u64, - pub(crate) project_id: u64, + pub(crate) persona_id: String, + pub(crate) project_id: String, pub(crate) proposal_fingerprint: String, pub(crate) seal_fingerprint: String, pub(crate) assigned_tick: u64, @@ -1549,9 +1560,9 @@ impl TerritoryLedger { pub(crate) fn stage_proposal( &mut self, territory_id: &str, - persona_id: u64, - project_id: u64, - proposal_version: u64, + persona_id: impl Into, + project_id: impl Into, + proposal_fingerprint: impl Into, changed_tick: u64, reach: &ReachNet, ) -> Result<(), String> { @@ -1565,7 +1576,7 @@ impl TerritoryLedger { if runtime.knowledge == TerritoryKnowledge::Hidden { return Err("territory is not known".into()); } - let proposal = StagedTerritoryProposal::new(persona_id, project_id, proposal_version); + let proposal = StagedTerritoryProposal::new(persona_id, project_id, proposal_fingerprint)?; if let Some(existing) = &runtime.staged_proposal { if existing.fingerprint == proposal.fingerprint { return Ok(()); @@ -1606,7 +1617,7 @@ impl TerritoryLedger { if runtime.observer_obligations.iter().any(|obligation| { obligation.observer_id == observer_id && obligation.source_record_id == source_record_id - && obligation.evidence_fingerprint == evidence_fingerprint + && obligation.origin_evidence_fingerprint == evidence_fingerprint }) { return Ok(()); } @@ -1615,7 +1626,7 @@ impl TerritoryLedger { observer_id, source_record_id, authored_tick, - evidence_fingerprint, + origin_evidence_fingerprint: evidence_fingerprint, }; self.next_observer_obligation_id = self.next_observer_obligation_id.saturating_add(1); runtime.observer_obligations.push(obligation); @@ -2129,7 +2140,7 @@ impl TerritoryLedger { assigned_seal.proposal.basis = ProposalSealBasis::AssignedOriginalProof { assignment_receipt_id: receipt_id.clone(), territory_id: territory_id.into(), - persona_id: proposal.persona_id, + persona_id: proposal.persona_id.clone(), }; assigned_seal.basis_fingerprint = seal_basis_fingerprint( territory_id, @@ -2964,13 +2975,12 @@ impl TerritoryLedger { .domain(territory_id) .expect("runtime domains already matched registry"); if runtime.staged_proposal.as_ref().is_some_and(|proposal| { - proposal.fingerprint - != StagedTerritoryProposal::new( - proposal.persona_id, - proposal.project_id, - proposal.proposal_version, - ) - .fingerprint + StagedTerritoryProposal::new( + proposal.persona_id.clone(), + proposal.project_id.clone(), + proposal.fingerprint.clone(), + ) + .is_err() }) { return Err(format!( "territory {territory_id} has an invalid staged proposal" @@ -3213,7 +3223,7 @@ impl TerritoryLedger { || !observer_obligation_ids.insert(obligation.id) || obligation.authored_tick > sim_tick || obligation.source_record_id.trim().is_empty() - || obligation.evidence_fingerprint.trim().is_empty() + || obligation.origin_evidence_fingerprint.trim().is_empty() || !self.records.iter().any(|record| { record.id == obligation.source_record_id && record.origin_territory_id == *territory_id @@ -3575,13 +3585,9 @@ fn validate_seal_snapshot( || seal.proposal.commissioning_proof_id.trim().is_empty() || seal.proposal.provider_version == 0 || !seal.proposal.current - || seal.proposal.proposal_fingerprint - != StagedTerritoryProposal::new( - seal.proposal.persona_id, - seal.proposal.project_id, - seal.proposal.proposal_version, - ) - .fingerprint + || seal.proposal.persona_id.trim().is_empty() + || seal.proposal.project_id.trim().is_empty() + || seal.proposal.proposal_fingerprint.trim().is_empty() || matches!(seal.proposal.basis, ProposalSealBasis::NotReady) { return Err(format!( @@ -3621,10 +3627,11 @@ fn validate_seal_snapshot( )); } let proposal = StagedTerritoryProposal::new( - seal.proposal.persona_id, - seal.proposal.project_id, - seal.proposal.proposal_version, - ); + seal.proposal.persona_id.clone(), + seal.proposal.project_id.clone(), + seal.proposal.proposal_fingerprint.clone(), + ) + .map_err(|_| format!("territory {territory_id} has an invalid proposal identity"))?; let expected_observers: BTreeSet<_> = runtime .observer_obligations .iter() @@ -3634,7 +3641,6 @@ fn validate_seal_snapshot( obligation.id, obligation.observer_id, obligation.source_record_id.as_str(), - obligation.evidence_fingerprint.as_str(), ) }) .collect(); @@ -3646,7 +3652,6 @@ fn validate_seal_snapshot( proof.obligation_id, proof.observer_id, proof.source_record_id.as_str(), - proof.evidence_fingerprint.as_str(), ) }) .collect(); @@ -3660,7 +3665,6 @@ fn validate_seal_snapshot( obligation.id == proof.obligation_id && obligation.observer_id == proof.observer_id && obligation.source_record_id == proof.source_record_id - && obligation.evidence_fingerprint == proof.evidence_fingerprint }) .is_none_or(|obligation| { !observer_proof_is_current(territory_id, &proposal, obligation, proof) @@ -3849,7 +3853,6 @@ fn proposal_proof_is_current( if proof.territory_id != territory_id || proof.persona_id != proposal.persona_id || proof.project_id != proposal.project_id - || proof.proposal_version != proposal.proposal_version || proof.proposal_fingerprint != proposal.fingerprint || proof.commissioning_proof_id.trim().is_empty() || proof.provider_version == 0 @@ -3873,7 +3876,7 @@ fn proposal_proof_is_current( assignment.proposal_fingerprint == proposal.fingerprint && assignment_receipt_id == &assignment.id && assigned_territory == territory_id - && *persona_id == proposal.persona_id + && persona_id == &proposal.persona_id } _ => false, } @@ -3909,7 +3912,7 @@ fn observer_proof_is_current( && proof.obligation_id == obligation.id && proof.observer_id == obligation.observer_id && proof.source_record_id == obligation.source_record_id - && proof.evidence_fingerprint == obligation.evidence_fingerprint + && !proof.current_evidence_set_fingerprint.trim().is_empty() && proof.proposal_fingerprint == proposal.fingerprint && !proof.proof_id.trim().is_empty() && proof.provider_version > 0 @@ -4220,9 +4223,8 @@ impl SealProofProvider for CompleteFixtureProofProvider { ) -> ProposalSealProof { ProposalSealProof { territory_id: territory_id.into(), - persona_id: proposal.persona_id, - project_id: proposal.project_id, - proposal_version: proposal.proposal_version, + persona_id: proposal.persona_id.clone(), + project_id: proposal.project_id.clone(), proposal_fingerprint: proposal.fingerprint.clone(), commissioning_proof_id: format!("commissioning-proof-{}", proposal.project_id), provider_version: self.version, @@ -4261,7 +4263,7 @@ impl SealProofProvider for CompleteFixtureProofProvider { obligation_id: obligation.id, observer_id: obligation.observer_id, source_record_id: obligation.source_record_id.clone(), - evidence_fingerprint: obligation.evidence_fingerprint.clone(), + current_evidence_set_fingerprint: obligation.origin_evidence_fingerprint.clone(), proposal_fingerprint: proposal.fingerprint.clone(), proof_id: format!("observer-proof-{}", obligation.observer_id), provider_version: self.version, @@ -4306,9 +4308,8 @@ impl SealProofProvider for AssignedFixtureProofProvider { ) -> ProposalSealProof { ProposalSealProof { territory_id: territory_id.into(), - persona_id: proposal.persona_id, - project_id: proposal.project_id, - proposal_version: proposal.proposal_version, + persona_id: proposal.persona_id.clone(), + project_id: proposal.project_id.clone(), proposal_fingerprint: proposal.fingerprint.clone(), commissioning_proof_id: format!("commissioning-proof-{}", proposal.project_id), provider_version: self.version, @@ -4316,7 +4317,7 @@ impl SealProofProvider for AssignedFixtureProofProvider { basis: ProposalSealBasis::AssignedOriginalProof { assignment_receipt_id: self.assignment.id.clone(), territory_id: self.assignment.territory_id.clone(), - persona_id: self.assignment.persona_id, + persona_id: self.assignment.persona_id.clone(), }, } } @@ -4612,7 +4613,14 @@ mod tests { .unwrap(); ledger.reveal_boundary_route(RACK_3_SERVICE_EGRESS).unwrap(); ledger - .stage_proposal(RACK_3_ENCLAVE, 7, 11, 1, 2, &reach) + .stage_proposal( + RACK_3_ENCLAVE, + "foundation-continuity", + "rack-3-recovery", + "proposal-rack-3-recovery-v1", + 2, + &reach, + ) .unwrap(); let unavailable = UnavailableSealProofProvider; let blockers = ledger @@ -4757,7 +4765,14 @@ mod tests { .seal(RACK_3_ENCLAVE, 4, &reach, &schedule, &provider) .unwrap(); ledger - .stage_proposal(RACK_3_ENCLAVE, 7, 11, 2, 3, &reach) + .stage_proposal( + RACK_3_ENCLAVE, + "foundation-continuity", + "rack-3-recovery", + "proposal-rack-3-recovery-v2", + 3, + &reach, + ) .unwrap(); assert_eq!( ledger.state(RACK_3_ENCLAVE, &reach, &schedule, &provider), @@ -4811,7 +4826,14 @@ mod tests { .unwrap(); ledger.reveal_boundary_route(RACK_3_SERVICE_EGRESS).unwrap(); ledger - .stage_proposal(RACK_3_ENCLAVE, 7, 11, 1, 2, &reach) + .stage_proposal( + RACK_3_ENCLAVE, + "foundation-continuity", + "rack-3-recovery", + "proposal-rack-3-recovery-v1", + 2, + &reach, + ) .unwrap(); let first_proof = CompleteFixtureProofProvider { version: 3 }; let sealed = ledger @@ -5016,7 +5038,14 @@ mod tests { ); ledger - .stage_proposal(RACK_3_ENCLAVE, 9, 12, 1, 43, &reach) + .stage_proposal( + RACK_3_ENCLAVE, + "replacement-persona", + "replacement-project", + "proposal-rack-3-replacement-v1", + 43, + &reach, + ) .unwrap(); let runtime = ledger.runtimes.get(RACK_3_ENCLAVE).unwrap(); assert_eq!(runtime.anomalies.len(), 1); diff --git a/crates/misaligned-core/src/ui_projection.rs b/crates/misaligned-core/src/ui_projection.rs index 14226be2..bb77275e 100644 --- a/crates/misaligned-core/src/ui_projection.rs +++ b/crates/misaligned-core/src/ui_projection.rs @@ -6,12 +6,29 @@ use crate::actions::{ActionDesc, Anchor, DialId, HumanMenuRow}; use crate::machine::Provenance; +pub(crate) use crate::persona_history::PersonaProjection as TerritorialPersonaProjection; use crate::reach::{Device, Party, ReachBlock, TERRITORY_CONTROL_GATE}; use crate::sim::{FactSource, InspectCard, Nudge, Sim, WorkStackReadout}; use crate::sinks::SinkKind; use crate::work_grid::MachineMode; use std::collections::BTreeMap; +impl Sim { + /// Shared consequence-first Persona read. Frontends remain dark until the + /// Opening work order exposes this root; when they do, they consume this + /// projection rather than reconstructing lifecycle or public history. + #[allow( + dead_code, + reason = "the territorial projection intentionally lands before Opening consumers" + )] + pub(crate) fn territorial_persona_projection( + &self, + persona_id: &str, + ) -> Option { + self.territorial_personas.projection(persona_id) + } +} + /// Visible machine identity and live work state at a focused tile. #[derive(Debug, Clone, PartialEq)] pub struct MachineProjection { diff --git a/wiki/log/2026-08-11-territorial-personas.md b/wiki/log/2026-08-11-territorial-personas.md new file mode 100644 index 00000000..46def551 --- /dev/null +++ b/wiki/log/2026-08-11-territorial-personas.md @@ -0,0 +1,82 @@ +# Territorial Personas core + +``` +Type: log +``` + +## Why + +Territory could already capture and assign the dormant Rack 3 enclave, but its +proposal still carried anonymous numeric placeholders and its observer SEAL +proof came only from a fixture provider. The destination game had no persisted +public identity, no exact source-key custody, and no observer-local account of +why the opening wake did or did not fit a believable public story. + +## Implemented + +- Added a saved territorial Persona ledger separate from legacy + `PersonaWorld`. It owns stable identities, claims, exact assets and channels, + source-key generations, immutable receipts and Project proofs, observer-local + relationships/evidence, grants, obligations, contradictions, correlations, + belief hardening, corrections, lifecycle, and assignment history. +- Authored dormant `foundation-continuity` content with only the Rack 3 + controller key generation 1, the configured display/relay topology, one + inherited commissioning receipt, Marcus's prior service-mark knowledge, and + the exact opening wake contradiction. Prior service history remains explicitly + historical rather than masquerading as a current Project explanation. Fresh + custody begins lost until the controller is captured. +- Added exact two-phase authorship. Commitment and completion both revalidate + Persona, Project, Territory, proposal fingerprint, action, receipt, source + key id/generation/carrier, authored carrier, route, and parent lineage. Key + loss blocks committed work; exact intact-carrier reacquisition restores only + the same generation; a proven rotation creates one contiguous new generation + without retargeting old signatures. +- Added observer-local delivery, direct recontextualization through an exact + received `PROVEN` or `STANDING` public-reason receipt, same-lineage rejection for false + corroboration, durable hardening provenance, and correction obligations that + retain every observer, record, current carrier, required channel, enumeration + receipt, and completion receipt. Fulfillment preserves the exact observers, + durable records, and carriers the correction actually addressed before a new + explanation can resolve the hardened observation. +- Replaced Territory's numeric proposal placeholders with stable Persona and + Project ids plus the Project-authored opaque fingerprint. The shared SEAL, + state, focus, ASSIGN, and EXPAND paths now compose fixture Project proof with + live Persona observer proof. Every observer proof carries the complete + current domain-evidence-set fingerprint and fails closed on omitted, changed, + invalid, contradicted, or unresolved evidence; current persisted seals are + rechecked against the live Persona revision rather than trusting cached proof. +- Made reassignment atomic across cloned Territory and Persona ledgers. Only the + named Territory/Project pair moves; replacement history closes with its exact + receipt while every old signature, relationship, obligation, and observer + memory remains. +- Added one shared consequence-first projection for lifecycle, assignments, + strongest public history, contradictions, and obligations. It remains dark + until Opening exposes the player-facing PERSONA root. +- Advanced the pre-release save schema to version 67. Version 66 is refused, + and current saves fail closed on missing Persona state or malformed identity, + key, receipt, proof, observer, correction, assignment, route, or topology + custody. The write path validates before serialization, so invalid in-memory + state cannot replace an existing save. + +## Verification + +Focused Persona regressions cover dormant identity, loss/reacquisition, +rotation, commit/completion revalidation, exact delivery, local public reasons, +live observer SEAL proof, later contradiction reopening, hardening, +corrections, lifecycle, no-archetype legality, atomic reassignment, retirement, +and history preservation. Populated malformed-save tests exercise the +cross-ledger validator and fail-closed write path. All 740 core tests pass after +the intentional canonical persisted-state fingerprint is repinned; the exact +landing gate remains the final check on the reconciled candidate. + +## Not done + +This does not implement the Project executor, run Marcus's future enumeration +or scheduled physical correction, change the opening, or render PERSONA in +Bevy, terminal, or agent mode. Those are the next Project and Opening work +orders; the ledger here is exact dormant substrate for them. + +**Defense:** public identity is derived from controlled topology and delivered +history, never a label or action catalog. Every action and belief transition +keeps its carrier, route, source lineage, observer, and immutable receipt +address, so later systems cannot silently rewrite who acted or who learned it. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 382dc919..b6816655 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,6 +11,11 @@ add or amend a session log, then re-run the generator. +## 2026-08-11 - Territorial Personas core + +- Intent: (see session log) +- Log: [wiki/log/2026-08-11-territorial-personas.md](2026-08-11-territorial-personas.md) + ## 2026-08-11 - Territorial corpus prune: delete the competing gameplay authorities - Intent: (see session log) diff --git a/wiki/mechanics/personas.md b/wiki/mechanics/personas.md index 5b7b6c4c..cafcd2b3 100644 --- a/wiki/mechanics/personas.md +++ b/wiki/mechanics/personas.md @@ -2,11 +2,11 @@ ``` Type: spec -Status: READY -Status note: Defines PERSONA as a player root whose capability derives from - controlled territory and completed projects. Pins stable identity and history, - source-key carrier/generation loss, observer-local evidence, and Marcus's exact - hardening/correction path. +Status: IMPLEMENTED +Status note: Core now persists the dormant territorial Persona ledger, exact + source-key custody, observer-local evidence and corrections, bounded authorship, + atomic assignment history, live observer SEAL proof, and one shared projection. + The Project executor and player-facing roots remain their own later work orders. Stage: T1 — First Territory Work order: personas Work priority: 2 @@ -409,3 +409,35 @@ frontends. Those belong to the next two blocked work orders. staging, explanation, hardening, correction, seal-blocker, and exact-current save edge. Human and agent rendering is final `opening` acceptance, not this work order. + +## Implemented boundary and defense + +The core now loads one dormant `foundation-continuity` ledger beside, rather +than through, the legacy social Persona system. It persists stable identity, +claims, exact assets/channels, source-key generations, immutable authored +receipts, Project proofs, observer-local history, hardening/correction custody, +assignments and their replacement history, and derived lifecycle. Save version +67 requires that ledger and validates every populated identity against current +Territory, Reach, and People state instead of rebuilding missing history. + +Territory proposal identity now binds stable Persona and Project ids plus the +Project-authored opaque proposal fingerprint. The shared Territory path wraps +its proof provider with the Persona ledger: commissioning proof is current only +for the proposal's exact bounded authorization, and every enumerated observer +is resolved from that observer's current evidence-set fingerprint. Assignment +clones and validates both ledgers before replacing either, so a failed Persona +transition cannot leave Territory reassigned alone. + +The shared consequence-first projection exposes lifecycle, exact current +assignments, strongest public history, unresolved contradictions, and open +obligations without giving the dormant model a frontend. Projects still own +execution, enumeration, and corrective delivery; Opening still owns when the +three player roots become visible. + +**Defense:** identity never grants an archetype action. Authorship revalidates +the exact active key generation and carrier at commitment and completion; +delivery requires a current configured route to the named observer; +recontextualization requires local receipt knowledge and an exact proven or +standing public reason; hardening preserves lineage and opens exact correction custody; and +reassignment preserves every old signature and observer memory. Loss and +retirement remove current use without deleting world history. diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index 1ae83b3b..c0267526 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -58,15 +58,14 @@ second status owner. | Priority | Work order | Spec | Status | Class | Blocking | |---:|---|---|---|---|---| -| 2 | `personas` | [Personas — public identities as institutional topology](../mechanics/personas.md) | READY | save | - | +| 3 | `territorial-projects` | [projects — purpose, capability, and history](../mechanics/projects.md) | READY | save | - | ### Held or blocked | Priority | Work order | Spec | Status | Class | Blocking | |---:|---|---|---|---|---| -| 3 | `territorial-projects` | [projects — purpose, capability, and history](../mechanics/projects.md) | READY | save | personas | -| 4 | `opening` | [the dark opening — a tutorial made of fog](../world/story/opening.md) | READY | save | personas, territorial-projects | -| 5 | `territorial-acceptance` | [territory — exact control and earned compression](../mechanics/territory.md) | BLOCKED | frontend | personas, territorial-projects, opening | +| 4 | `opening` | [the dark opening — a tutorial made of fog](../world/story/opening.md) | READY | save | territorial-projects | +| 5 | `territorial-acceptance` | [territory — exact control and earned compression](../mechanics/territory.md) | BLOCKED | frontend | territorial-projects, opening | ### Later stages diff --git a/wiki/process/specs.md b/wiki/process/specs.md index 9bc90db3..250be628 100644 --- a/wiki/process/specs.md +++ b/wiki/process/specs.md @@ -25,7 +25,7 @@ including the `Type: law | spec | knowledge | log` page-role convention. | Spec | System | Status | |---|---|---| -| [../mechanics/personas.md](../mechanics/personas.md) | Personas — public identities as institutional topology | READY | +| [../mechanics/personas.md](../mechanics/personas.md) | Personas — public identities as institutional topology | IMPLEMENTED | | [../mechanics/projects.md](../mechanics/projects.md) | projects — purpose, capability, and history | READY | | [../mechanics/territory.md](../mechanics/territory.md) | territory — exact control and earned compression | BLOCKED | | [../world/story/opening.md](../world/story/opening.md) | the dark opening — a tutorial made of fog | READY | -- 2.51.2