Skip to main content

canwu_sim/
lib.rs

1//! Deterministic runtime, validated commands, scheduling, plugins, and snapshots.
2
3#![allow(
4    clippy::missing_errors_doc,
5    clippy::module_name_repetitions,
6    clippy::too_many_lines
7)]
8
9mod boundary;
10mod hashing;
11mod ingress;
12mod manifest;
13mod migration;
14mod persistence;
15mod plugins;
16mod policy;
17mod random;
18mod records;
19mod replay;
20mod scheduling;
21mod settlement;
22mod state;
23mod transactions;
24mod validation;
25
26pub use boundary::{
27    BoundaryChange, BoundaryContext, BoundaryDirective, BoundaryEmission, BoundaryEmissionKind,
28    BoundaryIngressGeneration, BoundaryProposal, BoundaryReceipt, BoundaryRecord, BoundaryRequest,
29    BoundarySystemContract, BoundarySystemHandler, ReservationAllocation, ReservationDisposition,
30    ReservationOffer, ReservationOfferRecord, ReservationPoolKey, ReservationRef,
31    ReservationRequest, ReservationRequestRecord,
32};
33pub use ingress::{
34    IngressClass, IngressPayload, IngressReceipt, IngressRecord, PluginIngressDescriptor,
35    PluginIngressRequest,
36};
37pub use manifest::{ArtifactManifest, RUN_MANIFEST_FORMAT_VERSION, RunManifest};
38pub use persistence::{
39    CHECKPOINT_JOURNAL_FORMAT_VERSION, CheckpointJournal, CompactedSimulation, EvidenceCursor,
40    EvidenceJournalSegment, SimulationCheckpoint,
41};
42pub use policy::{
43    CommandPolicyContext, ControllerPolicy, InteractionPolicy, ObservationPolicy,
44    RUN_CONFIGURATION_FORMAT_VERSION, RunConfiguration, RunConfigurationSnapshot, RunPurpose,
45    SeatBinding, SeatPolicy, TracePolicy,
46};
47pub use random::{
48    RandomAlgorithm, RandomDrawOutcome, RandomDrawProducer, RandomDrawRecord, RandomStreamKey,
49    RandomStreamState,
50};
51pub use records::{
52    DomainRecord, DomainRecordChange, DomainRecordClass, DomainRecordDraft, DomainRecordLifecycle,
53    DomainRecordMutation, DomainRecordOperation, DomainRecordSchema, DomainReference,
54    DomainReferenceSchema, DomainReferenceTarget, DomainReferenceTargetKind,
55};
56
57use canwu_core::{
58    ArmyId, BoundaryId, CommandAttemptId, CommandId, CommandRequestId, DeterministicRng,
59    DomainRecordKind, DomainRecordRef, DomainRecordType, EntityRef, EventId, FieldSchema,
60    GovernmentId, IngressId, PersonId, RandomDrawId, RouteId, SchemaRegistry, TerritoryId,
61    TypeSchema, TypedDomainRecordRef,
62};
63pub use canwu_event::{CauseRef, EventAudience, EventKind, SimEvent};
64use canwu_knowledge::{
65    ActorKnowledge, ArmyKnowledge, EstimateRange, KnowledgeSnapshot, KnowledgeSource,
66};
67use canwu_time::{SimDuration, SimTime};
68use canwu_world::{
69    Army, Government, MapPoint, Person, Route, Territory, TransitState, WorldSnapshot,
70};
71use serde::{Deserialize, Serialize};
72use serde_json::Value;
73use std::cell::RefCell;
74use std::collections::{BTreeMap, BTreeSet, HashSet};
75use std::error::Error;
76use std::fmt::{Display, Formatter};
77use std::panic::{AssertUnwindSafe, catch_unwind};
78
79use hashing::{
80    ControlCommitmentMaterial, StateHashMaterial, authoritative_run_identity,
81    boundary_state_hash_for_commitments, canonical_hash, checkpoint_hash_for_commitments,
82    checkpoint_hash_for_configuration, commitment_roots_are_canonical, compute_boundary_hash,
83    domain_record_commitment_root, identity_commitment_root, is_canonical_hash,
84    knowledge_commitment_root, plugin_component_commitment_root, random_stream_commitment_root,
85    runtime_commitment_roots, scheduler_commitment_root, snapshot_boundary_head_state_hash,
86    snapshot_checkpoint_hash, snapshot_commitment_roots, snapshot_is_at_boundary_head,
87    snapshot_state_hash, state_hash, world_commitment_root,
88};
89use ingress::IngressQueueKey;
90use migration::{
91    PersistedAdmissionCursors, authoritative_revision_count, boundaries_before_attempts,
92    inferred_run_configuration, migrate_snapshot,
93};
94use settlement::{PendingBoundaryRandomDraw, boundary_has_event_ingress, boundary_system_due};
95use state::{
96    CommitmentDomains, JournalCommitmentRoots, RuntimeCommitmentCache,
97    RuntimeCommitmentRootUpdates, RuntimeCounters, RuntimeCurrentState,
98    RuntimeDomainCommitmentRoots, RuntimeEvidence, RuntimeMetadata, RuntimeScheduler, RuntimeState,
99};
100use transactions::{
101    BoundaryTransactionCheckpoint, ClockTransactionCheckpoint, CommandTransactionCheckpoint,
102    IngressTransactionCheckpoint, RejectionTransactionCheckpoint,
103    ScheduledBatchTransactionCheckpoint,
104};
105use validation::{
106    RuntimeValidationContext, claim_counter, core_world_entity_exists,
107    has_unqueued_command_history, proposal_entity_exists, proposal_entity_identity_exists,
108    runtime_current_entity_exists, runtime_entity_exists,
109    runtime_entity_exists_with_record_overlay, runtime_entity_identity_exists,
110    runtime_has_unqueued_command_history, snapshot_entity_exists_in_history,
111    validate_directives_with_context, validate_domain_dependents_with_records,
112    validate_run_configuration_entities, validate_runtime_cause,
113    validate_runtime_domain_dependents, validate_snapshot,
114};
115
116pub const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");
117pub const SNAPSHOT_FORMAT_VERSION: u32 = 4;
118/// Version of the independently migrated authoritative revision commitment.
119pub const STATE_REVISION_FORMAT_VERSION: u32 = 1;
120/// Version of persisted monotonic boundary-admission cursors.
121pub const ADMISSION_CURSOR_FORMAT_VERSION: u32 = 1;
122/// Version of the domain-separated checkpoint commitment contract.
123pub const COMMITMENT_FORMAT_VERSION: u32 = 1;
124/// Maximum nested depth of the compatibility synchronous event-reactor path.
125///
126/// New plugin mechanics should use phased boundary systems instead of relying
127/// on recursively emitted immediate events.
128pub const MAX_SYNCHRONOUS_REACTION_DEPTH: usize = 32;
129const CORE_STATE_NAMESPACE: &str = "canwu.core";
130const GENESIS_BOUNDARY_HASH: &str =
131    "0000000000000000000000000000000000000000000000000000000000000000";
132
133#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
134/// Canonical roots for independent authoritative state and evidence domains.
135pub struct CommitmentRoots {
136    pub world: String,
137    pub knowledge: String,
138    pub plugin_components: String,
139    pub domain_records: String,
140    pub scheduler: String,
141    pub commands: String,
142    pub events: String,
143    pub ingress: String,
144    pub random: String,
145    pub boundary_chain: String,
146    pub identity: String,
147    pub control: String,
148}
149
150#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
151#[serde(rename_all = "snake_case")]
152pub enum ErrorCode {
153    ActorNotFound,
154    ArchiveNotReady,
155    ArmyNotFound,
156    DestinationNotFound,
157    DuplicateBoundaryWriter,
158    DuplicateDomainRecord,
159    DuplicateDomainRecordKind,
160    DuplicatePlugin,
161    DuplicatePluginCommand,
162    DuplicatePluginIngress,
163    DuplicatePluginSystem,
164    DuplicateStateOwner,
165    DuplicateReservationOfferer,
166    EntityNotFound,
167    DomainRecordNotFound,
168    DomainRecordReferenced,
169    DomainRecordVersionConflict,
170    InvalidAuthority,
171    InvalidBoundary,
172    InvalidDuration,
173    InvalidDomainRecord,
174    IdempotencyConflict,
175    InteractionReadOnly,
176    InvalidPayload,
177    InvalidPluginRegistration,
178    InvalidRandomDraw,
179    InvalidRandomStream,
180    InvalidRunConfiguration,
181    InvalidRunManifest,
182    InvalidSnapshot,
183    IdentifierExhausted,
184    LegacyReplayUnavailable,
185    LateIngress,
186    MissingIdempotencyKey,
187    MixedCommandIngress,
188    NoRoute,
189    PluginCommandNotFound,
190    PluginManifestMismatch,
191    PluginNotActive,
192    PluginPanicked,
193    PluginRegistrationClosed,
194    ReplayMismatch,
195    ReplayEnvironmentMismatch,
196    SimulationRevisionConflict,
197    SimulationTimeConflict,
198    SynchronousReactionLimit,
199    UndeclaredRandomStream,
200    UndeclaredStateRead,
201    UndeclaredStateWrite,
202    UnsupportedSnapshotVersion,
203    ValueOutOfRange,
204}
205
206#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
207pub struct CanwuError {
208    pub code: ErrorCode,
209    pub message: String,
210    pub related_entities: Vec<EntityRef>,
211}
212
213impl CanwuError {
214    #[must_use]
215    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
216        Self {
217            code,
218            message: message.into(),
219            related_entities: Vec::new(),
220        }
221    }
222
223    #[must_use]
224    pub fn with_entity(mut self, entity: EntityRef) -> Self {
225        self.related_entities.push(entity);
226        self
227    }
228}
229
230impl Display for CanwuError {
231    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
232        write!(
233            formatter,
234            "{}: {}",
235            error_code_name(&self.code),
236            self.message
237        )
238    }
239}
240
241impl Error for CanwuError {}
242
243const fn error_code_name(code: &ErrorCode) -> &'static str {
244    match code {
245        ErrorCode::ActorNotFound => "actor_not_found",
246        ErrorCode::ArchiveNotReady => "archive_not_ready",
247        ErrorCode::ArmyNotFound => "army_not_found",
248        ErrorCode::DestinationNotFound => "destination_not_found",
249        ErrorCode::DuplicateBoundaryWriter => "duplicate_boundary_writer",
250        ErrorCode::DuplicateDomainRecord => "duplicate_domain_record",
251        ErrorCode::DuplicateDomainRecordKind => "duplicate_domain_record_kind",
252        ErrorCode::DuplicatePlugin => "duplicate_plugin",
253        ErrorCode::DuplicatePluginCommand => "duplicate_plugin_command",
254        ErrorCode::DuplicatePluginIngress => "duplicate_plugin_ingress",
255        ErrorCode::DuplicatePluginSystem => "duplicate_plugin_system",
256        ErrorCode::DuplicateStateOwner => "duplicate_state_owner",
257        ErrorCode::DuplicateReservationOfferer => "duplicate_reservation_offerer",
258        ErrorCode::EntityNotFound => "entity_not_found",
259        ErrorCode::DomainRecordNotFound => "domain_record_not_found",
260        ErrorCode::DomainRecordReferenced => "domain_record_referenced",
261        ErrorCode::DomainRecordVersionConflict => "domain_record_version_conflict",
262        ErrorCode::InvalidAuthority => "invalid_authority",
263        ErrorCode::InvalidBoundary => "invalid_boundary",
264        ErrorCode::InvalidDuration => "invalid_duration",
265        ErrorCode::InvalidDomainRecord => "invalid_domain_record",
266        ErrorCode::IdempotencyConflict => "idempotency_conflict",
267        ErrorCode::InteractionReadOnly => "interaction_read_only",
268        ErrorCode::InvalidPayload => "invalid_payload",
269        ErrorCode::InvalidPluginRegistration => "invalid_plugin_registration",
270        ErrorCode::InvalidRandomDraw => "invalid_random_draw",
271        ErrorCode::InvalidRandomStream => "invalid_random_stream",
272        ErrorCode::InvalidRunConfiguration => "invalid_run_configuration",
273        ErrorCode::InvalidRunManifest => "invalid_run_manifest",
274        ErrorCode::InvalidSnapshot => "invalid_snapshot",
275        ErrorCode::IdentifierExhausted => "identifier_exhausted",
276        ErrorCode::LegacyReplayUnavailable => "legacy_replay_unavailable",
277        ErrorCode::LateIngress => "late_ingress",
278        ErrorCode::MissingIdempotencyKey => "missing_idempotency_key",
279        ErrorCode::MixedCommandIngress => "mixed_command_ingress",
280        ErrorCode::NoRoute => "no_route",
281        ErrorCode::PluginCommandNotFound => "plugin_command_not_found",
282        ErrorCode::PluginManifestMismatch => "plugin_manifest_mismatch",
283        ErrorCode::PluginNotActive => "plugin_not_active",
284        ErrorCode::PluginPanicked => "plugin_panicked",
285        ErrorCode::PluginRegistrationClosed => "plugin_registration_closed",
286        ErrorCode::ReplayMismatch => "replay_mismatch",
287        ErrorCode::ReplayEnvironmentMismatch => "replay_environment_mismatch",
288        ErrorCode::SimulationRevisionConflict => "simulation_revision_conflict",
289        ErrorCode::SimulationTimeConflict => "simulation_time_conflict",
290        ErrorCode::SynchronousReactionLimit => "synchronous_reaction_limit",
291        ErrorCode::UndeclaredRandomStream => "undeclared_random_stream",
292        ErrorCode::UndeclaredStateRead => "undeclared_state_read",
293        ErrorCode::UndeclaredStateWrite => "undeclared_state_write",
294        ErrorCode::UnsupportedSnapshotVersion => "unsupported_snapshot_version",
295        ErrorCode::ValueOutOfRange => "value_out_of_range",
296    }
297}
298
299#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
300#[serde(tag = "type", content = "id", rename_all = "snake_case")]
301pub enum Issuer {
302    Actor(PersonId),
303    Human(String),
304    Ai(String),
305    Institution(String),
306    Replay(String),
307    Experiment(String),
308    Debug,
309    System(String),
310}
311
312#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
313#[serde(rename_all = "snake_case")]
314pub enum CommandIngress {
315    LegacyDirect,
316    LiveRequest,
317    FrozenReplay,
318}
319
320#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
321#[serde(tag = "type", rename_all = "snake_case")]
322pub enum DecisionOrigin {
323    Actor {
324        actor: PersonId,
325    },
326    Institution {
327        institution: EntityRef,
328        responsible_actor: Option<PersonId>,
329    },
330    Council {
331        council_id: String,
332    },
333    NoResponsibleActor {
334        reason: String,
335    },
336}
337
338#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
339pub struct CommandAuthority {
340    pub decision_origin: DecisionOrigin,
341    pub seat_id: Option<String>,
342    pub permission_profile_id: Option<String>,
343    pub command_subject: Option<EntityRef>,
344}
345
346impl CommandAuthority {
347    #[must_use]
348    pub const fn for_actor(actor: PersonId) -> Self {
349        Self {
350            decision_origin: DecisionOrigin::Actor { actor },
351            seat_id: None,
352            permission_profile_id: None,
353            command_subject: None,
354        }
355    }
356
357    #[must_use]
358    pub fn no_responsible_actor(reason: impl Into<String>) -> Self {
359        Self {
360            decision_origin: DecisionOrigin::NoResponsibleActor {
361                reason: reason.into(),
362            },
363            seat_id: None,
364            permission_profile_id: None,
365            command_subject: None,
366        }
367    }
368}
369
370#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
371pub struct CommandContext {
372    pub issuer: Issuer,
373    pub authority: CommandAuthority,
374    pub run_policy: CommandPolicyContext,
375    pub ingress: CommandIngress,
376    pub attempt_id: Option<CommandAttemptId>,
377    pub command_id: CommandId,
378    pub request_id: Option<CommandRequestId>,
379    pub revision: u64,
380    pub simulation_time: SimTime,
381    pub expected_revision: Option<u64>,
382    pub expected_time: Option<SimTime>,
383}
384
385#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
386#[repr(u8)]
387#[serde(rename_all = "snake_case")]
388pub enum BoundaryPhase {
389    EventIngress = 1,
390    BoundarySnapshot = 2,
391    DerivedFieldSolve = 3,
392    PerceptionAndAttentionRefresh = 4,
393    DecisionAndAcceptedEffectIntake = 5,
394    ReservationAndAllocation = 6,
395    DomainDeltaProposal = 7,
396    InvariantValidation = 8,
397    AtomicDomainCommit = 9,
398    HistoricalCandidateEvaluation = 10,
399    ConditionalTransitionCommit = 11,
400    StrategicAggregation = 12,
401    PerspectiveAndReportMaterialization = 13,
402    SaveReplayAndDiagnosticHashing = 14,
403}
404
405impl BoundaryPhase {
406    pub const ALL: [Self; 14] = [
407        Self::EventIngress,
408        Self::BoundarySnapshot,
409        Self::DerivedFieldSolve,
410        Self::PerceptionAndAttentionRefresh,
411        Self::DecisionAndAcceptedEffectIntake,
412        Self::ReservationAndAllocation,
413        Self::DomainDeltaProposal,
414        Self::InvariantValidation,
415        Self::AtomicDomainCommit,
416        Self::HistoricalCandidateEvaluation,
417        Self::ConditionalTransitionCommit,
418        Self::StrategicAggregation,
419        Self::PerspectiveAndReportMaterialization,
420        Self::SaveReplayAndDiagnosticHashing,
421    ];
422}
423
424#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
425#[serde(rename_all = "snake_case")]
426pub enum SystemCadence {
427    EventDriven,
428    SubDaily,
429    Daily,
430    Monthly,
431    Seasonal,
432    Annual,
433    EraScheduled,
434}
435
436#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
437#[serde(rename_all = "snake_case")]
438pub enum StateVisibility {
439    SameBoundary,
440    NextBoundary,
441}
442
443#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
444pub struct StateKey {
445    pub namespace: String,
446    pub name: String,
447}
448
449impl StateKey {
450    #[must_use]
451    pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
452        Self {
453            namespace: namespace.into(),
454            name: name.into(),
455        }
456    }
457
458    #[must_use]
459    pub fn core_people() -> Self {
460        Self::new(CORE_STATE_NAMESPACE, "people")
461    }
462
463    #[must_use]
464    pub fn core_governments() -> Self {
465        Self::new(CORE_STATE_NAMESPACE, "governments")
466    }
467
468    #[must_use]
469    pub fn core_territories() -> Self {
470        Self::new(CORE_STATE_NAMESPACE, "territories")
471    }
472
473    #[must_use]
474    pub fn core_routes() -> Self {
475        Self::new(CORE_STATE_NAMESPACE, "routes")
476    }
477
478    #[must_use]
479    pub fn core_armies() -> Self {
480        Self::new(CORE_STATE_NAMESPACE, "armies")
481    }
482
483    #[must_use]
484    pub fn core_knowledge() -> Self {
485        Self::new(CORE_STATE_NAMESPACE, "knowledge")
486    }
487
488    #[must_use]
489    pub fn core_commands() -> Self {
490        Self::new(CORE_STATE_NAMESPACE, "commands")
491    }
492
493    #[must_use]
494    pub fn core_events() -> Self {
495        Self::new(CORE_STATE_NAMESPACE, "events")
496    }
497
498    #[must_use]
499    pub fn core_ingress() -> Self {
500        Self::new(CORE_STATE_NAMESPACE, "ingress")
501    }
502}
503
504#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
505pub struct SystemContract {
506    pub name: String,
507    pub phase: BoundaryPhase,
508    pub cadence: SystemCadence,
509    pub reads: Vec<StateKey>,
510    pub writes: Vec<StateKey>,
511    pub visibility: StateVisibility,
512}
513
514impl SystemContract {
515    #[must_use]
516    pub fn event_driven(name: impl Into<String>, phase: BoundaryPhase) -> Self {
517        Self {
518            name: name.into(),
519            phase,
520            cadence: SystemCadence::EventDriven,
521            reads: Vec::new(),
522            writes: Vec::new(),
523            visibility: StateVisibility::SameBoundary,
524        }
525    }
526}
527
528#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
529#[serde(tag = "type", rename_all = "snake_case")]
530pub enum Command {
531    MoveArmy {
532        army: ArmyId,
533        destination: TerritoryId,
534    },
535    DebugSetArmyMorale {
536        army: ArmyId,
537        morale: u16,
538    },
539    Plugin {
540        plugin: String,
541        command: String,
542        payload: Value,
543    },
544}
545
546#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
547pub struct CommandEnvelope {
548    pub issuer: Issuer,
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    pub authority: Option<CommandAuthority>,
551    pub command: Command,
552    pub expected_time: Option<SimTime>,
553}
554
555impl CommandEnvelope {
556    #[must_use]
557    pub const fn new(issuer: Issuer, command: Command) -> Self {
558        Self {
559            issuer,
560            authority: None,
561            command,
562            expected_time: None,
563        }
564    }
565
566    #[must_use]
567    pub const fn at_time(mut self, expected_time: SimTime) -> Self {
568        self.expected_time = Some(expected_time);
569        self
570    }
571
572    #[must_use]
573    pub fn with_authority(mut self, authority: CommandAuthority) -> Self {
574        self.authority = Some(authority);
575        self
576    }
577}
578
579#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
580pub struct CommandRequest {
581    pub request_id: CommandRequestId,
582    /// Must equal the persisted authoritative revision at command admission.
583    ///
584    /// Accepted commands, persisted expected rejections, and completed
585    /// settlement boundaries advance the revision. Bare clock movement does
586    /// not, so declared external commands also carry `envelope.expected_time`.
587    pub expected_revision: u64,
588    pub envelope: CommandEnvelope,
589}
590
591impl CommandRequest {
592    #[must_use]
593    pub const fn new(
594        request_id: CommandRequestId,
595        expected_revision: u64,
596        envelope: CommandEnvelope,
597    ) -> Self {
598        Self {
599            request_id,
600            expected_revision,
601            envelope,
602        }
603    }
604}
605
606#[derive(Clone, Copy)]
607struct CommandAdmission {
608    request_id: Option<CommandRequestId>,
609    expected_revision: Option<u64>,
610    expected_time: Option<SimTime>,
611    revision_before: u64,
612    ingress: CommandIngress,
613}
614
615#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
616pub struct CommandRecord {
617    pub id: CommandId,
618    #[serde(default, skip_serializing_if = "Option::is_none")]
619    pub attempt_id: Option<CommandAttemptId>,
620    pub accepted_at: SimTime,
621    pub envelope: CommandEnvelope,
622    #[serde(default, skip_serializing_if = "Vec::is_empty")]
623    pub emitted_events: Vec<EventId>,
624}
625
626#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
627pub struct CommandReceipt {
628    pub attempt_id: Option<CommandAttemptId>,
629    pub command_id: CommandId,
630    pub request_id: Option<CommandRequestId>,
631    /// Authoritative revision after the accepted command commits.
632    pub revision: u64,
633    pub accepted_at: SimTime,
634    pub emitted_events: Vec<EventId>,
635}
636
637#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
638pub struct CommandRejection {
639    pub attempt_id: Option<CommandAttemptId>,
640    pub request_id: Option<CommandRequestId>,
641    /// Authoritative revision after persisted rejection evidence commits.
642    /// Non-persisted conflicts retain the already committed current revision.
643    pub retained_revision: u64,
644    pub rejected_at: SimTime,
645    pub error: CanwuError,
646}
647
648#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
649#[serde(tag = "decision", rename_all = "snake_case")]
650pub enum CommandOutcome {
651    Accepted { receipt: CommandReceipt },
652    Rejected { rejection: CommandRejection },
653}
654
655#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
656#[serde(tag = "decision", rename_all = "snake_case")]
657pub enum CommandAttemptOutcome {
658    Accepted { command_id: CommandId },
659    Rejected { error: CanwuError },
660}
661
662#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
663pub struct CommandAttemptRecord {
664    pub id: CommandAttemptId,
665    pub at: SimTime,
666    /// Authoritative revision immediately before this attempt transaction.
667    pub revision_before: u64,
668    pub ingress: CommandIngress,
669    pub request_id: Option<CommandRequestId>,
670    pub expected_revision: Option<u64>,
671    pub envelope: CommandEnvelope,
672    pub outcome: CommandAttemptOutcome,
673}
674
675#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
676pub struct DemoIds {
677    pub commander: PersonId,
678    pub observer: PersonId,
679    pub government: GovernmentId,
680    pub army: ArmyId,
681    pub western_territory: TerritoryId,
682    pub central_territory: TerritoryId,
683    pub eastern_territory: TerritoryId,
684}
685
686#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
687pub struct Scenario {
688    pub start_time: SimTime,
689    pub world: WorldSnapshot,
690    pub knowledge: KnowledgeSnapshot,
691    #[serde(default, skip_serializing_if = "Vec::is_empty")]
692    pub domain_records: Vec<DomainRecord>,
693}
694
695#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
696#[serde(rename_all = "snake_case")]
697pub enum PayloadValueType {
698    Null,
699    Boolean,
700    Integer,
701    String,
702    Object,
703    Array,
704}
705
706impl PayloadValueType {
707    fn matches(&self, value: &Value) -> bool {
708        match self {
709            Self::Null => value.is_null(),
710            Self::Boolean => value.is_boolean(),
711            Self::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
712            Self::String => value.is_string(),
713            Self::Object => value.is_object(),
714            Self::Array => value.is_array(),
715        }
716    }
717}
718
719#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
720pub struct PayloadProperty {
721    pub value_type: PayloadValueType,
722    pub required: bool,
723}
724
725#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
726#[serde(tag = "type", rename_all = "snake_case")]
727pub enum PayloadSchema {
728    Any,
729    Null,
730    Boolean,
731    Integer,
732    String,
733    Object {
734        properties: BTreeMap<String, PayloadProperty>,
735        allow_additional: bool,
736    },
737}
738
739impl PayloadSchema {
740    fn validate(&self, value: &Value) -> Result<(), CanwuError> {
741        let scalar_matches = match self {
742            Self::Any => return Ok(()),
743            Self::Null => value.is_null(),
744            Self::Boolean => value.is_boolean(),
745            Self::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
746            Self::String => value.is_string(),
747            Self::Object {
748                properties,
749                allow_additional,
750            } => {
751                let Some(object) = value.as_object() else {
752                    return Err(CanwuError::new(
753                        ErrorCode::InvalidPayload,
754                        "plugin command payload must be an object",
755                    ));
756                };
757                for (name, property) in properties {
758                    match object.get(name) {
759                        Some(field) if !property.value_type.matches(field) => {
760                            return Err(CanwuError::new(
761                                ErrorCode::InvalidPayload,
762                                format!("payload field {name} has the wrong type"),
763                            ));
764                        }
765                        None if property.required => {
766                            return Err(CanwuError::new(
767                                ErrorCode::InvalidPayload,
768                                format!("payload field {name} is required"),
769                            ));
770                        }
771                        Some(_) | None => {}
772                    }
773                }
774                if !allow_additional && object.keys().any(|name| !properties.contains_key(name)) {
775                    return Err(CanwuError::new(
776                        ErrorCode::InvalidPayload,
777                        "plugin command payload contains an undeclared field",
778                    ));
779                }
780                return Ok(());
781            }
782        };
783        if scalar_matches {
784            Ok(())
785        } else {
786            Err(CanwuError::new(
787                ErrorCode::InvalidPayload,
788                "plugin command payload does not match its declared schema",
789            ))
790        }
791    }
792}
793
794#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
795pub struct PluginActionDescriptor {
796    pub name: String,
797    pub description: String,
798    pub payload_schema: PayloadSchema,
799    pub reads: Vec<StateKey>,
800    pub writes: Vec<StateKey>,
801}
802
803#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
804pub struct PluginDescriptor {
805    pub name: String,
806    #[serde(default)]
807    pub version: String,
808    #[serde(default)]
809    pub semantic_hash: String,
810    /// Declarative visibility policies for emitted plugin event types.
811    /// Unlisted event types are private to player-facing projections.
812    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
813    pub event_audiences: BTreeMap<String, EventAudience>,
814    pub systems: Vec<SystemContract>,
815    #[serde(default)]
816    pub boundary_systems: Vec<BoundarySystemContract>,
817    pub commands: Vec<PluginActionDescriptor>,
818    #[serde(default, skip_serializing_if = "Vec::is_empty")]
819    pub ingress: Vec<PluginIngressDescriptor>,
820    pub schema_types: Vec<String>,
821    #[serde(default, skip_serializing_if = "Vec::is_empty")]
822    pub record_schemas: Vec<DomainRecordSchema>,
823}
824
825#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
826pub struct PluginComponentRecord {
827    pub plugin: String,
828    pub state: StateKey,
829    pub entity: EntityRef,
830    pub component: String,
831    pub value: Value,
832}
833
834#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
835struct PluginComponentKey {
836    plugin: String,
837    state: StateKey,
838    entity: EntityRef,
839    component: String,
840}
841
842#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
843#[serde(tag = "type", rename_all = "snake_case")]
844pub enum SystemDirective {
845    SetComponent {
846        state: StateKey,
847        entity: EntityRef,
848        component: String,
849        value: Value,
850        summary: String,
851    },
852    Emit {
853        event_type: String,
854        summary: String,
855        affected: Vec<EntityRef>,
856    },
857    Schedule {
858        after: SimDuration,
859        directive: Box<SystemDirective>,
860    },
861}
862
863enum SimulationViewState<'a> {
864    Runtime(&'a RuntimeState),
865    Boundary {
866        current: &'a RuntimeCurrentState,
867        now: SimTime,
868        evidence: &'a RuntimeEvidence,
869    },
870}
871
872impl SimulationViewState<'_> {
873    const fn current(&self) -> &RuntimeCurrentState {
874        match self {
875            Self::Runtime(state) => &state.current,
876            Self::Boundary { current, .. } => current,
877        }
878    }
879
880    const fn now(&self) -> SimTime {
881        match self {
882            Self::Runtime(state) => state.scheduler.now,
883            Self::Boundary { now, .. } => *now,
884        }
885    }
886
887    const fn evidence(&self) -> &RuntimeEvidence {
888        match self {
889            Self::Runtime(state) => &state.evidence,
890            Self::Boundary { evidence, .. } => evidence,
891        }
892    }
893}
894
895pub struct SimulationView<'a> {
896    state: SimulationViewState<'a>,
897    state_owners: &'a BTreeMap<StateKey, String>,
898    reader: Option<&'a str>,
899    allowed_reads: Option<&'a [StateKey]>,
900    allowed_ingress: Option<&'a HashSet<IngressId>>,
901    ingress_plugin: Option<&'a str>,
902    component_overlay: Option<&'a BTreeMap<PluginComponentKey, PluginComponentRecord>>,
903    proposed_components: Option<&'a BTreeMap<PluginComponentKey, PluginComponentRecord>>,
904    record_overlay: Option<&'a BTreeMap<DomainRecordRef, DomainRecord>>,
905    proposed_records: Option<&'a BTreeMap<DomainRecordRef, DomainRecord>>,
906    allocations: Option<&'a BTreeMap<ReservationRef, ReservationAllocation>>,
907    allowed_reservations: Option<&'a [ReservationRef]>,
908    random_session: Option<RefCell<random::RandomSession>>,
909}
910
911impl SimulationView<'_> {
912    #[must_use]
913    pub const fn time(&self) -> SimTime {
914        self.state.now()
915    }
916
917    pub fn army(&self, id: ArmyId) -> Result<Option<&Army>, CanwuError> {
918        self.require_read(&StateKey::core_armies())?;
919        Ok(self.state.current().armies.get(&id))
920    }
921
922    pub fn person(&self, id: PersonId) -> Result<Option<&Person>, CanwuError> {
923        self.require_read(&StateKey::core_people())?;
924        Ok(self.state.current().people.get(&id))
925    }
926
927    pub fn government(&self, id: GovernmentId) -> Result<Option<&Government>, CanwuError> {
928        self.require_read(&StateKey::core_governments())?;
929        Ok(self.state.current().governments.get(&id))
930    }
931
932    pub fn territory(&self, id: TerritoryId) -> Result<Option<&Territory>, CanwuError> {
933        self.require_read(&StateKey::core_territories())?;
934        Ok(self.state.current().territories.get(&id))
935    }
936
937    pub fn route(&self, id: RouteId) -> Result<Option<&Route>, CanwuError> {
938        self.require_read(&StateKey::core_routes())?;
939        Ok(self.state.current().routes.get(&id))
940    }
941
942    pub fn actor_knowledge(&self, actor: PersonId) -> Result<Option<&ActorKnowledge>, CanwuError> {
943        self.require_read(&StateKey::core_knowledge())?;
944        Ok(self.state.current().knowledge.for_actor(actor))
945    }
946
947    /// Resolves an exact command ID from the retained runtime journal in O(1).
948    ///
949    /// A command that has already been sealed into a live archive is not
950    /// available through this view and returns `None`.
951    pub fn command(&self, id: CommandId) -> Result<Option<&CommandRecord>, CanwuError> {
952        self.require_read(&StateKey::core_commands())?;
953        Ok(self.state.evidence().retained_command(id))
954    }
955
956    /// Resolves an exact event ID from the retained runtime journal in O(1).
957    ///
958    /// An event that has already been sealed into a live archive is not
959    /// available through this view and returns `None`.
960    pub fn event(&self, id: EventId) -> Result<Option<&SimEvent>, CanwuError> {
961        self.require_read(&StateKey::core_events())?;
962        Ok(self.state.evidence().retained_event(id))
963    }
964
965    pub fn ingress(&self, id: IngressId) -> Result<Option<&IngressRecord>, CanwuError> {
966        self.require_read(&StateKey::core_ingress())?;
967        if self
968            .allowed_ingress
969            .is_none_or(|allowed| !allowed.contains(&id))
970        {
971            return Ok(None);
972        }
973        let record = self.state.evidence().retained_ingress(id);
974        if let (Some(owner), Some(record)) = (self.ingress_plugin, record)
975            && !matches!(
976                &record.payload,
977                IngressPayload::Plugin { plugin, .. } if plugin == owner
978            )
979        {
980            return Ok(None);
981        }
982        Ok(record)
983    }
984
985    pub fn domain_record(
986        &self,
987        reference: &DomainRecordRef,
988    ) -> Result<Option<&DomainRecord>, CanwuError> {
989        self.require_read(&records::record_state_key(&reference.kind))?;
990        Ok(self
991            .record_overlay
992            .and_then(|overlay| overlay.get(reference))
993            .or_else(|| self.state.current().domain_records.get(reference)))
994    }
995
996    pub fn typed_domain_record<T: DomainRecordType>(
997        &self,
998        reference: &TypedDomainRecordRef<T>,
999    ) -> Result<Option<&DomainRecord>, CanwuError> {
1000        self.domain_record(reference.as_untyped())
1001    }
1002
1003    pub fn proposed_domain_record(
1004        &self,
1005        reference: &DomainRecordRef,
1006    ) -> Result<Option<&DomainRecord>, CanwuError> {
1007        self.require_read(&records::record_state_key(&reference.kind))?;
1008        Ok(self
1009            .proposed_records
1010            .and_then(|records| records.get(reference)))
1011    }
1012
1013    pub fn proposed_typed_domain_record<T: DomainRecordType>(
1014        &self,
1015        reference: &TypedDomainRecordRef<T>,
1016    ) -> Result<Option<&DomainRecord>, CanwuError> {
1017        self.proposed_domain_record(reference.as_untyped())
1018    }
1019
1020    pub fn reservation(
1021        &self,
1022        reservation: &ReservationRef,
1023    ) -> Result<Option<&ReservationAllocation>, CanwuError> {
1024        let reader = self.reader.unwrap_or("unscoped caller");
1025        if self
1026            .allowed_reservations
1027            .is_none_or(|allowed| !allowed.contains(reservation))
1028        {
1029            return Err(CanwuError::new(
1030                ErrorCode::UndeclaredStateRead,
1031                format!(
1032                    "system {reader} did not declare reservation read {}.{}.{}",
1033                    reservation.plugin, reservation.system, reservation.request
1034                ),
1035            ));
1036        }
1037        Ok(self.allocations.and_then(|values| values.get(reservation)))
1038    }
1039
1040    pub fn random_range(
1041        &self,
1042        stream: &RandomStreamKey,
1043        upper_exclusive: u64,
1044        purpose: &str,
1045    ) -> Result<u64, CanwuError> {
1046        let Some(session) = &self.random_session else {
1047            return Err(CanwuError::new(
1048                ErrorCode::UndeclaredRandomStream,
1049                format!(
1050                    "system {} has no declared random streams",
1051                    self.reader.unwrap_or("unscoped caller")
1052                ),
1053            ));
1054        };
1055        session.borrow_mut().range(stream, upper_exclusive, purpose)
1056    }
1057
1058    pub fn component(
1059        &self,
1060        state: &StateKey,
1061        entity: &EntityRef,
1062        component: &str,
1063    ) -> Result<Option<&Value>, CanwuError> {
1064        self.require_read(state)?;
1065        let Some(owner) = self.state_owners.get(state) else {
1066            return Err(CanwuError::new(
1067                ErrorCode::UndeclaredStateRead,
1068                format!(
1069                    "state {}.{} has no registered owner",
1070                    state.namespace, state.name
1071                ),
1072            ));
1073        };
1074        let key = component_key(owner, state, entity, component);
1075        Ok(self
1076            .component_overlay
1077            .and_then(|overlay| overlay.get(&key))
1078            .or_else(|| self.state.current().plugin_components.get(&key))
1079            .map(|record| &record.value))
1080    }
1081
1082    pub fn proposed_component(
1083        &self,
1084        state: &StateKey,
1085        entity: &EntityRef,
1086        component: &str,
1087    ) -> Result<Option<&Value>, CanwuError> {
1088        self.require_read(state)?;
1089        let Some(owner) = self.state_owners.get(state) else {
1090            return Err(CanwuError::new(
1091                ErrorCode::UndeclaredStateRead,
1092                format!(
1093                    "state {}.{} has no registered owner",
1094                    state.namespace, state.name
1095                ),
1096            ));
1097        };
1098        let key = component_key(owner, state, entity, component);
1099        Ok(self
1100            .proposed_components
1101            .and_then(|proposals| proposals.get(&key))
1102            .map(|record| &record.value))
1103    }
1104
1105    fn require_read(&self, state: &StateKey) -> Result<(), CanwuError> {
1106        if self
1107            .allowed_reads
1108            .is_some_and(|reads| !reads.contains(state))
1109        {
1110            return Err(CanwuError::new(
1111                ErrorCode::UndeclaredStateRead,
1112                format!(
1113                    "{} did not declare read access to {}.{}",
1114                    self.reader.unwrap_or("internal system"),
1115                    state.namespace,
1116                    state.name
1117                ),
1118            ));
1119        }
1120        Ok(())
1121    }
1122
1123    fn finish_random_session(self) -> Option<random::RandomExecution> {
1124        self.random_session
1125            .map(RefCell::into_inner)
1126            .map(random::RandomSession::finish)
1127    }
1128}
1129
1130/// Compatibility-only synchronous event reactor.
1131///
1132/// The handler runs inside the event's current transaction. Emitting another
1133/// event from its directives re-enters the same reactor graph and is bounded
1134/// by [`MAX_SYNCHRONOUS_REACTION_DEPTH`]. New mechanics should use a phased
1135/// [`BoundarySystemHandler`] instead.
1136pub type SimulationSystemHandler =
1137    fn(&SimulationView<'_>, &SimEvent) -> Result<Vec<SystemDirective>, CanwuError>;
1138
1139pub type PluginCommandHandler =
1140    fn(&SimulationView<'_>, &CommandContext, &Value) -> Result<Vec<SystemDirective>, CanwuError>;
1141
1142/// A stateless executable package whose persisted identity must change whenever
1143/// its authoritative behavior changes.
1144pub trait SimulationPlugin {
1145    fn name(&self) -> &str;
1146    /// Returns the package or rules release recorded in snapshots.
1147    fn version(&self) -> &str;
1148    /// Returns a lowercase 64-character author-controlled semantic hash.
1149    ///
1150    /// This must change when handler behavior changes even if the serialized
1151    /// registration descriptor remains structurally identical.
1152    fn semantic_hash(&self) -> &str;
1153    fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError>;
1154}
1155
1156#[derive(Clone, Default)]
1157pub struct PluginRegistry {
1158    descriptors: BTreeMap<String, PluginDescriptor>,
1159    active_plugins: BTreeSet<String>,
1160    systems: Vec<RegisteredSystem>,
1161    boundary_systems: Vec<RegisteredBoundarySystem>,
1162    commands: BTreeMap<(String, String), RegisteredCommand>,
1163    ingress: BTreeMap<(String, String), PluginIngressDescriptor>,
1164    state_owners: BTreeMap<StateKey, String>,
1165    immediate_write_states: BTreeMap<StateKey, String>,
1166    boundary_writers: BTreeMap<(BoundaryWriteStage, StateKey), (String, String)>,
1167    reservation_offerers: BTreeMap<StateKey, (String, String)>,
1168    random_stream_owners: BTreeMap<RandomStreamKey, (String, String)>,
1169    record_schemas: records::DomainRecordSchemas,
1170}
1171
1172#[derive(Clone)]
1173struct RegisteredSystem {
1174    plugin: String,
1175    contract: SystemContract,
1176    handler: SimulationSystemHandler,
1177}
1178
1179#[derive(Clone)]
1180struct RegisteredBoundarySystem {
1181    plugin: String,
1182    contract: BoundarySystemContract,
1183    handler: BoundarySystemHandler,
1184}
1185
1186#[derive(Clone)]
1187struct RegisteredCommand {
1188    descriptor: PluginActionDescriptor,
1189    handler: PluginCommandHandler,
1190}
1191
1192pub struct PluginRegistrar<'a> {
1193    plugin: String,
1194    registry: &'a mut PluginRegistry,
1195    schema: &'a mut SchemaRegistry,
1196}
1197
1198#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
1199enum BoundaryWriteStage {
1200    Ordinary,
1201    Transition,
1202    Aggregation,
1203    Perspective,
1204}
1205
1206#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
1207enum DomainRecordCommitStage {
1208    Ordinary,
1209    Transition,
1210    Aggregation,
1211    Perspective,
1212    Deferred,
1213}
1214
1215impl DomainRecordCommitStage {
1216    const ALL: [Self; 5] = [
1217        Self::Ordinary,
1218        Self::Transition,
1219        Self::Aggregation,
1220        Self::Perspective,
1221        Self::Deferred,
1222    ];
1223
1224    const fn ordinal(self) -> u8 {
1225        match self {
1226            Self::Ordinary => 1,
1227            Self::Transition => 2,
1228            Self::Aggregation => 3,
1229            Self::Perspective => 4,
1230            Self::Deferred => 5,
1231        }
1232    }
1233}
1234
1235#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
1236struct DomainHistoryCut {
1237    boundary: usize,
1238    stage: u8,
1239}
1240
1241impl DomainHistoryCut {
1242    const GENESIS: Self = Self {
1243        boundary: 0,
1244        stage: 0,
1245    };
1246
1247    const fn after_boundaries(boundary: usize) -> Self {
1248        Self { boundary, stage: 5 }
1249    }
1250
1251    const fn after_stage(boundary: usize, stage: DomainRecordCommitStage) -> Self {
1252        Self {
1253            boundary,
1254            stage: stage.ordinal(),
1255        }
1256    }
1257}
1258
1259#[derive(Clone, Debug, Default)]
1260struct BoundaryDomainEntityCuts {
1261    changes: BTreeMap<DomainRecordRef, Vec<DomainEntityStageChange>>,
1262}
1263
1264impl BoundaryDomainEntityCuts {
1265    fn record(&mut self, stage: DomainRecordCommitStage, change: &DomainRecordChange) {
1266        let previous_live = change
1267            .previous
1268            .as_ref()
1269            .is_some_and(domain_record_is_live_entity);
1270        let current_live = domain_record_is_live_entity(&change.current);
1271        if previous_live != current_live {
1272            self.changes
1273                .entry(change.current.reference.clone())
1274                .or_default()
1275                .push(DomainEntityStageChange {
1276                    stage,
1277                    plugin: change.plugin.clone(),
1278                    system: change.system.clone(),
1279                    previous_live,
1280                    current_live,
1281                });
1282        }
1283    }
1284
1285    fn is_live(
1286        &self,
1287        final_records: &BTreeMap<DomainRecordRef, DomainRecord>,
1288        reference: &DomainRecordRef,
1289        stage: Option<DomainRecordCommitStage>,
1290    ) -> bool {
1291        let mut live = final_records
1292            .get(reference)
1293            .is_some_and(domain_record_is_live_entity);
1294        if let Some(changes) = self.changes.get(reference) {
1295            for change in changes.iter().rev() {
1296                if stage.is_some_and(|stage| change.stage <= stage) {
1297                    break;
1298                }
1299                live = change.previous_live;
1300            }
1301        }
1302        live
1303    }
1304
1305    fn is_live_for_proposal(
1306        &self,
1307        final_records: &BTreeMap<DomainRecordRef, DomainRecord>,
1308        reference: &DomainRecordRef,
1309        phase: BoundaryPhase,
1310        commit_stage: DomainRecordCommitStage,
1311        plugin: &str,
1312        system: &str,
1313    ) -> bool {
1314        let visible_after = match phase {
1315            BoundaryPhase::DomainDeltaProposal => None,
1316            BoundaryPhase::HistoricalCandidateEvaluation => Some(DomainRecordCommitStage::Ordinary),
1317            BoundaryPhase::StrategicAggregation => Some(DomainRecordCommitStage::Transition),
1318            BoundaryPhase::PerspectiveAndReportMaterialization => {
1319                Some(DomainRecordCommitStage::Aggregation)
1320            }
1321            BoundaryPhase::EventIngress
1322            | BoundaryPhase::BoundarySnapshot
1323            | BoundaryPhase::DerivedFieldSolve
1324            | BoundaryPhase::PerceptionAndAttentionRefresh
1325            | BoundaryPhase::DecisionAndAcceptedEffectIntake
1326            | BoundaryPhase::ReservationAndAllocation
1327            | BoundaryPhase::InvariantValidation
1328            | BoundaryPhase::AtomicDomainCommit
1329            | BoundaryPhase::ConditionalTransitionCommit
1330            | BoundaryPhase::SaveReplayAndDiagnosticHashing => return false,
1331        };
1332        let before_proposal = self.is_live(final_records, reference, visible_after);
1333        self.changes
1334            .get(reference)
1335            .and_then(|changes| {
1336                changes.iter().find(|change| {
1337                    change.stage == commit_stage
1338                        && change.plugin == plugin
1339                        && change.system == system
1340                })
1341            })
1342            .map_or(before_proposal, |change| change.current_live)
1343    }
1344
1345    fn identity_exists_for_proposal(
1346        &self,
1347        final_records: &BTreeMap<DomainRecordRef, DomainRecord>,
1348        reference: &DomainRecordRef,
1349        phase: BoundaryPhase,
1350        commit_stage: DomainRecordCommitStage,
1351        plugin: &str,
1352        system: &str,
1353    ) -> bool {
1354        if !final_records.contains_key(reference) {
1355            return false;
1356        }
1357        let visible_after = match phase {
1358            BoundaryPhase::DomainDeltaProposal => None,
1359            BoundaryPhase::HistoricalCandidateEvaluation => Some(DomainRecordCommitStage::Ordinary),
1360            BoundaryPhase::StrategicAggregation => Some(DomainRecordCommitStage::Transition),
1361            BoundaryPhase::PerspectiveAndReportMaterialization => {
1362                Some(DomainRecordCommitStage::Aggregation)
1363            }
1364            BoundaryPhase::EventIngress
1365            | BoundaryPhase::BoundarySnapshot
1366            | BoundaryPhase::DerivedFieldSolve
1367            | BoundaryPhase::PerceptionAndAttentionRefresh
1368            | BoundaryPhase::DecisionAndAcceptedEffectIntake
1369            | BoundaryPhase::ReservationAndAllocation
1370            | BoundaryPhase::InvariantValidation
1371            | BoundaryPhase::AtomicDomainCommit
1372            | BoundaryPhase::ConditionalTransitionCommit
1373            | BoundaryPhase::SaveReplayAndDiagnosticHashing => return false,
1374        };
1375        self.changes
1376            .get(reference)
1377            .and_then(|changes| {
1378                changes
1379                    .iter()
1380                    .find(|change| !change.previous_live && change.current_live)
1381            })
1382            .is_none_or(|creation| {
1383                visible_after.is_some_and(|stage| creation.stage <= stage)
1384                    || (creation.stage == commit_stage
1385                        && creation.plugin == plugin
1386                        && creation.system == system)
1387            })
1388    }
1389}
1390
1391#[derive(Clone, Debug)]
1392struct DomainEntityStageChange {
1393    stage: DomainRecordCommitStage,
1394    plugin: String,
1395    system: String,
1396    previous_live: bool,
1397    current_live: bool,
1398}
1399
1400#[derive(Clone, Debug)]
1401struct DomainRecordHistory {
1402    lifetimes: BTreeMap<DomainRecordRef, DomainEntityLifetime>,
1403}
1404
1405impl DomainRecordHistory {
1406    fn from_initial_records(records: &BTreeMap<DomainRecordRef, DomainRecord>) -> Self {
1407        let lifetimes = records
1408            .values()
1409            .filter(|record| record.class == DomainRecordClass::Entity)
1410            .map(|record| {
1411                (
1412                    record.reference.clone(),
1413                    DomainEntityLifetime {
1414                        created_at: DomainHistoryCut::GENESIS,
1415                        deleted_at: record.is_deleted().then_some(DomainHistoryCut::GENESIS),
1416                    },
1417                )
1418            })
1419            .collect();
1420        Self { lifetimes }
1421    }
1422
1423    fn apply_boundary(
1424        &mut self,
1425        boundary: usize,
1426        cuts: &BoundaryDomainEntityCuts,
1427    ) -> Result<(), CanwuError> {
1428        for (reference, changes) in &cuts.changes {
1429            for change in changes {
1430                let cut = DomainHistoryCut::after_stage(boundary, change.stage);
1431                match (change.previous_live, change.current_live) {
1432                    (false, true) => {
1433                        if self
1434                            .lifetimes
1435                            .insert(
1436                                reference.clone(),
1437                                DomainEntityLifetime {
1438                                    created_at: cut,
1439                                    deleted_at: None,
1440                                },
1441                            )
1442                            .is_some()
1443                        {
1444                            return invalid_snapshot(
1445                                "domain entity history recreates an existing stable identity",
1446                            );
1447                        }
1448                    }
1449                    (true, false) => {
1450                        let Some(lifetime) = self.lifetimes.get_mut(reference) else {
1451                            return invalid_snapshot(
1452                                "domain entity history deletes an identity before creation",
1453                            );
1454                        };
1455                        if lifetime.deleted_at.replace(cut).is_some() {
1456                            return invalid_snapshot(
1457                                "domain entity history deletes the same identity more than once",
1458                            );
1459                        }
1460                    }
1461                    (false, false) | (true, true) => {}
1462                }
1463            }
1464        }
1465        Ok(())
1466    }
1467
1468    fn is_live(&self, reference: &DomainRecordRef, cut: DomainHistoryCut) -> bool {
1469        self.lifetimes.get(reference).is_some_and(|lifetime| {
1470            lifetime.created_at <= cut && lifetime.deleted_at.is_none_or(|deleted| cut < deleted)
1471        })
1472    }
1473
1474    fn exists(&self, reference: &DomainRecordRef, cut: DomainHistoryCut) -> bool {
1475        self.lifetimes
1476            .get(reference)
1477            .is_some_and(|lifetime| lifetime.created_at <= cut)
1478    }
1479
1480    fn before_time(snapshot: &SimulationSnapshot, at: SimTime) -> DomainHistoryCut {
1481        let count = snapshot
1482            .boundaries
1483            .partition_point(|boundary| boundary.at < at);
1484        DomainHistoryCut::after_boundaries(count)
1485    }
1486}
1487
1488#[derive(Clone, Copy, Debug)]
1489struct DomainEntityLifetime {
1490    created_at: DomainHistoryCut,
1491    deleted_at: Option<DomainHistoryCut>,
1492}
1493
1494fn domain_record_is_live_entity(record: &DomainRecord) -> bool {
1495    record.class == DomainRecordClass::Entity && !record.is_deleted()
1496}
1497
1498const fn boundary_write_stage(phase: BoundaryPhase) -> Option<BoundaryWriteStage> {
1499    match phase {
1500        BoundaryPhase::DomainDeltaProposal => Some(BoundaryWriteStage::Ordinary),
1501        BoundaryPhase::HistoricalCandidateEvaluation => Some(BoundaryWriteStage::Transition),
1502        BoundaryPhase::StrategicAggregation => Some(BoundaryWriteStage::Aggregation),
1503        BoundaryPhase::PerspectiveAndReportMaterialization => Some(BoundaryWriteStage::Perspective),
1504        BoundaryPhase::EventIngress
1505        | BoundaryPhase::BoundarySnapshot
1506        | BoundaryPhase::DerivedFieldSolve
1507        | BoundaryPhase::PerceptionAndAttentionRefresh
1508        | BoundaryPhase::DecisionAndAcceptedEffectIntake
1509        | BoundaryPhase::ReservationAndAllocation
1510        | BoundaryPhase::InvariantValidation
1511        | BoundaryPhase::AtomicDomainCommit
1512        | BoundaryPhase::ConditionalTransitionCommit
1513        | BoundaryPhase::SaveReplayAndDiagnosticHashing => None,
1514    }
1515}
1516
1517const fn domain_record_commit_stage(
1518    phase: BoundaryPhase,
1519    visibility: StateVisibility,
1520) -> Option<DomainRecordCommitStage> {
1521    let stage = match phase {
1522        BoundaryPhase::DomainDeltaProposal => DomainRecordCommitStage::Ordinary,
1523        BoundaryPhase::HistoricalCandidateEvaluation => DomainRecordCommitStage::Transition,
1524        BoundaryPhase::StrategicAggregation => DomainRecordCommitStage::Aggregation,
1525        BoundaryPhase::PerspectiveAndReportMaterialization => DomainRecordCommitStage::Perspective,
1526        BoundaryPhase::EventIngress
1527        | BoundaryPhase::BoundarySnapshot
1528        | BoundaryPhase::DerivedFieldSolve
1529        | BoundaryPhase::PerceptionAndAttentionRefresh
1530        | BoundaryPhase::DecisionAndAcceptedEffectIntake
1531        | BoundaryPhase::ReservationAndAllocation
1532        | BoundaryPhase::InvariantValidation
1533        | BoundaryPhase::AtomicDomainCommit
1534        | BoundaryPhase::ConditionalTransitionCommit
1535        | BoundaryPhase::SaveReplayAndDiagnosticHashing => return None,
1536    };
1537    Some(match visibility {
1538        StateVisibility::SameBoundary => stage,
1539        StateVisibility::NextBoundary => DomainRecordCommitStage::Deferred,
1540    })
1541}
1542
1543fn validate_type_schema(schema: &TypeSchema) -> Result<(), CanwuError> {
1544    if schema.type_name.trim().is_empty() || schema.type_name != schema.type_name.trim() {
1545        return Err(CanwuError::new(
1546            ErrorCode::InvalidPluginRegistration,
1547            "plugin schema type name must be non-empty and have no surrounding whitespace",
1548        ));
1549    }
1550    let mut field_names = BTreeSet::new();
1551    for field in &schema.fields {
1552        if field.name.trim().is_empty()
1553            || field.name != field.name.trim()
1554            || field.value_type.trim().is_empty()
1555            || field.value_type != field.value_type.trim()
1556            || field
1557                .reference_type
1558                .as_ref()
1559                .is_some_and(|value| value.trim().is_empty() || value != value.trim())
1560            || !field_names.insert(&field.name)
1561        {
1562            return Err(CanwuError::new(
1563                ErrorCode::InvalidPluginRegistration,
1564                format!("schema {} contains an invalid field", schema.type_name),
1565            ));
1566        }
1567    }
1568    Ok(())
1569}
1570
1571#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1572struct ScheduleKey {
1573    at: SimTime,
1574    sequence: u64,
1575}
1576
1577#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1578#[serde(tag = "type", rename_all = "snake_case")]
1579enum ScheduledAction {
1580    ArmyArrival {
1581        army: ArmyId,
1582        destination: TerritoryId,
1583        order_event: EventId,
1584        correlation_id: u64,
1585    },
1586    KnowledgeReport {
1587        recipient: PersonId,
1588        army: ArmyId,
1589        location: TerritoryId,
1590        observed_at: SimTime,
1591        dispatch_event: EventId,
1592        correlation_id: u64,
1593    },
1594    PluginDirective {
1595        plugin: String,
1596        directive: Box<SystemDirective>,
1597        allowed_writes: Vec<StateKey>,
1598        cause: CauseRef,
1599        correlation_id: u64,
1600    },
1601}
1602
1603#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1604struct ScheduledRecord {
1605    key: ScheduleKey,
1606    action: ScheduledAction,
1607}
1608
1609const fn one_u64() -> u64 {
1610    1
1611}
1612
1613#[allow(clippy::trivially_copy_pass_by_ref)]
1614const fn is_zero_u64(value: &u64) -> bool {
1615    *value == 0
1616}
1617
1618#[allow(clippy::trivially_copy_pass_by_ref)]
1619const fn is_zero_u32(value: &u32) -> bool {
1620    *value == 0
1621}
1622
1623#[allow(clippy::trivially_copy_pass_by_ref)]
1624const fn is_one_u64(value: &u64) -> bool {
1625    *value == 1
1626}
1627
1628fn command_attempt_slice_is_empty(value: &&[CommandAttemptRecord]) -> bool {
1629    value.is_empty()
1630}
1631
1632fn command_attempt_id_slice_is_empty(value: &&[CommandAttemptId]) -> bool {
1633    value.is_empty()
1634}
1635
1636fn domain_record_slice_is_empty(value: &&[DomainRecord]) -> bool {
1637    value.is_empty()
1638}
1639
1640fn domain_record_change_slice_is_empty(value: &&[DomainRecordChange]) -> bool {
1641    value.is_empty()
1642}
1643
1644fn ingress_record_slice_is_empty(value: &&[IngressRecord]) -> bool {
1645    value.is_empty()
1646}
1647
1648const BOUNDARY_STATE_HASH_V1_PREFIX: &str = "v1:";
1649
1650#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1651enum BoundaryStateHashFormat {
1652    LegacyV0,
1653    CommitmentsV1,
1654}
1655
1656fn boundary_state_hash_format(value: Option<&str>) -> Result<BoundaryStateHashFormat, CanwuError> {
1657    match value {
1658        Some(value) if value.starts_with(BOUNDARY_STATE_HASH_V1_PREFIX) => {
1659            let hash = &value[BOUNDARY_STATE_HASH_V1_PREFIX.len()..];
1660            if !is_canonical_hash(hash) {
1661                return invalid_snapshot("boundary state commitment v1 is not canonical");
1662            }
1663            Ok(BoundaryStateHashFormat::CommitmentsV1)
1664        }
1665        Some(value) if is_canonical_hash(value) => Ok(BoundaryStateHashFormat::LegacyV0),
1666        Some(_) => invalid_snapshot("boundary state commitment format is unsupported"),
1667        None => Ok(BoundaryStateHashFormat::LegacyV0),
1668    }
1669}
1670
1671#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1672pub struct SimulationSnapshot {
1673    pub engine_version: String,
1674    pub snapshot_format_version: u32,
1675    #[serde(default)]
1676    pub run_manifest: Option<RunManifest>,
1677    #[serde(default)]
1678    pub run_manifest_hash: String,
1679    #[serde(default, skip_serializing_if = "Option::is_none")]
1680    pub run_configuration: Option<RunConfigurationSnapshot>,
1681    #[serde(default)]
1682    pub checkpoint_hash: String,
1683    #[serde(default, skip_serializing_if = "is_zero_u32")]
1684    /// Version of the domain-separated checkpoint commitment contract.
1685    pub commitment_format_version: u32,
1686    #[serde(default, skip_serializing_if = "Option::is_none")]
1687    /// Persisted canonical roots verified before a snapshot becomes live.
1688    pub commitment_roots: Option<CommitmentRoots>,
1689    #[serde(default)]
1690    /// Version of the revision migration and checkpoint sub-contract.
1691    pub revision_format_version: u32,
1692    #[serde(default, skip_serializing_if = "is_zero_u64")]
1693    /// Monotonic revision after all persisted attempt and boundary transactions.
1694    pub state_revision: u64,
1695    #[serde(default, skip_serializing_if = "is_zero_u32")]
1696    /// Revision-evidence format available to exact replay; zero is migration-only.
1697    pub replay_revision_format_version: u32,
1698    #[serde(default, skip_serializing_if = "is_zero_u32")]
1699    /// Version of the persisted boundary-admission cursor contract.
1700    pub admission_cursor_format_version: u32,
1701    #[serde(default, skip_serializing_if = "is_zero_u64")]
1702    /// Number of command-attempt records consumed by completed boundaries.
1703    pub admitted_attempt_count: u64,
1704    #[serde(default, skip_serializing_if = "is_zero_u64")]
1705    /// Number of accepted-command records consumed by completed boundaries.
1706    pub admitted_command_count: u64,
1707    #[serde(default, skip_serializing_if = "is_zero_u64")]
1708    /// Number of event records consumed as boundary ingress.
1709    pub admitted_event_count: u64,
1710    pub initial_time: SimTime,
1711    #[serde(default, skip_serializing_if = "Option::is_none")]
1712    pub initial_scenario: Option<Scenario>,
1713    pub now: SimTime,
1714    pub plugin_registration_closed: bool,
1715    pub world: WorldSnapshot,
1716    pub knowledge: KnowledgeSnapshot,
1717    pub events: Vec<SimEvent>,
1718    pub commands: Vec<CommandRecord>,
1719    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1720    pub command_attempts: Vec<CommandAttemptRecord>,
1721    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1722    pub ingress: Vec<IngressRecord>,
1723    #[serde(default)]
1724    pub boundaries: Vec<BoundaryRecord>,
1725    pub plugin_components: Vec<PluginComponentRecord>,
1726    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1727    pub domain_records: Vec<DomainRecord>,
1728    pub plugin_descriptors: Vec<PluginDescriptor>,
1729    pub schema: SchemaRegistry,
1730    #[serde(default)]
1731    pub root_seed: u64,
1732    #[serde(default)]
1733    pub random_streams: Vec<RandomStreamState>,
1734    #[serde(default)]
1735    pub random_draws: Vec<RandomDrawRecord>,
1736    scheduled: Vec<ScheduledRecord>,
1737    #[serde(default, rename = "rng", skip_serializing_if = "Option::is_none")]
1738    legacy_rng: Option<DeterministicRng>,
1739    next_event_id: u64,
1740    next_command_id: u64,
1741    #[serde(default = "one_u64", skip_serializing_if = "is_one_u64")]
1742    next_command_attempt_id: u64,
1743    #[serde(default = "one_u64", skip_serializing_if = "is_one_u64")]
1744    next_ingress_id: u64,
1745    #[serde(default)]
1746    next_boundary_id: u64,
1747    #[serde(default)]
1748    next_random_draw_id: u64,
1749    next_schedule_sequence: u64,
1750    next_correlation_id: u64,
1751}
1752
1753#[derive(Clone, Debug, PartialEq, Serialize)]
1754/// Complete recorded environment and input journal for exact replay.
1755pub struct ReplayJournal {
1756    pub engine_version: String,
1757    pub snapshot_format_version: u32,
1758    pub root_seed: u64,
1759    pub run_manifest: RunManifest,
1760    pub run_manifest_hash: String,
1761    pub run_configuration: RunConfigurationSnapshot,
1762    pub plugin_descriptors: Vec<PluginDescriptor>,
1763    pub plugin_registration_closed: bool,
1764    pub commands: Vec<CommandRecord>,
1765    pub command_attempts: Vec<CommandAttemptRecord>,
1766    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1767    pub ingress: Vec<IngressRecord>,
1768    pub boundaries: Vec<BoundaryRecord>,
1769    pub final_time: SimTime,
1770    pub checkpoint_hash: String,
1771    /// Checkpoint commitment format reproduced by exact replay.
1772    pub commitment_format_version: u32,
1773    /// Revision-evidence format verified by this exact replay journal.
1774    pub revision_format_version: u32,
1775    /// Final persisted authoritative revision after replay.
1776    pub final_revision: u64,
1777}
1778
1779#[derive(Deserialize)]
1780struct ReplayJournalWire {
1781    engine_version: String,
1782    snapshot_format_version: u32,
1783    root_seed: u64,
1784    run_manifest: RunManifest,
1785    run_manifest_hash: String,
1786    #[serde(default)]
1787    run_configuration: Option<RunConfigurationSnapshot>,
1788    plugin_descriptors: Vec<PluginDescriptor>,
1789    plugin_registration_closed: bool,
1790    commands: Vec<CommandRecord>,
1791    #[serde(default)]
1792    command_attempts: Vec<CommandAttemptRecord>,
1793    #[serde(default)]
1794    ingress: Vec<IngressRecord>,
1795    boundaries: Vec<BoundaryRecord>,
1796    final_time: SimTime,
1797    checkpoint_hash: String,
1798    #[serde(default)]
1799    commitment_format_version: u32,
1800    #[serde(default)]
1801    revision_format_version: u32,
1802    #[serde(default)]
1803    final_revision: u64,
1804}
1805
1806impl<'de> Deserialize<'de> for ReplayJournal {
1807    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1808    where
1809        D: serde::Deserializer<'de>,
1810    {
1811        let wire = ReplayJournalWire::deserialize(deserializer)?;
1812        let run_configuration = wire
1813            .run_configuration
1814            .map_or_else(|| inferred_run_configuration(&wire.run_manifest), Ok)
1815            .map_err(serde::de::Error::custom)?;
1816        Ok(Self {
1817            engine_version: wire.engine_version,
1818            snapshot_format_version: wire.snapshot_format_version,
1819            root_seed: wire.root_seed,
1820            run_manifest: wire.run_manifest,
1821            run_manifest_hash: wire.run_manifest_hash,
1822            run_configuration,
1823            plugin_descriptors: wire.plugin_descriptors,
1824            plugin_registration_closed: wire.plugin_registration_closed,
1825            commands: wire.commands,
1826            command_attempts: wire.command_attempts,
1827            ingress: wire.ingress,
1828            boundaries: wire.boundaries,
1829            final_time: wire.final_time,
1830            checkpoint_hash: wire.checkpoint_hash,
1831            commitment_format_version: wire.commitment_format_version,
1832            revision_format_version: wire.revision_format_version,
1833            final_revision: wire.final_revision,
1834        })
1835    }
1836}
1837
1838pub struct Simulation {
1839    state: RuntimeState,
1840    schema: SchemaRegistry,
1841    plugins: PluginRegistry,
1842    sync_reaction_depth: usize,
1843}
1844
1845impl Simulation {
1846    /// Creates a simulation after validating that scenario references are sound.
1847    pub fn new(seed: u64, scenario: Scenario) -> Result<Self, CanwuError> {
1848        require_plugin_aware_initial_records(&scenario)?;
1849        let run_manifest = RunManifest::for_scenario("canwu.inline", "scenario", "1", &scenario)?;
1850        Self::new_with_configuration_snapshot(
1851            seed,
1852            scenario,
1853            run_manifest,
1854            RunConfigurationSnapshot::CompatibilityV1,
1855        )
1856    }
1857
1858    /// Creates a simulation and activates the plugins required by initial
1859    /// application-defined records before returning a snapshot-capable runtime.
1860    pub fn new_with_plugins(
1861        seed: u64,
1862        scenario: Scenario,
1863        plugins: &[&dyn SimulationPlugin],
1864    ) -> Result<Self, CanwuError> {
1865        let run_manifest = RunManifest::for_scenario("canwu.inline", "scenario", "1", &scenario)?;
1866        Self::new_with_manifest_and_plugins(seed, scenario, run_manifest, plugins)
1867    }
1868
1869    /// Creates a simulation with an exact, persisted run environment identity.
1870    pub fn new_with_manifest(
1871        seed: u64,
1872        scenario: Scenario,
1873        run_manifest: RunManifest,
1874    ) -> Result<Self, CanwuError> {
1875        require_plugin_aware_initial_records(&scenario)?;
1876        Self::new_with_configuration_snapshot(
1877            seed,
1878            scenario,
1879            run_manifest,
1880            RunConfigurationSnapshot::CompatibilityV1,
1881        )
1882    }
1883
1884    /// Creates a manifested run and activates all initial domain packages before
1885    /// the runtime can be observed or snapshotted.
1886    pub fn new_with_manifest_and_plugins(
1887        seed: u64,
1888        scenario: Scenario,
1889        run_manifest: RunManifest,
1890        plugins: &[&dyn SimulationPlugin],
1891    ) -> Result<Self, CanwuError> {
1892        let simulation = Self::new_with_configuration_snapshot(
1893            seed,
1894            scenario,
1895            run_manifest,
1896            RunConfigurationSnapshot::CompatibilityV1,
1897        )?;
1898        Self::activate_initial_plugins(simulation, plugins)
1899    }
1900
1901    /// Creates a run whose six policy dimensions are persisted and bound to
1902    /// the run-configuration artifact in `run_manifest`.
1903    pub fn new_with_run_configuration(
1904        seed: u64,
1905        scenario: Scenario,
1906        run_manifest: RunManifest,
1907        mut run_configuration: RunConfiguration,
1908    ) -> Result<Self, CanwuError> {
1909        require_plugin_aware_initial_records(&scenario)?;
1910        run_configuration.canonicalize();
1911        Self::new_with_configuration_snapshot(
1912            seed,
1913            scenario,
1914            run_manifest,
1915            RunConfigurationSnapshot::Declared(run_configuration),
1916        )
1917    }
1918
1919    /// Creates a declared-policy run and activates all initial domain packages
1920    /// before the runtime can be observed or snapshotted.
1921    pub fn new_with_run_configuration_and_plugins(
1922        seed: u64,
1923        scenario: Scenario,
1924        run_manifest: RunManifest,
1925        mut run_configuration: RunConfiguration,
1926        plugins: &[&dyn SimulationPlugin],
1927    ) -> Result<Self, CanwuError> {
1928        run_configuration.canonicalize();
1929        let simulation = Self::new_with_configuration_snapshot(
1930            seed,
1931            scenario,
1932            run_manifest,
1933            RunConfigurationSnapshot::Declared(run_configuration),
1934        )?;
1935        Self::activate_initial_plugins(simulation, plugins)
1936    }
1937
1938    fn activate_initial_plugins(
1939        mut simulation: Self,
1940        plugins: &[&dyn SimulationPlugin],
1941    ) -> Result<Self, CanwuError> {
1942        for plugin in plugins {
1943            simulation.register_plugin(*plugin)?;
1944        }
1945        simulation.ensure_runtime_ready()?;
1946        Ok(simulation)
1947    }
1948
1949    fn new_with_configuration_snapshot(
1950        seed: u64,
1951        mut scenario: Scenario,
1952        mut run_manifest: RunManifest,
1953        run_configuration: RunConfigurationSnapshot,
1954    ) -> Result<Self, CanwuError> {
1955        canonicalize_scenario(&mut scenario);
1956        validate_scenario(&scenario)?;
1957        manifest::canonicalize(&mut run_manifest);
1958        manifest::validate(&run_manifest, Some(&scenario), false)?;
1959        manifest::validate_run_configuration(&run_manifest, &run_configuration)?;
1960        validate_run_configuration_entities(
1961            &run_configuration,
1962            &scenario.world,
1963            &scenario.domain_records,
1964        )?;
1965        let run_manifest_hash = manifest::hash(&run_manifest)?;
1966        if scenario
1967            .world
1968            .armies
1969            .iter()
1970            .any(|army| army.transit.is_some())
1971        {
1972            return Err(CanwuError::new(
1973                ErrorCode::InvalidSnapshot,
1974                "initial scenarios cannot contain transit without admitted command/event/queue evidence",
1975            ));
1976        }
1977        let schema = base_schema();
1978        let plugins = PluginRegistry::default();
1979        let core_stream = RandomStreamState::initial(seed, random::core_report_delay_stream());
1980        let initial_scenario = Some(scenario.clone());
1981        let mut simulation = Self {
1982            state: RuntimeState {
1983                current: RuntimeCurrentState {
1984                    people: scenario
1985                        .world
1986                        .people
1987                        .into_iter()
1988                        .map(|value| (value.id, value))
1989                        .collect(),
1990                    governments: scenario
1991                        .world
1992                        .governments
1993                        .into_iter()
1994                        .map(|value| (value.id, value))
1995                        .collect(),
1996                    territories: scenario
1997                        .world
1998                        .territories
1999                        .into_iter()
2000                        .map(|value| (value.id, value))
2001                        .collect(),
2002                    routes: scenario
2003                        .world
2004                        .routes
2005                        .into_iter()
2006                        .map(|value| (value.id, value))
2007                        .collect(),
2008                    armies: scenario
2009                        .world
2010                        .armies
2011                        .into_iter()
2012                        .map(|value| (value.id, value))
2013                        .collect(),
2014                    knowledge: scenario.knowledge,
2015                    plugin_components: BTreeMap::new(),
2016                    domain_records: scenario
2017                        .domain_records
2018                        .into_iter()
2019                        .map(|record| (record.reference.clone(), record))
2020                        .collect(),
2021                    root_seed: seed,
2022                    random_streams: BTreeMap::from([(core_stream.key.clone(), core_stream)]),
2023                },
2024                scheduler: RuntimeScheduler {
2025                    initial_time: scenario.start_time,
2026                    now: scenario.start_time,
2027                    actions: BTreeMap::new(),
2028                    pending_ingress: BTreeSet::new(),
2029                },
2030                counters: RuntimeCounters {
2031                    next_event_id: 1,
2032                    next_command_id: 1,
2033                    next_command_attempt_id: 1,
2034                    next_ingress_id: 1,
2035                    next_boundary_id: 1,
2036                    next_random_draw_id: 1,
2037                    next_schedule_sequence: 1,
2038                    next_correlation_id: 1,
2039                    state_revision: 0,
2040                    admitted_attempt_count: 0,
2041                    admitted_command_count: 0,
2042                    admitted_event_count: 0,
2043                },
2044                metadata: RuntimeMetadata {
2045                    initial_scenario,
2046                    run_manifest,
2047                    run_manifest_hash,
2048                    run_configuration,
2049                    checkpoint_hash: String::new(),
2050                    commitment_format_version: COMMITMENT_FORMAT_VERSION,
2051                    commitment_roots: None,
2052                    commitment_cache: None,
2053                    plugin_registration_closed: false,
2054                    replay_revision_format_version: STATE_REVISION_FORMAT_VERSION,
2055                },
2056                evidence: RuntimeEvidence {
2057                    archived: EvidenceCursor::default(),
2058                    archived_boundary_head: None,
2059                    archived_legacy_commands: false,
2060                    archived_tracked_attempts: false,
2061                    archived_unqueued_command_history: false,
2062                    archived_command_requests: BTreeMap::new(),
2063                    archived_ingress_requests: BTreeMap::new(),
2064                    events: Vec::new(),
2065                    commands: Vec::new(),
2066                    command_attempts: Vec::new(),
2067                    ingress: Vec::new(),
2068                    boundaries: Vec::new(),
2069                    random_draws: Vec::new(),
2070                },
2071            },
2072            schema,
2073            plugins,
2074            sync_reaction_depth: 0,
2075        };
2076        simulation.refresh_checkpoint_hash()?;
2077        Ok(simulation)
2078    }
2079
2080    pub fn demo(seed: u64) -> Result<(Self, DemoIds), CanwuError> {
2081        let (scenario, ids) = demo_scenario();
2082        Self::new(seed, scenario).map(|simulation| (simulation, ids))
2083    }
2084
2085    pub fn register_plugin<P: SimulationPlugin + ?Sized>(
2086        &mut self,
2087        plugin: &P,
2088    ) -> Result<(), CanwuError> {
2089        let plugin_name = plugin.name().trim();
2090        if plugin_name.is_empty() || plugin_name != plugin.name() {
2091            return Err(CanwuError::new(
2092                ErrorCode::InvalidPluginRegistration,
2093                "plugin name must be non-empty and have no surrounding whitespace",
2094            ));
2095        }
2096        let rehydrating = self.plugins.descriptors.contains_key(plugin_name)
2097            && !self.plugins.active_plugins.contains(plugin_name);
2098        if self.state.metadata.plugin_registration_closed && !rehydrating {
2099            return Err(CanwuError::new(
2100                ErrorCode::PluginRegistrationClosed,
2101                "new plugins must be registered before authoritative execution begins",
2102            ));
2103        }
2104        let state_start = self.state.clone();
2105        let schema_start = self.schema.clone();
2106        let plugins_start = self.plugins.clone();
2107        let result = (|| {
2108            self.plugins.register(plugin, &mut self.schema)?;
2109            self.invalidate_commitments(
2110                CommitmentDomains::RANDOM_STREAMS | CommitmentDomains::IDENTITY,
2111            );
2112            if !self.plugins.record_schemas.is_empty()
2113                && self.state.metadata.initial_scenario.is_none()
2114            {
2115                return Err(CanwuError::new(
2116                    ErrorCode::UnsupportedSnapshotVersion,
2117                    "this snapshot predates manifest-bound domain-record genesis and cannot activate record schemas",
2118                ));
2119            }
2120            records::validate_records_for_owner(
2121                &self.state.current.domain_records,
2122                &self.plugins.record_schemas,
2123                plugin_name,
2124                self.state.scheduler.now,
2125                &|entity| runtime_entity_exists(&self.state, entity),
2126            )?;
2127            for stream in self.plugins.random_stream_owners.keys() {
2128                self.state
2129                    .current
2130                    .random_streams
2131                    .entry(stream.clone())
2132                    .or_insert_with(|| {
2133                        RandomStreamState::initial(self.state.current.root_seed, stream.clone())
2134                    });
2135            }
2136            self.refresh_checkpoint_hash()
2137        })();
2138        if let Err(error) = result {
2139            self.state = state_start;
2140            self.schema = schema_start;
2141            self.plugins = plugins_start;
2142            return Err(error);
2143        }
2144        Ok(())
2145    }
2146
2147    fn ensure_runtime_ready(&self) -> Result<(), CanwuError> {
2148        self.plugins.ensure_active()?;
2149        records::validate_record_store(
2150            &self.state.current.domain_records,
2151            &self.plugins.record_schemas,
2152            self.state.scheduler.now,
2153            &|entity| runtime_entity_exists(&self.state, entity),
2154        )
2155    }
2156
2157    fn domain_record_feature_enabled(&self) -> bool {
2158        !self.plugins.record_schemas.is_empty()
2159            || !self.state.current.domain_records.is_empty()
2160            || self
2161                .state
2162                .evidence
2163                .boundaries
2164                .iter()
2165                .any(|boundary| !boundary.record_changes.is_empty())
2166    }
2167
2168    fn bound_initial_scenario(&self) -> Option<&Scenario> {
2169        if self.domain_record_feature_enabled() {
2170            self.state.metadata.initial_scenario.as_ref()
2171        } else {
2172            None
2173        }
2174    }
2175
2176    #[must_use]
2177    pub const fn time(&self) -> SimTime {
2178        self.state.scheduler.now
2179    }
2180
2181    #[must_use]
2182    pub const fn run_manifest(&self) -> &RunManifest {
2183        &self.state.metadata.run_manifest
2184    }
2185
2186    #[must_use]
2187    pub const fn run_configuration(&self) -> &RunConfigurationSnapshot {
2188        &self.state.metadata.run_configuration
2189    }
2190
2191    #[must_use]
2192    /// Returns the persisted authoritative transaction revision.
2193    ///
2194    /// Accepted commands, persisted expected rejections, and completed
2195    /// settlement boundaries each advance it exactly once. Failed work, exact
2196    /// retries, bare clock movement, queued but unadmitted ingress, and plugin
2197    /// setup do not advance it; use the expected-time guard with external
2198    /// commands to detect clock and scheduled-work advancement.
2199    pub const fn revision(&self) -> u64 {
2200        self.state.counters.state_revision
2201    }
2202
2203    #[must_use]
2204    pub fn run_manifest_hash(&self) -> &str {
2205        &self.state.metadata.run_manifest_hash
2206    }
2207
2208    #[must_use]
2209    pub fn checkpoint_hash(&self) -> &str {
2210        &self.state.metadata.checkpoint_hash
2211    }
2212
2213    /// Hash of simulated state and causal evidence. Run-purpose, controller,
2214    /// seat, observation, interaction, and trace policy remain save identity
2215    /// but are deliberately excluded from this authoritative result identity.
2216    pub fn authoritative_state_hash(&self) -> Result<String, CanwuError> {
2217        self.compute_boundary_state_hash()
2218    }
2219
2220    #[must_use]
2221    pub fn world(&self) -> WorldSnapshot {
2222        WorldSnapshot {
2223            people: self.state.current.people.values().cloned().collect(),
2224            governments: self.state.current.governments.values().cloned().collect(),
2225            territories: self.state.current.territories.values().cloned().collect(),
2226            routes: self.state.current.routes.values().cloned().collect(),
2227            armies: self.state.current.armies.values().cloned().collect(),
2228        }
2229    }
2230
2231    #[must_use]
2232    pub fn knowledge(&self) -> &KnowledgeSnapshot {
2233        &self.state.current.knowledge
2234    }
2235
2236    #[must_use]
2237    pub fn events(&self) -> &[SimEvent] {
2238        &self.state.evidence.events
2239    }
2240
2241    #[must_use]
2242    pub fn command_log(&self) -> &[CommandRecord] {
2243        &self.state.evidence.commands
2244    }
2245
2246    #[must_use]
2247    pub fn command_attempts(&self) -> &[CommandAttemptRecord] {
2248        &self.state.evidence.command_attempts
2249    }
2250
2251    #[must_use]
2252    pub fn ingress_log(&self) -> &[IngressRecord] {
2253        &self.state.evidence.ingress
2254    }
2255
2256    #[must_use]
2257    pub fn domain_record(&self, reference: &DomainRecordRef) -> Option<&DomainRecord> {
2258        self.state.current.domain_records.get(reference)
2259    }
2260
2261    #[must_use]
2262    pub fn typed_domain_record<T: DomainRecordType>(
2263        &self,
2264        reference: &TypedDomainRecordRef<T>,
2265    ) -> Option<&DomainRecord> {
2266        self.domain_record(reference.as_untyped())
2267    }
2268
2269    pub fn domain_records(&self) -> impl Iterator<Item = &DomainRecord> {
2270        self.state.current.domain_records.values()
2271    }
2272
2273    #[must_use]
2274    pub fn boundaries(&self) -> &[BoundaryRecord] {
2275        &self.state.evidence.boundaries
2276    }
2277
2278    #[must_use]
2279    pub fn random_draws(&self) -> &[RandomDrawRecord] {
2280        &self.state.evidence.random_draws
2281    }
2282
2283    #[must_use]
2284    pub fn boundary_head_hash(&self) -> Option<&str> {
2285        self.state.evidence.boundary_head_hash()
2286    }
2287
2288    #[must_use]
2289    pub const fn schema(&self) -> &SchemaRegistry {
2290        &self.schema
2291    }
2292
2293    pub fn plugin_descriptors(&self) -> impl Iterator<Item = &PluginDescriptor> {
2294        self.plugins.descriptors()
2295    }
2296
2297    /// Returns the persisted audience declaration for a plugin event.
2298    ///
2299    /// Built-in event visibility remains part of the public actor-relative
2300    /// projection. Unlisted plugin event types deliberately resolve to
2301    /// [`EventAudience::Private`].
2302    #[must_use]
2303    pub fn event_audience(&self, event: &SimEvent) -> EventAudience {
2304        match &event.kind {
2305            EventKind::Plugin { plugin, event_type } => {
2306                self.plugins.event_audience(plugin, event_type)
2307            }
2308            EventKind::MoveOrdered { .. }
2309            | EventKind::ArmyArrived { .. }
2310            | EventKind::ReportDispatched { .. }
2311            | EventKind::KnowledgeUpdated { .. }
2312            | EventKind::DebugFieldChanged { .. } => EventAudience::Private,
2313        }
2314    }
2315
2316    #[must_use]
2317    pub fn replay_journal(&self) -> ReplayJournal {
2318        ReplayJournal {
2319            engine_version: ENGINE_VERSION.to_owned(),
2320            snapshot_format_version: SNAPSHOT_FORMAT_VERSION,
2321            root_seed: self.state.current.root_seed,
2322            run_manifest: self.state.metadata.run_manifest.clone(),
2323            run_manifest_hash: self.state.metadata.run_manifest_hash.clone(),
2324            run_configuration: self.state.metadata.run_configuration.clone(),
2325            plugin_descriptors: self.plugins.descriptors().cloned().collect(),
2326            plugin_registration_closed: self.state.metadata.plugin_registration_closed,
2327            commands: self.state.evidence.commands.clone(),
2328            command_attempts: self.state.evidence.command_attempts.clone(),
2329            ingress: self.state.evidence.ingress.clone(),
2330            boundaries: self.state.evidence.boundaries.clone(),
2331            final_time: self.state.scheduler.now,
2332            checkpoint_hash: self.state.metadata.checkpoint_hash.clone(),
2333            commitment_format_version: self.state.metadata.commitment_format_version,
2334            revision_format_version: self.state.metadata.replay_revision_format_version,
2335            final_revision: self.state.counters.state_revision,
2336        }
2337    }
2338
2339    fn compute_boundary_state_hash_for(
2340        &mut self,
2341        format: BoundaryStateHashFormat,
2342    ) -> Result<String, CanwuError> {
2343        match format {
2344            BoundaryStateHashFormat::LegacyV0 => self.compute_boundary_state_hash(),
2345            BoundaryStateHashFormat::CommitmentsV1 => {
2346                let roots = self.refresh_runtime_commitment_roots()?;
2347                boundary_state_hash_for_commitments(&roots)
2348            }
2349        }
2350    }
2351
2352    fn compute_boundary_state_hash(&self) -> Result<String, CanwuError> {
2353        let world = self.world();
2354        let plugin_components: Vec<_> = self
2355            .state
2356            .current
2357            .plugin_components
2358            .values()
2359            .cloned()
2360            .collect();
2361        let domain_records: Vec<_> = self
2362            .state
2363            .current
2364            .domain_records
2365            .values()
2366            .cloned()
2367            .collect();
2368        let plugin_descriptors: Vec<_> = self.plugins.descriptors().cloned().collect();
2369        let scheduled: Vec<_> = self
2370            .state
2371            .scheduler
2372            .actions
2373            .iter()
2374            .map(|(key, action)| ScheduledRecord {
2375                key: key.clone(),
2376                action: action.clone(),
2377            })
2378            .collect();
2379        let random_streams: Vec<_> = self
2380            .state
2381            .current
2382            .random_streams
2383            .values()
2384            .cloned()
2385            .collect();
2386        let (authoritative_manifest, authoritative_manifest_hash) = authoritative_run_identity(
2387            &self.state.metadata.run_manifest,
2388            &self.state.metadata.run_manifest_hash,
2389            &self.state.metadata.run_configuration,
2390        )?;
2391        let initial_scenario = self.bound_initial_scenario();
2392        state_hash(&StateHashMaterial {
2393            engine_version: ENGINE_VERSION,
2394            snapshot_format_version: SNAPSHOT_FORMAT_VERSION,
2395            run_manifest: &authoritative_manifest,
2396            run_manifest_hash: &authoritative_manifest_hash,
2397            initial_time: self.state.scheduler.initial_time,
2398            initial_scenario,
2399            now: self.state.scheduler.now,
2400            plugin_registration_closed: self.state.metadata.plugin_registration_closed,
2401            world: &world,
2402            knowledge: &self.state.current.knowledge,
2403            events: &self.state.evidence.events,
2404            commands: &self.state.evidence.commands,
2405            command_attempts: &self.state.evidence.command_attempts,
2406            ingress: &self.state.evidence.ingress,
2407            plugin_components: &plugin_components,
2408            domain_records: &domain_records,
2409            plugin_descriptors: &plugin_descriptors,
2410            schema: &self.schema,
2411            scheduled: &scheduled,
2412            root_seed: self.state.current.root_seed,
2413            random_streams: &random_streams,
2414            random_draws: &self.state.evidence.random_draws,
2415            next_event_id: self.state.counters.next_event_id,
2416            next_command_id: self.state.counters.next_command_id,
2417            next_command_attempt_id: self.state.counters.next_command_attempt_id,
2418            next_ingress_id: self.state.counters.next_ingress_id,
2419            next_boundary_id: self.state.counters.next_boundary_id,
2420            next_random_draw_id: self.state.counters.next_random_draw_id,
2421            next_schedule_sequence: self.state.counters.next_schedule_sequence,
2422            next_correlation_id: self.state.counters.next_correlation_id,
2423        })
2424    }
2425
2426    fn compute_commitment_root_updates(
2427        &self,
2428        needs: CommitmentDomains,
2429    ) -> Result<RuntimeCommitmentRootUpdates, CanwuError> {
2430        let world = needs
2431            .contains(CommitmentDomains::WORLD)
2432            .then(|| world_commitment_root(&self.world()))
2433            .transpose()?;
2434        let knowledge = needs
2435            .contains(CommitmentDomains::KNOWLEDGE)
2436            .then(|| knowledge_commitment_root(&self.state.current.knowledge))
2437            .transpose()?;
2438        let plugin_components = needs
2439            .contains(CommitmentDomains::PLUGIN_COMPONENTS)
2440            .then(|| {
2441                let values: Vec<_> = self
2442                    .state
2443                    .current
2444                    .plugin_components
2445                    .values()
2446                    .cloned()
2447                    .collect();
2448                plugin_component_commitment_root(&values)
2449            })
2450            .transpose()?;
2451        let domain_records = needs
2452            .contains(CommitmentDomains::DOMAIN_RECORDS)
2453            .then(|| {
2454                let values: Vec<_> = self
2455                    .state
2456                    .current
2457                    .domain_records
2458                    .values()
2459                    .cloned()
2460                    .collect();
2461                domain_record_commitment_root(&values)
2462            })
2463            .transpose()?;
2464        let scheduler = needs
2465            .contains(CommitmentDomains::SCHEDULER)
2466            .then(|| {
2467                let scheduled: Vec<_> = self
2468                    .state
2469                    .scheduler
2470                    .actions
2471                    .iter()
2472                    .map(|(key, action)| ScheduledRecord {
2473                        key: key.clone(),
2474                        action: action.clone(),
2475                    })
2476                    .collect();
2477                scheduler_commitment_root(self.state.scheduler.now, &scheduled)
2478            })
2479            .transpose()?;
2480        let random_streams = needs
2481            .contains(CommitmentDomains::RANDOM_STREAMS)
2482            .then(|| {
2483                let values: Vec<_> = self
2484                    .state
2485                    .current
2486                    .random_streams
2487                    .values()
2488                    .cloned()
2489                    .collect();
2490                random_stream_commitment_root(&values)
2491            })
2492            .transpose()?;
2493        let identity = if needs.contains(CommitmentDomains::IDENTITY) {
2494            let descriptors: Vec<_> = self.plugins.descriptors().cloned().collect();
2495            let (manifest, manifest_hash) = authoritative_run_identity(
2496                &self.state.metadata.run_manifest,
2497                &self.state.metadata.run_manifest_hash,
2498                &self.state.metadata.run_configuration,
2499            )?;
2500            Some(identity_commitment_root(
2501                ENGINE_VERSION,
2502                SNAPSHOT_FORMAT_VERSION,
2503                &manifest,
2504                &manifest_hash,
2505                self.state.scheduler.initial_time,
2506                self.bound_initial_scenario(),
2507                &descriptors,
2508                &self.schema,
2509            )?)
2510        } else {
2511            None
2512        };
2513        Ok(RuntimeCommitmentRootUpdates {
2514            world,
2515            knowledge,
2516            plugin_components,
2517            domain_records,
2518            scheduler,
2519            random_streams,
2520            identity,
2521        })
2522    }
2523
2524    fn invalidate_commitments(&mut self, domains: CommitmentDomains) {
2525        if let Some(cache) = self.state.metadata.commitment_cache.as_mut() {
2526            cache.invalidate(domains);
2527        }
2528    }
2529
2530    fn refresh_runtime_commitment_roots(&mut self) -> Result<CommitmentRoots, CanwuError> {
2531        if self.state.metadata.commitment_format_version != COMMITMENT_FORMAT_VERSION {
2532            return Err(CanwuError::new(
2533                ErrorCode::UnsupportedSnapshotVersion,
2534                format!(
2535                    "commitment format {} cannot produce boundary state commitment v1",
2536                    self.state.metadata.commitment_format_version
2537                ),
2538            ));
2539        }
2540        let needs = {
2541            let cache = if let Some(cache) = self.state.metadata.commitment_cache.as_mut() {
2542                cache
2543            } else {
2544                self.state.metadata.commitment_cache =
2545                    Some(RuntimeCommitmentCache::from_evidence(&self.state.evidence)?);
2546                self.state
2547                    .metadata
2548                    .commitment_cache
2549                    .as_mut()
2550                    .expect("the commitment cache was initialized")
2551            };
2552            cache.sync(&self.state.evidence)?;
2553            cache.needs()
2554        };
2555        let updates = self.compute_commitment_root_updates(needs)?;
2556        let boundary_head = self.boundary_head_hash().map(str::to_owned);
2557        let control = ControlCommitmentMaterial {
2558            plugin_registration_closed: self.state.metadata.plugin_registration_closed,
2559            next_event_id: self.state.counters.next_event_id,
2560            next_command_id: self.state.counters.next_command_id,
2561            next_command_attempt_id: self.state.counters.next_command_attempt_id,
2562            next_ingress_id: self.state.counters.next_ingress_id,
2563            next_boundary_id: self.state.counters.next_boundary_id,
2564            next_random_draw_id: self.state.counters.next_random_draw_id,
2565            next_schedule_sequence: self.state.counters.next_schedule_sequence,
2566            next_correlation_id: self.state.counters.next_correlation_id,
2567        };
2568        let (domain_roots, journal_roots) = {
2569            let cache = self
2570                .state
2571                .metadata
2572                .commitment_cache
2573                .as_mut()
2574                .expect("the commitment cache was initialized");
2575            cache.apply(updates);
2576            (cache.domain_roots()?, cache.roots())
2577        };
2578        runtime_commitment_roots(
2579            &domain_roots,
2580            &journal_roots,
2581            self.state.current.root_seed,
2582            boundary_head.as_deref(),
2583            &control,
2584        )
2585    }
2586
2587    fn refresh_checkpoint_hash(&mut self) -> Result<(), CanwuError> {
2588        if self.state.metadata.commitment_format_version == COMMITMENT_FORMAT_VERSION {
2589            let roots = self.refresh_runtime_commitment_roots()?;
2590            self.state.metadata.checkpoint_hash = checkpoint_hash_for_commitments(
2591                &roots,
2592                &self.state.metadata.run_manifest_hash,
2593                self.state.metadata.commitment_format_version,
2594                STATE_REVISION_FORMAT_VERSION,
2595                self.state.counters.state_revision,
2596                self.state.metadata.replay_revision_format_version,
2597            )?;
2598            self.state.metadata.commitment_roots = Some(roots);
2599        } else if self.state.metadata.commitment_format_version == 0 {
2600            let state_hash = self.compute_boundary_state_hash()?;
2601            self.state.metadata.checkpoint_hash = checkpoint_hash_for_configuration(
2602                &state_hash,
2603                self.boundary_head_hash(),
2604                &self.state.metadata.run_manifest_hash,
2605                &self.state.metadata.run_configuration,
2606                STATE_REVISION_FORMAT_VERSION,
2607                self.state.counters.state_revision,
2608                self.state.metadata.replay_revision_format_version,
2609            )?;
2610            self.state.metadata.commitment_roots = None;
2611            self.state.metadata.commitment_cache = None;
2612        } else {
2613            return Err(CanwuError::new(
2614                ErrorCode::UnsupportedSnapshotVersion,
2615                format!(
2616                    "commitment format {} is unsupported; this engine writes format {COMMITMENT_FORMAT_VERSION}",
2617                    self.state.metadata.commitment_format_version
2618                ),
2619            ));
2620        }
2621        Ok(())
2622    }
2623
2624    fn next_state_revision(&self) -> Result<u64, CanwuError> {
2625        self.state
2626            .counters
2627            .state_revision
2628            .checked_add(1)
2629            .ok_or_else(|| {
2630                CanwuError::new(
2631                    ErrorCode::IdentifierExhausted,
2632                    "authoritative state revision space is exhausted",
2633                )
2634            })
2635    }
2636
2637    fn advance_state_revision(&mut self) -> Result<u64, CanwuError> {
2638        let next = self.next_state_revision()?;
2639        self.state.counters.state_revision = next;
2640        Ok(next)
2641    }
2642
2643    #[must_use]
2644    pub fn snapshot(&self) -> SimulationSnapshot {
2645        let mut snapshot = self.checkpoint_state();
2646        snapshot.events.clone_from(&self.state.evidence.events);
2647        snapshot.commands.clone_from(&self.state.evidence.commands);
2648        snapshot
2649            .command_attempts
2650            .clone_from(&self.state.evidence.command_attempts);
2651        snapshot.ingress.clone_from(&self.state.evidence.ingress);
2652        snapshot
2653            .boundaries
2654            .clone_from(&self.state.evidence.boundaries);
2655        snapshot
2656            .random_draws
2657            .clone_from(&self.state.evidence.random_draws);
2658        snapshot
2659    }
2660
2661    pub fn snapshot_json(&self) -> Result<String, CanwuError> {
2662        serde_json::to_string_pretty(&self.snapshot()).map_err(|error| {
2663            CanwuError::new(
2664                ErrorCode::InvalidSnapshot,
2665                format!("could not serialize snapshot: {error}"),
2666            )
2667        })
2668    }
2669
2670    pub fn from_snapshot(snapshot: SimulationSnapshot) -> Result<Self, CanwuError> {
2671        let snapshot = migrate_snapshot(snapshot)?;
2672        validate_scenario(&Scenario {
2673            start_time: snapshot.now,
2674            world: snapshot.world.clone(),
2675            knowledge: snapshot.knowledge.clone(),
2676            domain_records: snapshot.domain_records.clone(),
2677        })?;
2678        let plugins = PluginRegistry::from_descriptors(snapshot.plugin_descriptors.clone())?;
2679        validate_snapshot(&snapshot, &plugins)?;
2680        let admitted_ingress: BTreeSet<_> = snapshot
2681            .boundaries
2682            .iter()
2683            .flat_map(|boundary| boundary.admitted_ingress.iter().copied())
2684            .collect();
2685        let pending_ingress = snapshot
2686            .ingress
2687            .iter()
2688            .filter(|record| !admitted_ingress.contains(&record.id))
2689            .map(IngressQueueKey::from_record)
2690            .collect();
2691        let initial_scenario = match snapshot.initial_scenario.clone() {
2692            Some(initial_scenario) => Some(initial_scenario),
2693            None if !snapshot.plugin_registration_closed => match snapshot.run_manifest.as_ref() {
2694                Some(run_manifest @ RunManifest::Declared { .. }) => {
2695                    let initial_scenario = Scenario {
2696                        start_time: snapshot.initial_time,
2697                        world: snapshot.world.clone(),
2698                        knowledge: snapshot.knowledge.clone(),
2699                        domain_records: snapshot.domain_records.clone(),
2700                    };
2701                    manifest::validate(run_manifest, Some(&initial_scenario), true)?;
2702                    Some(initial_scenario)
2703                }
2704                _ => None,
2705            },
2706            None => None,
2707        };
2708        let mut simulation = Self {
2709            state: RuntimeState {
2710                current: RuntimeCurrentState {
2711                    people: snapshot
2712                        .world
2713                        .people
2714                        .into_iter()
2715                        .map(|value| (value.id, value))
2716                        .collect(),
2717                    governments: snapshot
2718                        .world
2719                        .governments
2720                        .into_iter()
2721                        .map(|value| (value.id, value))
2722                        .collect(),
2723                    territories: snapshot
2724                        .world
2725                        .territories
2726                        .into_iter()
2727                        .map(|value| (value.id, value))
2728                        .collect(),
2729                    routes: snapshot
2730                        .world
2731                        .routes
2732                        .into_iter()
2733                        .map(|value| (value.id, value))
2734                        .collect(),
2735                    armies: snapshot
2736                        .world
2737                        .armies
2738                        .into_iter()
2739                        .map(|value| (value.id, value))
2740                        .collect(),
2741                    knowledge: snapshot.knowledge,
2742                    plugin_components: snapshot
2743                        .plugin_components
2744                        .into_iter()
2745                        .map(|record| {
2746                            (
2747                                component_key(
2748                                    &record.plugin,
2749                                    &record.state,
2750                                    &record.entity,
2751                                    &record.component,
2752                                ),
2753                                record,
2754                            )
2755                        })
2756                        .collect(),
2757                    domain_records: snapshot
2758                        .domain_records
2759                        .into_iter()
2760                        .map(|record| (record.reference.clone(), record))
2761                        .collect(),
2762                    root_seed: snapshot.root_seed,
2763                    random_streams: snapshot
2764                        .random_streams
2765                        .into_iter()
2766                        .map(|state| (state.key.clone(), state))
2767                        .collect(),
2768                },
2769                scheduler: RuntimeScheduler {
2770                    initial_time: snapshot.initial_time,
2771                    now: snapshot.now,
2772                    actions: snapshot
2773                        .scheduled
2774                        .into_iter()
2775                        .map(|record| (record.key, record.action))
2776                        .collect(),
2777                    pending_ingress,
2778                },
2779                counters: RuntimeCounters {
2780                    next_event_id: snapshot.next_event_id,
2781                    next_command_id: snapshot.next_command_id,
2782                    next_command_attempt_id: snapshot.next_command_attempt_id,
2783                    next_ingress_id: snapshot.next_ingress_id,
2784                    next_boundary_id: snapshot.next_boundary_id,
2785                    next_random_draw_id: snapshot.next_random_draw_id,
2786                    next_schedule_sequence: snapshot.next_schedule_sequence,
2787                    next_correlation_id: snapshot.next_correlation_id,
2788                    state_revision: snapshot.state_revision,
2789                    admitted_attempt_count: snapshot.admitted_attempt_count,
2790                    admitted_command_count: snapshot.admitted_command_count,
2791                    admitted_event_count: snapshot.admitted_event_count,
2792                },
2793                metadata: RuntimeMetadata {
2794                    initial_scenario,
2795                    run_manifest: snapshot.run_manifest.clone().ok_or_else(|| {
2796                        invalid_snapshot_error("snapshot is missing its run manifest")
2797                    })?,
2798                    run_manifest_hash: snapshot.run_manifest_hash.clone(),
2799                    run_configuration: snapshot.run_configuration.clone().ok_or_else(|| {
2800                        invalid_snapshot_error("snapshot is missing its run configuration")
2801                    })?,
2802                    checkpoint_hash: snapshot.checkpoint_hash.clone(),
2803                    commitment_format_version: snapshot.commitment_format_version,
2804                    commitment_roots: snapshot.commitment_roots.clone(),
2805                    commitment_cache: None,
2806                    plugin_registration_closed: snapshot.plugin_registration_closed,
2807                    replay_revision_format_version: snapshot.replay_revision_format_version,
2808                },
2809                evidence: RuntimeEvidence {
2810                    archived: EvidenceCursor::default(),
2811                    archived_boundary_head: None,
2812                    archived_legacy_commands: false,
2813                    archived_tracked_attempts: false,
2814                    archived_unqueued_command_history: false,
2815                    archived_command_requests: BTreeMap::new(),
2816                    archived_ingress_requests: BTreeMap::new(),
2817                    events: snapshot.events,
2818                    commands: snapshot.commands,
2819                    command_attempts: snapshot.command_attempts,
2820                    ingress: snapshot.ingress,
2821                    boundaries: snapshot.boundaries,
2822                    random_draws: snapshot.random_draws,
2823                },
2824            },
2825            schema: snapshot.schema,
2826            plugins,
2827            sync_reaction_depth: 0,
2828        };
2829        simulation.refresh_checkpoint_hash()?;
2830        Ok(simulation)
2831    }
2832
2833    pub fn from_snapshot_json(json: &str) -> Result<Self, CanwuError> {
2834        let snapshot = serde_json::from_str(json).map_err(|error| {
2835            CanwuError::new(
2836                ErrorCode::InvalidSnapshot,
2837                format!("could not deserialize snapshot: {error}"),
2838            )
2839        })?;
2840        Self::from_snapshot(snapshot)
2841    }
2842
2843    pub fn from_snapshot_with_plugins(
2844        snapshot: SimulationSnapshot,
2845        plugins: &[&dyn SimulationPlugin],
2846    ) -> Result<Self, CanwuError> {
2847        let mut simulation = Self::from_snapshot(snapshot)?;
2848        for plugin in plugins {
2849            simulation.register_plugin(*plugin)?;
2850        }
2851        simulation.ensure_runtime_ready()?;
2852        Ok(simulation)
2853    }
2854
2855    pub fn from_snapshot_json_with_plugins(
2856        json: &str,
2857        plugins: &[&dyn SimulationPlugin],
2858    ) -> Result<Self, CanwuError> {
2859        let snapshot = serde_json::from_str(json).map_err(|error| {
2860            CanwuError::new(
2861                ErrorCode::InvalidSnapshot,
2862                format!("could not deserialize snapshot: {error}"),
2863            )
2864        })?;
2865        Self::from_snapshot_with_plugins(snapshot, plugins)
2866    }
2867
2868    #[must_use]
2869    pub fn fork(&self) -> Self {
2870        Self {
2871            state: self.state.clone(),
2872            schema: self.schema.clone(),
2873            plugins: self.plugins.clone(),
2874            sync_reaction_depth: 0,
2875        }
2876    }
2877
2878    fn prepare_command(
2879        &self,
2880        envelope: &CommandEnvelope,
2881        context: &CommandContext,
2882    ) -> Result<PreparedCommand, CanwuError> {
2883        match &envelope.command {
2884            Command::MoveArmy { army, destination } => {
2885                let Some(actor) = decision_actor(&context.authority) else {
2886                    return Err(CanwuError::new(
2887                        ErrorCode::InvalidAuthority,
2888                        "move commands require an accountable actor origin",
2889                    ));
2890                };
2891                let person = self.state.current.people.get(&actor).ok_or_else(|| {
2892                    CanwuError::new(
2893                        ErrorCode::ActorNotFound,
2894                        format!("actor {actor} was not found"),
2895                    )
2896                    .with_entity(EntityRef::Person(actor))
2897                })?;
2898                let army_state = self.state.current.armies.get(army).ok_or_else(|| {
2899                    CanwuError::new(
2900                        ErrorCode::ArmyNotFound,
2901                        format!("army {army} was not found"),
2902                    )
2903                    .with_entity(EntityRef::Army(*army))
2904                })?;
2905                if army_state.commander != person.id {
2906                    return Err(CanwuError::new(
2907                        ErrorCode::InvalidAuthority,
2908                        format!("{} does not command {}", person.name, army_state.name),
2909                    )
2910                    .with_entity(EntityRef::Person(person.id))
2911                    .with_entity(EntityRef::Army(*army)));
2912                }
2913                if army_state.transit.is_some() {
2914                    return Err(CanwuError::new(
2915                        ErrorCode::InvalidAuthority,
2916                        format!("{} is already moving", army_state.name),
2917                    )
2918                    .with_entity(EntityRef::Army(*army)));
2919                }
2920                if !self.state.current.territories.contains_key(destination) {
2921                    return Err(CanwuError::new(
2922                        ErrorCode::DestinationNotFound,
2923                        format!("destination {destination} was not found"),
2924                    )
2925                    .with_entity(EntityRef::Territory(*destination)));
2926                }
2927                let route = self
2928                    .state
2929                    .current
2930                    .routes
2931                    .values()
2932                    .find(|route| route.connects(army_state.location, *destination))
2933                    .ok_or_else(|| {
2934                        CanwuError::new(
2935                            ErrorCode::NoRoute,
2936                            format!(
2937                                "no direct route connects territory {} to {destination}",
2938                                army_state.location
2939                            ),
2940                        )
2941                    })?;
2942                let arrival_at = self
2943                    .state
2944                    .scheduler
2945                    .now
2946                    .checked_add(SimDuration::minutes(route.travel_minutes))
2947                    .ok_or_else(|| {
2948                        CanwuError::new(
2949                            ErrorCode::InvalidDuration,
2950                            "army arrival time exceeds the supported range",
2951                        )
2952                    })?;
2953                Ok(PreparedCommand::MoveArmy {
2954                    army: *army,
2955                    actor,
2956                    from: army_state.location,
2957                    destination: *destination,
2958                    arrival_at,
2959                })
2960            }
2961            Command::DebugSetArmyMorale { army, morale } => {
2962                if envelope.issuer != Issuer::Debug {
2963                    return Err(CanwuError::new(
2964                        ErrorCode::InvalidAuthority,
2965                        "debug state edits require the explicit debug issuer",
2966                    ));
2967                }
2968                if *morale > 100 {
2969                    return Err(CanwuError::new(
2970                        ErrorCode::ValueOutOfRange,
2971                        "army morale must be between 0 and 100",
2972                    ));
2973                }
2974                let old_morale = self.state.current.armies.get(army).map_or_else(
2975                    || {
2976                        Err(CanwuError::new(
2977                            ErrorCode::ArmyNotFound,
2978                            format!("army {army} was not found"),
2979                        ))
2980                    },
2981                    |army_state| Ok(army_state.morale),
2982                )?;
2983                Ok(PreparedCommand::DebugMorale {
2984                    army: *army,
2985                    old_morale,
2986                    new_morale: *morale,
2987                })
2988            }
2989            Command::Plugin {
2990                plugin,
2991                command,
2992                payload,
2993            } => {
2994                let registered = self
2995                    .plugins
2996                    .commands
2997                    .get(&(plugin.clone(), command.clone()))
2998                    .ok_or_else(|| {
2999                        CanwuError::new(
3000                            ErrorCode::PluginCommandNotFound,
3001                            format!("plugin command {plugin}.{command} is not registered"),
3002                        )
3003                    })?;
3004                let handler = registered.handler;
3005                let descriptor = registered.descriptor.clone();
3006                descriptor.payload_schema.validate(payload)?;
3007                let reader = format!("{plugin}.{command}");
3008                let directives = catch_unwind(AssertUnwindSafe(|| {
3009                    handler(
3010                        &self.plugin_view(&reader, &descriptor.reads),
3011                        context,
3012                        payload,
3013                    )
3014                }))
3015                .map_err(|_| {
3016                    CanwuError::new(
3017                        ErrorCode::PluginPanicked,
3018                        format!("plugin command {plugin}.{command} panicked"),
3019                    )
3020                })??;
3021                validate_directives_with_context(
3022                    &RuntimeValidationContext::new(&self.state),
3023                    plugin,
3024                    &descriptor.writes,
3025                    &self.plugins.state_owners,
3026                    &self.plugins.record_schemas,
3027                    &directives,
3028                )?;
3029                Ok(PreparedCommand::Plugin {
3030                    plugin: plugin.clone(),
3031                    directives,
3032                    allowed_writes: descriptor.writes,
3033                })
3034            }
3035        }
3036    }
3037
3038    fn apply_prepared(
3039        &mut self,
3040        prepared: PreparedCommand,
3041        command_id: CommandId,
3042        correlation_id: u64,
3043    ) -> Result<(), CanwuError> {
3044        match prepared {
3045            PreparedCommand::MoveArmy {
3046                army,
3047                actor,
3048                from,
3049                destination,
3050                arrival_at,
3051            } => {
3052                let army_state = self.state.current.armies.get_mut(&army).ok_or_else(|| {
3053                    CanwuError::new(ErrorCode::ArmyNotFound, "validated army disappeared")
3054                })?;
3055                army_state.transit = Some(TransitState {
3056                    from,
3057                    to: destination,
3058                    departed_at: self.state.scheduler.now,
3059                    arrives_at: arrival_at,
3060                });
3061                let event = self.emit(
3062                    EventKind::MoveOrdered {
3063                        army,
3064                        from,
3065                        to: destination,
3066                        arrival_at,
3067                    },
3068                    vec![
3069                        EntityRef::Army(army),
3070                        EntityRef::Person(actor),
3071                        EntityRef::Territory(from),
3072                        EntityRef::Territory(destination),
3073                    ],
3074                    format!("Army {army} was ordered from {from} to {destination}"),
3075                    Some(CauseRef::Command(command_id)),
3076                    correlation_id,
3077                )?;
3078                self.schedule_at(
3079                    arrival_at,
3080                    ScheduledAction::ArmyArrival {
3081                        army,
3082                        destination,
3083                        order_event: event,
3084                        correlation_id,
3085                    },
3086                )?;
3087            }
3088            PreparedCommand::DebugMorale {
3089                army,
3090                old_morale,
3091                new_morale,
3092            } => {
3093                self.state
3094                    .current
3095                    .armies
3096                    .get_mut(&army)
3097                    .ok_or_else(|| {
3098                        CanwuError::new(ErrorCode::ArmyNotFound, "validated army disappeared")
3099                    })?
3100                    .morale = new_morale;
3101                self.emit(
3102                    EventKind::DebugFieldChanged {
3103                        entity: EntityRef::Army(army),
3104                        field: "morale".to_owned(),
3105                        old_value: old_morale.to_string(),
3106                        new_value: new_morale.to_string(),
3107                    },
3108                    vec![EntityRef::Army(army)],
3109                    format!(
3110                        "Debug command changed army {army} morale {old_morale} -> {new_morale}"
3111                    ),
3112                    Some(CauseRef::Command(command_id)),
3113                    correlation_id,
3114                )?;
3115            }
3116            PreparedCommand::Plugin {
3117                plugin,
3118                directives,
3119                allowed_writes,
3120            } => {
3121                self.apply_directives(
3122                    &plugin,
3123                    directives,
3124                    &allowed_writes,
3125                    &CauseRef::Command(command_id),
3126                    correlation_id,
3127                )?;
3128            }
3129        }
3130        Ok(())
3131    }
3132}
3133
3134enum PreparedCommand {
3135    MoveArmy {
3136        army: ArmyId,
3137        actor: PersonId,
3138        from: TerritoryId,
3139        destination: TerritoryId,
3140        arrival_at: SimTime,
3141    },
3142    DebugMorale {
3143        army: ArmyId,
3144        old_morale: u16,
3145        new_morale: u16,
3146    },
3147    Plugin {
3148        plugin: String,
3149        directives: Vec<SystemDirective>,
3150        allowed_writes: Vec<StateKey>,
3151    },
3152}
3153
3154impl PreparedCommand {
3155    fn commitment_invalidation(&self) -> CommitmentDomains {
3156        match self {
3157            Self::MoveArmy { .. } => {
3158                CommitmentDomains::WORLD
3159                    | CommitmentDomains::KNOWLEDGE
3160                    | CommitmentDomains::PLUGIN_COMPONENTS
3161                    | CommitmentDomains::SCHEDULER
3162            }
3163            Self::DebugMorale { .. } => {
3164                CommitmentDomains::WORLD
3165                    | CommitmentDomains::PLUGIN_COMPONENTS
3166                    | CommitmentDomains::SCHEDULER
3167            }
3168            Self::Plugin { .. } => {
3169                CommitmentDomains::PLUGIN_COMPONENTS | CommitmentDomains::SCHEDULER
3170            }
3171        }
3172    }
3173}
3174
3175fn validate_directives(
3176    plugin: &str,
3177    allowed_writes: &[StateKey],
3178    state_owners: &BTreeMap<StateKey, String>,
3179    record_schemas: &records::DomainRecordSchemas,
3180    entity_exists: &dyn Fn(&EntityRef) -> bool,
3181    directives: &[SystemDirective],
3182) -> Result<(), CanwuError> {
3183    for directive in directives {
3184        match directive {
3185            SystemDirective::SetComponent {
3186                state,
3187                entity,
3188                component,
3189                ..
3190            } => {
3191                if component.trim().is_empty() || component != component.trim() {
3192                    return Err(CanwuError::new(
3193                        ErrorCode::InvalidPayload,
3194                        "plugin component name must be non-empty and canonical",
3195                    ));
3196                }
3197                if !allowed_writes.contains(state) {
3198                    return Err(CanwuError::new(
3199                        ErrorCode::UndeclaredStateWrite,
3200                        format!(
3201                            "plugin {plugin} did not declare write access to {}.{}",
3202                            state.namespace, state.name
3203                        ),
3204                    ));
3205                }
3206                if state_owners.get(state).is_none_or(|owner| owner != plugin) {
3207                    return Err(CanwuError::new(
3208                        ErrorCode::UndeclaredStateWrite,
3209                        format!(
3210                            "plugin {plugin} does not own state {}.{}",
3211                            state.namespace, state.name
3212                        ),
3213                    ));
3214                }
3215                if is_domain_record_state(record_schemas, state) {
3216                    return Err(CanwuError::new(
3217                        ErrorCode::UndeclaredStateWrite,
3218                        "domain record state cannot be written as an immediate component",
3219                    ));
3220                }
3221                if !entity_exists(entity) {
3222                    return Err(CanwuError::new(
3223                        ErrorCode::EntityNotFound,
3224                        format!("plugin {plugin} targeted missing entity {entity}"),
3225                    )
3226                    .with_entity(entity.clone()));
3227                }
3228            }
3229            SystemDirective::Emit { event_type, .. }
3230                if event_type.trim().is_empty() || event_type != event_type.trim() =>
3231            {
3232                return Err(CanwuError::new(
3233                    ErrorCode::InvalidPayload,
3234                    "plugin event type must be non-empty and canonical",
3235                ));
3236            }
3237            SystemDirective::Emit { affected, .. }
3238                if affected.iter().any(|entity| !entity_exists(entity)) =>
3239            {
3240                return Err(CanwuError::new(
3241                    ErrorCode::EntityNotFound,
3242                    format!("plugin {plugin} emitted an event for a missing entity"),
3243                ));
3244            }
3245            SystemDirective::Schedule { after, directive } => {
3246                if *after <= SimDuration::ZERO {
3247                    return Err(CanwuError::new(
3248                        ErrorCode::InvalidDuration,
3249                        "plugin systems must schedule work strictly in the future",
3250                    ));
3251                }
3252                validate_directives(
3253                    plugin,
3254                    allowed_writes,
3255                    state_owners,
3256                    record_schemas,
3257                    entity_exists,
3258                    std::slice::from_ref(directive),
3259                )?;
3260            }
3261            SystemDirective::Emit { .. } => {}
3262        }
3263    }
3264    Ok(())
3265}
3266
3267fn resolve_command_authority(envelope: &CommandEnvelope) -> Result<CommandAuthority, CanwuError> {
3268    if let Some(authority) = &envelope.authority {
3269        return Ok(authority.clone());
3270    }
3271    match &envelope.issuer {
3272        Issuer::Actor(actor) => Ok(CommandAuthority::for_actor(*actor)),
3273        Issuer::Debug => Ok(CommandAuthority::no_responsible_actor("debug-command")),
3274        Issuer::System(system) => Ok(CommandAuthority::no_responsible_actor(format!(
3275            "system:{system}"
3276        ))),
3277        Issuer::Human(_)
3278        | Issuer::Ai(_)
3279        | Issuer::Institution(_)
3280        | Issuer::Replay(_)
3281        | Issuer::Experiment(_) => Err(CanwuError::new(
3282            ErrorCode::InvalidAuthority,
3283            "typed command origins require an explicit authority context",
3284        )),
3285    }
3286}
3287
3288fn validate_command_ingress_policy(
3289    run_configuration: &RunConfigurationSnapshot,
3290    issuer: &Issuer,
3291    authority: &CommandAuthority,
3292    admission: CommandAdmission,
3293    entity_exists: &dyn Fn(&EntityRef) -> bool,
3294) -> Result<(), CanwuError> {
3295    let CommandAdmission {
3296        request_id,
3297        expected_revision,
3298        expected_time,
3299        revision_before: current_revision,
3300        ingress,
3301    } = admission;
3302    if request_id.is_some_and(|id| id.get() == 0) {
3303        return Err(CanwuError::new(
3304            ErrorCode::InvalidPayload,
3305            "command request IDs must be nonzero",
3306        ));
3307    }
3308    if let Some(expected) = expected_revision
3309        && expected != current_revision
3310    {
3311        return Err(CanwuError::new(
3312            ErrorCode::SimulationRevisionConflict,
3313            format!(
3314                "command expected revision {expected}, but simulation is at revision {current_revision}"
3315            ),
3316        ));
3317    }
3318    validate_command_authority(authority, entity_exists)?;
3319    if matches!(issuer, Issuer::Replay(_)) != (ingress == CommandIngress::FrozenReplay) {
3320        return Err(CanwuError::new(
3321            ErrorCode::InvalidAuthority,
3322            "replay command origins are valid only for frozen replay ingress",
3323        ));
3324    }
3325
3326    let RunConfigurationSnapshot::Declared(configuration) = run_configuration else {
3327        return Ok(());
3328    };
3329    if ingress == CommandIngress::LegacyDirect {
3330        return Err(CanwuError::new(
3331            ErrorCode::InvalidAuthority,
3332            "declared runs require tracked request or frozen replay ingress",
3333        ));
3334    }
3335    let external = !matches!(issuer, Issuer::System(_));
3336    if configuration.require_idempotency_keys && external && request_id.is_none() {
3337        return Err(CanwuError::new(
3338            ErrorCode::MissingIdempotencyKey,
3339            "this run requires a stable command request ID",
3340        ));
3341    }
3342    if configuration.require_idempotency_keys && external && expected_revision.is_none() {
3343        return Err(CanwuError::new(
3344            ErrorCode::SimulationRevisionConflict,
3345            "this run requires an expected command revision",
3346        ));
3347    }
3348    if configuration.interaction == InteractionPolicy::ReadOnly
3349        && !matches!(issuer, Issuer::Replay(_) | Issuer::System(_))
3350    {
3351        return Err(CanwuError::new(
3352            ErrorCode::InteractionReadOnly,
3353            "the run interaction policy rejects newly authored authoritative commands",
3354        ));
3355    }
3356    if external && expected_time.is_none() {
3357        return Err(CanwuError::new(
3358            ErrorCode::SimulationTimeConflict,
3359            "declared external commands require an expected simulation time",
3360        ));
3361    }
3362
3363    match issuer {
3364        Issuer::Actor(_) => Err(CanwuError::new(
3365            ErrorCode::InvalidAuthority,
3366            "declared runs require a typed human, AI, institution, replay, experiment, debug, or system origin",
3367        )),
3368        Issuer::Human(controller) => {
3369            let Some(binding) = &configuration.seat_binding else {
3370                return Err(CanwuError::new(
3371                    ErrorCode::InvalidAuthority,
3372                    "human commands require the run's exact seat binding",
3373                ));
3374            };
3375            if configuration.controller != ControllerPolicy::HumanRoleBound
3376                || controller != &binding.controller_id
3377                || authority.seat_id.as_deref() != Some(binding.seat_id.as_str())
3378                || authority.permission_profile_id.as_deref()
3379                    != Some(binding.permission_profile_id.as_str())
3380                || !authority_matches_seat_binding(configuration.seat, binding, authority)
3381            {
3382                return Err(CanwuError::new(
3383                    ErrorCode::InvalidAuthority,
3384                    "human command origin does not match the active controller, seat binding, and permission profile",
3385                ));
3386            }
3387            Ok(())
3388        }
3389        Issuer::Ai(controller) | Issuer::Institution(controller) => {
3390            if !canonical_text(controller)
3391                || matches!(
3392                    authority.decision_origin,
3393                    DecisionOrigin::NoResponsibleActor { .. }
3394                )
3395            {
3396                return Err(CanwuError::new(
3397                    ErrorCode::InvalidAuthority,
3398                    "AI and institutional commands require a canonical controller and responsible decision origin",
3399                ));
3400            }
3401            Ok(())
3402        }
3403        Issuer::Replay(source) => {
3404            if !canonical_text(source)
3405                || ingress != CommandIngress::FrozenReplay
3406                || configuration.purpose != RunPurpose::Replay
3407                || configuration.controller != ControllerPolicy::ReplayController
3408                || configuration.interaction != InteractionPolicy::ReadOnly
3409            {
3410                return Err(CanwuError::new(
3411                    ErrorCode::InvalidAuthority,
3412                    "replay command sources require a replay-purpose, replay-controller, read-only run",
3413                ));
3414            }
3415            if let Some(binding) = &configuration.seat_binding
3416                && (source != &binding.controller_id
3417                    || authority.seat_id.as_deref() != Some(binding.seat_id.as_str())
3418                    || authority.permission_profile_id.as_deref()
3419                        != Some(binding.permission_profile_id.as_str())
3420                    || !authority_matches_seat_binding(configuration.seat, binding, authority))
3421            {
3422                return Err(CanwuError::new(
3423                    ErrorCode::InvalidAuthority,
3424                    "frozen replay input does not match its recorded controller and seat binding",
3425                ));
3426            }
3427            Ok(())
3428        }
3429        Issuer::Experiment(intervention) => {
3430            if configuration.interaction != InteractionPolicy::VersionedExperiment
3431                || !configuration.declared_interventions.contains(intervention)
3432            {
3433                return Err(CanwuError::new(
3434                    ErrorCode::InvalidAuthority,
3435                    "experiment commands must name an intervention declared by the run",
3436                ));
3437            }
3438            Ok(())
3439        }
3440        Issuer::Debug => {
3441            if !configuration.diagnostic_commands_enabled {
3442                return Err(CanwuError::new(
3443                    ErrorCode::InvalidAuthority,
3444                    "debug command authority is disabled by the run configuration",
3445                ));
3446            }
3447            Ok(())
3448        }
3449        Issuer::System(system) => {
3450            if !canonical_text(system)
3451                || !matches!(
3452                    authority.decision_origin,
3453                    DecisionOrigin::NoResponsibleActor { .. }
3454                )
3455            {
3456                return Err(CanwuError::new(
3457                    ErrorCode::InvalidAuthority,
3458                    "system commands require a canonical system ID and typed no-responsible-actor origin",
3459                ));
3460            }
3461            Ok(())
3462        }
3463    }
3464}
3465
3466fn validate_command_authority(
3467    authority: &CommandAuthority,
3468    entity_exists: &dyn Fn(&EntityRef) -> bool,
3469) -> Result<(), CanwuError> {
3470    if authority
3471        .seat_id
3472        .as_ref()
3473        .is_some_and(|value| !canonical_text(value))
3474        || authority
3475            .permission_profile_id
3476            .as_ref()
3477            .is_some_and(|value| !canonical_text(value))
3478        || authority.seat_id.is_some() != authority.permission_profile_id.is_some()
3479        || authority
3480            .command_subject
3481            .as_ref()
3482            .is_some_and(|entity| !entity_exists(entity))
3483    {
3484        return Err(CanwuError::new(
3485            ErrorCode::InvalidAuthority,
3486            "command authority contains an invalid seat, permission profile, or subject",
3487        ));
3488    }
3489    match &authority.decision_origin {
3490        DecisionOrigin::Actor { actor } => {
3491            if !entity_exists(&EntityRef::Person(*actor)) {
3492                return Err(CanwuError::new(
3493                    ErrorCode::InvalidAuthority,
3494                    "command decision origin references a missing actor",
3495                ));
3496            }
3497        }
3498        DecisionOrigin::Institution {
3499            institution,
3500            responsible_actor,
3501        } => {
3502            if !entity_exists(institution)
3503                || responsible_actor.is_some_and(|actor| !entity_exists(&EntityRef::Person(actor)))
3504            {
3505                return Err(CanwuError::new(
3506                    ErrorCode::InvalidAuthority,
3507                    "command decision origin references a missing institution or actor",
3508                ));
3509            }
3510        }
3511        DecisionOrigin::Council { council_id } if !canonical_text(council_id) => {
3512            return Err(CanwuError::new(
3513                ErrorCode::InvalidAuthority,
3514                "command council origin requires a canonical ID",
3515            ));
3516        }
3517        DecisionOrigin::NoResponsibleActor { reason } if !canonical_text(reason) => {
3518            return Err(CanwuError::new(
3519                ErrorCode::InvalidAuthority,
3520                "no-responsible-actor origins require a canonical reason",
3521            ));
3522        }
3523        DecisionOrigin::Council { .. } | DecisionOrigin::NoResponsibleActor { .. } => {}
3524    }
3525    Ok(())
3526}
3527
3528fn authority_matches_seat_binding(
3529    seat: SeatPolicy,
3530    binding: &SeatBinding,
3531    authority: &CommandAuthority,
3532) -> bool {
3533    match (seat, &authority.decision_origin) {
3534        (SeatPolicy::CharacterBound, DecisionOrigin::Actor { actor }) => {
3535            binding.actor == Some(*actor) && binding.institution.is_none()
3536        }
3537        (
3538            SeatPolicy::InstitutionBound,
3539            DecisionOrigin::Institution {
3540                institution,
3541                responsible_actor,
3542            },
3543        ) => {
3544            binding.institution.as_ref() == Some(institution)
3545                && binding
3546                    .actor
3547                    .is_none_or(|actor| Some(actor) == *responsible_actor)
3548        }
3549        (SeatPolicy::ObserverSeat | SeatPolicy::AdvisorSeat, origin) => {
3550            let actor_matches = binding.actor.is_none_or(
3551                |expected| matches!(origin, DecisionOrigin::Actor { actor } if *actor == expected),
3552            );
3553            let institution_matches = binding.institution.as_ref().is_none_or(|expected| {
3554                matches!(
3555                    origin,
3556                    DecisionOrigin::Institution { institution, .. } if institution == expected
3557                )
3558            });
3559            actor_matches && institution_matches
3560        }
3561        _ => false,
3562    }
3563}
3564
3565const fn decision_actor(authority: &CommandAuthority) -> Option<PersonId> {
3566    match &authority.decision_origin {
3567        DecisionOrigin::Actor { actor } => Some(*actor),
3568        DecisionOrigin::Institution {
3569            responsible_actor, ..
3570        } => *responsible_actor,
3571        DecisionOrigin::Council { .. } | DecisionOrigin::NoResponsibleActor { .. } => None,
3572    }
3573}
3574
3575const fn is_expected_command_rejection(code: &ErrorCode) -> bool {
3576    matches!(
3577        code,
3578        ErrorCode::ActorNotFound
3579            | ErrorCode::ArmyNotFound
3580            | ErrorCode::DestinationNotFound
3581            | ErrorCode::EntityNotFound
3582            | ErrorCode::IdempotencyConflict
3583            | ErrorCode::InteractionReadOnly
3584            | ErrorCode::InvalidAuthority
3585            | ErrorCode::InvalidDuration
3586            | ErrorCode::InvalidPayload
3587            | ErrorCode::MissingIdempotencyKey
3588            | ErrorCode::MixedCommandIngress
3589            | ErrorCode::NoRoute
3590            | ErrorCode::PluginCommandNotFound
3591            | ErrorCode::SimulationRevisionConflict
3592            | ErrorCode::SimulationTimeConflict
3593            | ErrorCode::ValueOutOfRange
3594    )
3595}
3596
3597fn canonical_text(value: &str) -> bool {
3598    !value.is_empty() && value == value.trim()
3599}
3600
3601fn component_key(
3602    plugin: &str,
3603    state: &StateKey,
3604    entity: &EntityRef,
3605    component: &str,
3606) -> PluginComponentKey {
3607    PluginComponentKey {
3608        plugin: plugin.to_owned(),
3609        state: state.clone(),
3610        entity: entity.clone(),
3611        component: component.to_owned(),
3612    }
3613}
3614
3615fn record_change_affected_entities(change: &DomainRecordChange) -> Vec<EntityRef> {
3616    (change.current.class == DomainRecordClass::Entity)
3617        .then(|| EntityRef::Domain(change.current.reference.clone()))
3618        .into_iter()
3619        .collect()
3620}
3621
3622fn is_domain_record_state(schemas: &records::DomainRecordSchemas, state: &StateKey) -> bool {
3623    schemas.contains_key(&DomainRecordKind::new(&state.namespace, &state.name))
3624}
3625
3626fn snapshot_command_attempt_preflight_error(
3627    snapshot: &SimulationSnapshot,
3628    attempt: &CommandAttemptRecord,
3629    history: &DomainRecordHistory,
3630    cut: DomainHistoryCut,
3631) -> Option<CanwuError> {
3632    let authority = match resolve_command_authority(&attempt.envelope) {
3633        Ok(authority) => authority,
3634        Err(error) => return Some(error),
3635    };
3636    if let Err(error) = validate_command_ingress_policy(
3637        snapshot
3638            .run_configuration
3639            .as_ref()
3640            .expect("snapshot run configuration is validated before command attempts"),
3641        &attempt.envelope.issuer,
3642        &authority,
3643        CommandAdmission {
3644            request_id: attempt.request_id,
3645            expected_revision: attempt.expected_revision,
3646            expected_time: attempt.envelope.expected_time,
3647            revision_before: attempt.revision_before,
3648            ingress: attempt.ingress,
3649        },
3650        &|entity| snapshot_entity_exists_in_history(snapshot, history, cut, entity),
3651    ) {
3652        return Some(error);
3653    }
3654    attempt.envelope.expected_time.and_then(|expected_time| {
3655        (expected_time != attempt.at).then(|| {
3656            CanwuError::new(
3657                ErrorCode::SimulationTimeConflict,
3658                format!(
3659                    "command expected time {expected_time}, but simulation is at {}",
3660                    attempt.at
3661                ),
3662            )
3663        })
3664    })
3665}
3666
3667fn invalid_snapshot_error(message: impl Into<String>) -> CanwuError {
3668    CanwuError::new(ErrorCode::InvalidSnapshot, message)
3669}
3670
3671fn invalid_snapshot<T>(message: impl Into<String>) -> Result<T, CanwuError> {
3672    Err(invalid_snapshot_error(message))
3673}
3674
3675fn require_plugin_aware_initial_records(scenario: &Scenario) -> Result<(), CanwuError> {
3676    if scenario.domain_records.is_empty() {
3677        return Ok(());
3678    }
3679    Err(CanwuError::new(
3680        ErrorCode::PluginNotActive,
3681        "scenarios with initial domain records require a plugin-aware constructor",
3682    ))
3683}
3684
3685fn canonicalize_scenario(scenario: &mut Scenario) {
3686    scenario.world.people.sort_by_key(|value| value.id);
3687    scenario.world.governments.sort_by_key(|value| value.id);
3688    scenario.world.territories.sort_by_key(|value| value.id);
3689    scenario.world.routes.sort_by_key(|value| value.id);
3690    scenario.world.armies.sort_by_key(|value| value.id);
3691    scenario
3692        .domain_records
3693        .sort_by(|left, right| left.reference.cmp(&right.reference));
3694}
3695
3696fn validate_scenario(scenario: &Scenario) -> Result<(), CanwuError> {
3697    validate_unique_ids(&scenario.world.people, |value| value.id, "person")?;
3698    validate_unique_ids(&scenario.world.governments, |value| value.id, "government")?;
3699    validate_unique_ids(&scenario.world.territories, |value| value.id, "territory")?;
3700    validate_unique_ids(&scenario.world.routes, |value| value.id, "route")?;
3701    validate_unique_ids(&scenario.world.armies, |value| value.id, "army")?;
3702
3703    for person in &scenario.world.people {
3704        if scenario.world.government(person.government).is_none()
3705            || scenario.world.territory(person.current_location).is_none()
3706        {
3707            return Err(CanwuError::new(
3708                ErrorCode::InvalidSnapshot,
3709                format!(
3710                    "person {} references a missing government or location",
3711                    person.id
3712                ),
3713            ));
3714        }
3715    }
3716    for government in &scenario.world.governments {
3717        if scenario.world.territory(government.capital).is_none() {
3718            return Err(CanwuError::new(
3719                ErrorCode::InvalidSnapshot,
3720                format!("government {} references a missing capital", government.id),
3721            ));
3722        }
3723    }
3724    for territory in &scenario.world.territories {
3725        if scenario.world.government(territory.controller).is_none()
3726            || !territory.position.x.is_finite()
3727            || !territory.position.y.is_finite()
3728        {
3729            return Err(CanwuError::new(
3730                ErrorCode::InvalidSnapshot,
3731                format!(
3732                    "territory {} has a missing controller or non-finite position",
3733                    territory.id
3734                ),
3735            ));
3736        }
3737    }
3738    for army in &scenario.world.armies {
3739        if scenario.world.person(army.commander).is_none()
3740            || scenario.world.government(army.government).is_none()
3741        {
3742            return Err(CanwuError::new(
3743                ErrorCode::InvalidSnapshot,
3744                format!(
3745                    "army {} references a missing commander or government",
3746                    army.id
3747                ),
3748            ));
3749        }
3750        if scenario.world.territory(army.location).is_none() {
3751            return Err(CanwuError::new(
3752                ErrorCode::InvalidSnapshot,
3753                format!("army {} references a missing location", army.id),
3754            ));
3755        }
3756        if let Some(transit) = &army.transit
3757            && (scenario.world.territory(transit.from).is_none()
3758                || scenario.world.territory(transit.to).is_none()
3759                || transit.arrives_at < transit.departed_at
3760                || transit.departed_at > scenario.start_time
3761                || army.location != transit.from)
3762        {
3763            return Err(CanwuError::new(
3764                ErrorCode::InvalidSnapshot,
3765                format!("army {} has invalid transit state", army.id),
3766            ));
3767        }
3768    }
3769    for route in &scenario.world.routes {
3770        if scenario.world.territory(route.from).is_none()
3771            || scenario.world.territory(route.to).is_none()
3772            || route.travel_minutes <= 0
3773        {
3774            return Err(CanwuError::new(
3775                ErrorCode::InvalidSnapshot,
3776                format!("route {} has invalid endpoints or travel time", route.id),
3777            ));
3778        }
3779    }
3780    records::validate_initial_records(&scenario.domain_records, scenario.start_time, &|entity| {
3781        core_world_entity_exists(&scenario.world, entity)
3782    })?;
3783    for (actor_id, actor) in &scenario.knowledge.actors {
3784        if actor.actor != *actor_id || scenario.world.person(*actor_id).is_none() {
3785            return Err(CanwuError::new(
3786                ErrorCode::InvalidSnapshot,
3787                format!("knowledge actor {actor_id} is inconsistent or missing"),
3788            ));
3789        }
3790        for (army_id, record) in &actor.armies {
3791            if record.army != *army_id
3792                || scenario.world.army(*army_id).is_none()
3793                || record
3794                    .known_location
3795                    .is_some_and(|location| scenario.world.territory(location).is_none())
3796                || record.estimated_strength.minimum > record.estimated_strength.maximum
3797                || record.confidence_per_mille > 1000
3798                || record.observed_at > record.learned_at
3799                || record.observed_at > scenario.start_time
3800                || record.learned_at > scenario.start_time
3801            {
3802                return Err(CanwuError::new(
3803                    ErrorCode::InvalidSnapshot,
3804                    format!("knowledge record for actor {actor_id} and army {army_id} is invalid"),
3805                ));
3806            }
3807        }
3808    }
3809    Ok(())
3810}
3811
3812fn validate_unique_ids<T, I, F>(values: &[T], mut id_of: F, label: &str) -> Result<(), CanwuError>
3813where
3814    I: Copy + Default + Display + Ord,
3815    F: FnMut(&T) -> I,
3816{
3817    let mut ids = BTreeSet::new();
3818    for value in values {
3819        let id = id_of(value);
3820        if id == I::default() {
3821            return Err(CanwuError::new(
3822                ErrorCode::InvalidSnapshot,
3823                format!("{label} IDs must be nonzero"),
3824            ));
3825        }
3826        if !ids.insert(id) {
3827            return Err(CanwuError::new(
3828                ErrorCode::InvalidSnapshot,
3829                format!("duplicate {label} ID {id}"),
3830            ));
3831        }
3832    }
3833    Ok(())
3834}
3835
3836fn validate_strict_id_order<T, I, F>(
3837    values: &[T],
3838    mut id_of: F,
3839    label: &str,
3840) -> Result<(), CanwuError>
3841where
3842    I: Copy + Ord,
3843    F: FnMut(&T) -> I,
3844{
3845    if values
3846        .windows(2)
3847        .any(|pair| id_of(&pair[0]) >= id_of(&pair[1]))
3848    {
3849        return invalid_snapshot(format!("snapshot {label} are not in canonical ID order"));
3850    }
3851    Ok(())
3852}
3853
3854fn field(name: &str, value_type: &str, description: &str) -> FieldSchema {
3855    FieldSchema {
3856        name: name.to_owned(),
3857        value_type: value_type.to_owned(),
3858        description: description.to_owned(),
3859        reference_type: None,
3860        writable_via_debug_command: false,
3861    }
3862}
3863
3864fn base_schema() -> SchemaRegistry {
3865    let mut schema = SchemaRegistry::default();
3866    schema.register(TypeSchema {
3867        type_name: "person".to_owned(),
3868        description: "Historical actor with roles and a location".to_owned(),
3869        fields: vec![
3870            field("id", "PersonId", "Stable person identifier"),
3871            field("name", "String", "Display name"),
3872            field("government", "GovernmentId", "Government membership"),
3873            field("current_location", "TerritoryId", "Current territory"),
3874            field("roles", "Vec<String>", "Offices and authorities"),
3875        ],
3876    });
3877    schema.register(TypeSchema {
3878        type_name: "army".to_owned(),
3879        description: "Mobile military organization".to_owned(),
3880        fields: vec![
3881            field("id", "ArmyId", "Stable army identifier"),
3882            field("commander", "PersonId", "Commanding person"),
3883            field("location", "TerritoryId", "Ground-truth territory"),
3884            field("strength", "u32", "Ground-truth personnel strength"),
3885            FieldSchema {
3886                name: "morale".to_owned(),
3887                value_type: "u16".to_owned(),
3888                description: "Morale from 0 through 100".to_owned(),
3889                reference_type: None,
3890                writable_via_debug_command: true,
3891            },
3892            field("transit", "Option<TransitState>", "Pending movement"),
3893        ],
3894    });
3895    schema.register(TypeSchema {
3896        type_name: "territory".to_owned(),
3897        description: "Administrative and geographic unit".to_owned(),
3898        fields: vec![
3899            field("id", "TerritoryId", "Stable territory identifier"),
3900            field("controller", "GovernmentId", "Controlling government"),
3901            field("position", "MapPoint", "Abstract visualization point"),
3902        ],
3903    });
3904    schema.register(TypeSchema {
3905        type_name: "route".to_owned(),
3906        description: "Travel connection between territories".to_owned(),
3907        fields: vec![
3908            field("from", "TerritoryId", "First route endpoint"),
3909            field("to", "TerritoryId", "Second route endpoint"),
3910            field("travel_minutes", "i64", "Deterministic travel duration"),
3911            field("terrain", "String", "Terrain classification"),
3912        ],
3913    });
3914    schema.register(TypeSchema {
3915        type_name: "event".to_owned(),
3916        description: "Inspectable state-change or information event".to_owned(),
3917        fields: vec![field("timestamp", "SimTime", "Simulation occurrence time")],
3918    });
3919    schema
3920}
3921
3922#[must_use]
3923pub fn demo_scenario() -> (Scenario, DemoIds) {
3924    let ids = DemoIds {
3925        commander: PersonId::new(1),
3926        observer: PersonId::new(2),
3927        government: GovernmentId::new(1),
3928        army: ArmyId::new(1),
3929        western_territory: TerritoryId::new(1),
3930        central_territory: TerritoryId::new(2),
3931        eastern_territory: TerritoryId::new(3),
3932    };
3933    let world = WorldSnapshot {
3934        people: vec![
3935            Person {
3936                id: ids.commander,
3937                name: "General Shen".to_owned(),
3938                government: ids.government,
3939                current_location: ids.central_territory,
3940                roles: vec!["army_commander".to_owned()],
3941            },
3942            Person {
3943                id: ids.observer,
3944                name: "Minister Luo".to_owned(),
3945                government: ids.government,
3946                current_location: ids.western_territory,
3947                roles: vec!["civil_minister".to_owned()],
3948            },
3949        ],
3950        governments: vec![Government {
3951            id: ids.government,
3952            name: "State of Yun".to_owned(),
3953            capital: ids.central_territory,
3954        }],
3955        territories: vec![
3956            Territory {
3957                id: ids.western_territory,
3958                name: "Westford".to_owned(),
3959                controller: ids.government,
3960                position: MapPoint { x: 80.0, y: 180.0 },
3961            },
3962            Territory {
3963                id: ids.central_territory,
3964                name: "Yun Capital".to_owned(),
3965                controller: ids.government,
3966                position: MapPoint { x: 240.0, y: 120.0 },
3967            },
3968            Territory {
3969                id: ids.eastern_territory,
3970                name: "Eastwatch".to_owned(),
3971                controller: ids.government,
3972                position: MapPoint { x: 420.0, y: 210.0 },
3973            },
3974        ],
3975        routes: vec![
3976            Route {
3977                id: RouteId::new(1),
3978                name: "Western Post Road".to_owned(),
3979                from: ids.western_territory,
3980                to: ids.central_territory,
3981                travel_minutes: SimDuration::hours(12).as_minutes(),
3982                terrain: "road".to_owned(),
3983            },
3984            Route {
3985                id: RouteId::new(2),
3986                name: "Eastern River Road".to_owned(),
3987                from: ids.central_territory,
3988                to: ids.eastern_territory,
3989                travel_minutes: SimDuration::hours(18).as_minutes(),
3990                terrain: "river_road".to_owned(),
3991            },
3992        ],
3993        armies: vec![Army {
3994            id: ids.army,
3995            name: "First Field Army".to_owned(),
3996            government: ids.government,
3997            commander: ids.commander,
3998            location: ids.central_territory,
3999            strength: 8_000,
4000            morale: 72,
4001            transit: None,
4002        }],
4003    };
4004    let initial_time = SimTime::EPOCH;
4005    let mut knowledge = KnowledgeSnapshot::default();
4006    knowledge.actors.insert(
4007        ids.commander,
4008        ActorKnowledge {
4009            actor: ids.commander,
4010            armies: BTreeMap::from([(
4011                ids.army,
4012                ArmyKnowledge {
4013                    army: ids.army,
4014                    known_name: Some("First Field Army".to_owned()),
4015                    known_location: Some(ids.central_territory),
4016                    estimated_strength: EstimateRange {
4017                        minimum: 8_000,
4018                        maximum: 8_000,
4019                    },
4020                    observed_at: initial_time,
4021                    learned_at: initial_time,
4022                    confidence_per_mille: 1000,
4023                    source: KnowledgeSource::CommandResponsibility,
4024                },
4025            )]),
4026        },
4027    );
4028    knowledge.actors.insert(
4029        ids.observer,
4030        ActorKnowledge {
4031            actor: ids.observer,
4032            armies: BTreeMap::from([(
4033                ids.army,
4034                ArmyKnowledge {
4035                    army: ids.army,
4036                    known_name: Some("First Field Army".to_owned()),
4037                    known_location: Some(ids.central_territory),
4038                    estimated_strength: EstimateRange {
4039                        minimum: 7_000,
4040                        maximum: 9_000,
4041                    },
4042                    observed_at: initial_time,
4043                    learned_at: initial_time,
4044                    confidence_per_mille: 700,
4045                    source: KnowledgeSource::ScenarioRecord,
4046                },
4047            )]),
4048        },
4049    );
4050    (
4051        Scenario {
4052            start_time: initial_time,
4053            world,
4054            knowledge,
4055            domain_records: Vec::new(),
4056        },
4057        ids,
4058    )
4059}
4060
4061#[cfg(test)]
4062mod tests {
4063    #![allow(clippy::unnecessary_wraps)]
4064
4065    use super::*;
4066
4067    #[derive(Debug, Eq, PartialEq)]
4068    struct CacheFingerprint {
4069        journals: [String; 5],
4070        domains: [Option<String>; 7],
4071    }
4072
4073    fn cache_fingerprint(simulation: &Simulation) -> CacheFingerprint {
4074        let cache = simulation
4075            .state
4076            .metadata
4077            .commitment_cache
4078            .as_ref()
4079            .expect("current runtimes should maintain a commitment cache");
4080        CacheFingerprint {
4081            journals: [
4082                cache.commands.root(),
4083                cache.attempts.root(),
4084                cache.events.root(),
4085                cache.ingress.root(),
4086                cache.random_draws.root(),
4087            ],
4088            domains: [
4089                cache.world.clone(),
4090                cache.knowledge.clone(),
4091                cache.plugin_components.clone(),
4092                cache.domain_records.clone(),
4093                cache.scheduler.clone(),
4094                cache.random_streams.clone(),
4095                cache.identity.clone(),
4096            ],
4097        }
4098    }
4099
4100    macro_rules! test_plugin_identity {
4101        ($hash:literal) => {
4102            fn version(&self) -> &'static str {
4103                "test-v1"
4104            }
4105
4106            fn semantic_hash(&self) -> &'static str {
4107                $hash
4108            }
4109        };
4110    }
4111
4112    struct AuthorityPlugin;
4113    struct ChangedAuthorityPlugin;
4114
4115    fn authority_command(
4116        view: &SimulationView<'_>,
4117        context: &CommandContext,
4118        _payload: &Value,
4119    ) -> Result<Vec<SystemDirective>, CanwuError> {
4120        let actor = PersonId::new(1);
4121        let army = ArmyId::new(1);
4122        if context.issuer != Issuer::Actor(actor) {
4123            return Err(CanwuError::new(
4124                ErrorCode::InvalidAuthority,
4125                "the command issuer does not own this test action",
4126            ));
4127        }
4128        if view.army(army)?.is_none() {
4129            return Err(CanwuError::new(
4130                ErrorCode::ArmyNotFound,
4131                "the test army does not exist",
4132            ));
4133        }
4134        Ok(vec![SystemDirective::SetComponent {
4135            state: StateKey::new("military", "stance"),
4136            entity: EntityRef::Army(army),
4137            component: "stance".to_owned(),
4138            value: Value::String("hold".to_owned()),
4139            summary: "The authorized actor changed the army stance".to_owned(),
4140        }])
4141    }
4142
4143    fn register_authority(registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4144        registrar.register_command(
4145            PluginActionDescriptor {
4146                name: "set_stance".to_owned(),
4147                description: "Set a test stance".to_owned(),
4148                payload_schema: PayloadSchema::Null,
4149                reads: vec![StateKey::core_armies()],
4150                writes: vec![StateKey::new("military", "stance")],
4151            },
4152            authority_command,
4153        )
4154    }
4155
4156    impl SimulationPlugin for AuthorityPlugin {
4157        fn name(&self) -> &'static str {
4158            "authority-test"
4159        }
4160
4161        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000001");
4162
4163        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4164            register_authority(registrar)
4165        }
4166    }
4167
4168    impl SimulationPlugin for ChangedAuthorityPlugin {
4169        fn name(&self) -> &'static str {
4170            "authority-test"
4171        }
4172
4173        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000013");
4174
4175        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4176            register_authority(registrar)
4177        }
4178    }
4179
4180    struct MarkerPlugin {
4181        name: &'static str,
4182        writes: Vec<StateKey>,
4183    }
4184
4185    fn marker_system(
4186        _view: &SimulationView<'_>,
4187        event: &SimEvent,
4188    ) -> Result<Vec<SystemDirective>, CanwuError> {
4189        if !matches!(event.kind, EventKind::MoveOrdered { .. }) {
4190            return Ok(Vec::new());
4191        }
4192        Ok(vec![SystemDirective::Emit {
4193            event_type: "marker".to_owned(),
4194            summary: "movement marker".to_owned(),
4195            affected: Vec::new(),
4196        }])
4197    }
4198
4199    impl SimulationPlugin for MarkerPlugin {
4200        fn name(&self) -> &str {
4201            self.name
4202        }
4203
4204        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000002");
4205
4206        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4207            let mut contract = SystemContract::event_driven(
4208                "movement-marker",
4209                BoundaryPhase::PerspectiveAndReportMaterialization,
4210            );
4211            contract.writes.clone_from(&self.writes);
4212            registrar.register_system(contract, marker_system)
4213        }
4214    }
4215
4216    struct RecursivePlugin;
4217
4218    fn recursive_system(
4219        _view: &SimulationView<'_>,
4220        event: &SimEvent,
4221    ) -> Result<Vec<SystemDirective>, CanwuError> {
4222        let should_recurse = match &event.kind {
4223            EventKind::MoveOrdered { .. } => true,
4224            EventKind::Plugin { plugin, event_type } => {
4225                plugin == "recursive-test" && event_type == "loop"
4226            }
4227            _ => false,
4228        };
4229        if should_recurse {
4230            Ok(vec![SystemDirective::Emit {
4231                event_type: "loop".to_owned(),
4232                summary: "recursive compatibility event".to_owned(),
4233                affected: Vec::new(),
4234            }])
4235        } else {
4236            Ok(Vec::new())
4237        }
4238    }
4239
4240    impl SimulationPlugin for RecursivePlugin {
4241        fn name(&self) -> &'static str {
4242            "recursive-test"
4243        }
4244
4245        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000004");
4246
4247        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4248            registrar.register_system(
4249                SystemContract::event_driven(
4250                    "recursive-reactor",
4251                    BoundaryPhase::PerspectiveAndReportMaterialization,
4252                ),
4253                recursive_system,
4254            )
4255        }
4256    }
4257
4258    struct FailingPlugin;
4259
4260    fn failing_command(
4261        _view: &SimulationView<'_>,
4262        _context: &CommandContext,
4263        payload: &Value,
4264    ) -> Result<Vec<SystemDirective>, CanwuError> {
4265        let mutation = SystemDirective::SetComponent {
4266            state: StateKey::new("failure-fixture", "flag"),
4267            entity: EntityRef::Army(ArmyId::new(1)),
4268            component: "flag".to_owned(),
4269            value: Value::Bool(true),
4270            summary: "Set a flag before the injected failure".to_owned(),
4271        };
4272        if payload.get("scheduled").and_then(Value::as_bool) == Some(true) {
4273            Ok(vec![SystemDirective::Schedule {
4274                after: SimDuration::days(1),
4275                directive: Box::new(mutation),
4276            }])
4277        } else {
4278            Ok(vec![mutation])
4279        }
4280    }
4281
4282    fn failing_event_system(
4283        _view: &SimulationView<'_>,
4284        event: &SimEvent,
4285    ) -> Result<Vec<SystemDirective>, CanwuError> {
4286        if matches!(
4287            &event.kind,
4288            EventKind::Plugin { plugin, event_type }
4289                if plugin == "failing-test" && event_type == "flag_changed"
4290        ) {
4291            Ok(vec![SystemDirective::Schedule {
4292                after: SimDuration::ZERO,
4293                directive: Box::new(SystemDirective::Emit {
4294                    event_type: "unreachable".to_owned(),
4295                    summary: "This directive must be rejected".to_owned(),
4296                    affected: Vec::new(),
4297                }),
4298            }])
4299        } else {
4300            Ok(Vec::new())
4301        }
4302    }
4303
4304    fn panicking_command(
4305        _view: &SimulationView<'_>,
4306        _context: &CommandContext,
4307        _payload: &Value,
4308    ) -> Result<Vec<SystemDirective>, CanwuError> {
4309        panic!("injected plugin panic")
4310    }
4311
4312    impl SimulationPlugin for FailingPlugin {
4313        fn name(&self) -> &'static str {
4314            "failing-test"
4315        }
4316
4317        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000003");
4318
4319        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4320            registrar.register_system(
4321                SystemContract::event_driven(
4322                    "reject-flag-event",
4323                    BoundaryPhase::InvariantValidation,
4324                ),
4325                failing_event_system,
4326            )?;
4327            registrar.register_command(
4328                PluginActionDescriptor {
4329                    name: "mutate".to_owned(),
4330                    description: "Exercise transactional rollback".to_owned(),
4331                    payload_schema: PayloadSchema::Object {
4332                        properties: BTreeMap::from([(
4333                            "scheduled".to_owned(),
4334                            PayloadProperty {
4335                                value_type: PayloadValueType::Boolean,
4336                                required: true,
4337                            },
4338                        )]),
4339                        allow_additional: false,
4340                    },
4341                    reads: Vec::new(),
4342                    writes: vec![StateKey::new("failure-fixture", "flag")],
4343                },
4344                failing_command,
4345            )?;
4346            registrar.register_command(
4347                PluginActionDescriptor {
4348                    name: "panic".to_owned(),
4349                    description: "Exercise the plugin panic boundary".to_owned(),
4350                    payload_schema: PayloadSchema::Null,
4351                    reads: Vec::new(),
4352                    writes: Vec::new(),
4353                },
4354                panicking_command,
4355            )
4356        }
4357    }
4358
4359    fn no_op_command(
4360        _view: &SimulationView<'_>,
4361        _context: &CommandContext,
4362        _payload: &Value,
4363    ) -> Result<Vec<SystemDirective>, CanwuError> {
4364        Ok(Vec::new())
4365    }
4366
4367    fn no_op_boundary(
4368        _view: &SimulationView<'_>,
4369        _context: &BoundaryContext,
4370    ) -> Result<BoundaryProposal, CanwuError> {
4371        Ok(BoundaryProposal::default())
4372    }
4373
4374    struct JournalCommandPlugin;
4375
4376    impl SimulationPlugin for JournalCommandPlugin {
4377        fn name(&self) -> &'static str {
4378            "journal-command"
4379        }
4380
4381        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000004");
4382
4383        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4384            registrar.register_command(
4385                PluginActionDescriptor {
4386                    name: "noop".to_owned(),
4387                    description: "Append deterministic command evidence".to_owned(),
4388                    payload_schema: PayloadSchema::Null,
4389                    reads: Vec::new(),
4390                    writes: Vec::new(),
4391                },
4392                no_op_command,
4393            )
4394        }
4395    }
4396
4397    fn emit_archive_probe(
4398        _view: &SimulationView<'_>,
4399        _context: &BoundaryContext,
4400    ) -> Result<BoundaryProposal, CanwuError> {
4401        Ok(BoundaryProposal {
4402            directives: vec![BoundaryDirective::Emit {
4403                event_type: "archive_probe".to_owned(),
4404                summary: "Emit evidence across the archive admission frontier".to_owned(),
4405                affected: vec![EntityRef::Person(PersonId::new(1))],
4406            }],
4407            ..BoundaryProposal::default()
4408        })
4409    }
4410
4411    struct ArchiveEmissionPlugin;
4412
4413    impl SimulationPlugin for ArchiveEmissionPlugin {
4414        fn name(&self) -> &'static str {
4415            "archive-emission"
4416        }
4417
4418        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000031");
4419
4420        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4421            let mut contract = BoundarySystemContract::new(
4422                "emit",
4423                BoundaryPhase::DomainDeltaProposal,
4424                SystemCadence::Daily,
4425            );
4426            contract.emits = vec!["archive_probe".to_owned()];
4427            registrar.register_boundary_system(contract, emit_archive_probe)
4428        }
4429    }
4430
4431    struct BoundaryGhostPlugin;
4432
4433    impl SimulationPlugin for BoundaryGhostPlugin {
4434        fn name(&self) -> &'static str {
4435            "boundary-ghost-test"
4436        }
4437
4438        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000005");
4439
4440        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4441            registrar.register_command(
4442                PluginActionDescriptor {
4443                    name: "seed".to_owned(),
4444                    description: "Own immediate state for the conflict fixture".to_owned(),
4445                    payload_schema: PayloadSchema::Null,
4446                    reads: Vec::new(),
4447                    writes: vec![StateKey::new("boundary-conflict", "immediate")],
4448                },
4449                no_op_command,
4450            )?;
4451            let mut rejected = BoundarySystemContract::new(
4452                "rejected",
4453                BoundaryPhase::DomainDeltaProposal,
4454                SystemCadence::Daily,
4455            );
4456            rejected.writes = vec![
4457                StateKey::new("boundary-conflict", "immediate"),
4458                StateKey::new("boundary-ghost", "value"),
4459            ];
4460            if registrar
4461                .register_boundary_system(rejected, no_op_boundary)
4462                .is_ok()
4463            {
4464                return Err(CanwuError::new(
4465                    ErrorCode::InvalidPluginRegistration,
4466                    "the boundary ghost fixture expected a writer-mode conflict",
4467                ));
4468            }
4469            Ok(())
4470        }
4471    }
4472
4473    struct GhostPlugin;
4474
4475    impl SimulationPlugin for GhostPlugin {
4476        fn name(&self) -> &'static str {
4477            "ghost-test"
4478        }
4479
4480        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000006");
4481
4482        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4483            let ignored = registrar.register_command(
4484                PluginActionDescriptor {
4485                    name: "ignored".to_owned(),
4486                    description: "A deliberately rejected registration".to_owned(),
4487                    payload_schema: PayloadSchema::Null,
4488                    reads: Vec::new(),
4489                    writes: vec![
4490                        StateKey::new("fresh-domain", "value"),
4491                        StateKey::new("shared-domain", "balance"),
4492                    ],
4493                },
4494                no_op_command,
4495            );
4496            if ignored.is_ok() {
4497                return Err(CanwuError::new(
4498                    ErrorCode::InvalidPluginRegistration,
4499                    "the ghost fixture expected an ownership conflict",
4500                ));
4501            }
4502            Ok(())
4503        }
4504    }
4505
4506    fn seed_secret(
4507        _view: &SimulationView<'_>,
4508        _context: &CommandContext,
4509        _payload: &Value,
4510    ) -> Result<Vec<SystemDirective>, CanwuError> {
4511        Ok(vec![SystemDirective::SetComponent {
4512            state: StateKey::new("secret-domain", "value"),
4513            entity: EntityRef::Army(ArmyId::new(1)),
4514            component: "value".to_owned(),
4515            value: Value::String("classified".to_owned()),
4516            summary: "Seed classified state".to_owned(),
4517        }])
4518    }
4519
4520    struct SecretPlugin;
4521
4522    impl SimulationPlugin for SecretPlugin {
4523        fn name(&self) -> &'static str {
4524            "secret-owner"
4525        }
4526
4527        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000007");
4528
4529        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4530            registrar.register_command(
4531                PluginActionDescriptor {
4532                    name: "seed".to_owned(),
4533                    description: "Seed owned state".to_owned(),
4534                    payload_schema: PayloadSchema::Null,
4535                    reads: Vec::new(),
4536                    writes: vec![StateKey::new("secret-domain", "value")],
4537                },
4538                seed_secret,
4539            )
4540        }
4541    }
4542
4543    fn undeclared_read(
4544        view: &SimulationView<'_>,
4545        _context: &CommandContext,
4546        _payload: &Value,
4547    ) -> Result<Vec<SystemDirective>, CanwuError> {
4548        let _ = view.component(
4549            &StateKey::new("secret-domain", "value"),
4550            &EntityRef::Army(ArmyId::new(1)),
4551            "value",
4552        )?;
4553        Ok(Vec::new())
4554    }
4555
4556    fn undeclared_write(
4557        _view: &SimulationView<'_>,
4558        _context: &CommandContext,
4559        _payload: &Value,
4560    ) -> Result<Vec<SystemDirective>, CanwuError> {
4561        Ok(vec![SystemDirective::SetComponent {
4562            state: StateKey::new("secret-domain", "value"),
4563            entity: EntityRef::Army(ArmyId::new(1)),
4564            component: "value".to_owned(),
4565            value: Value::String("overwritten".to_owned()),
4566            summary: "Attempt an undeclared write".to_owned(),
4567        }])
4568    }
4569
4570    fn missing_entity_write(
4571        _view: &SimulationView<'_>,
4572        _context: &CommandContext,
4573        _payload: &Value,
4574    ) -> Result<Vec<SystemDirective>, CanwuError> {
4575        Ok(vec![SystemDirective::SetComponent {
4576            state: StateKey::new("access-domain", "declared"),
4577            entity: EntityRef::Army(ArmyId::new(999)),
4578            component: "declared".to_owned(),
4579            value: Value::Bool(true),
4580            summary: "Attempt to write state for a missing entity".to_owned(),
4581        }])
4582    }
4583
4584    struct UndeclaredAccessPlugin;
4585
4586    impl SimulationPlugin for UndeclaredAccessPlugin {
4587        fn name(&self) -> &'static str {
4588            "undeclared-access"
4589        }
4590
4591        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000008");
4592
4593        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4594            registrar.register_command(
4595                PluginActionDescriptor {
4596                    name: "missing".to_owned(),
4597                    description: "Attempt to target a missing entity".to_owned(),
4598                    payload_schema: PayloadSchema::Null,
4599                    reads: Vec::new(),
4600                    writes: vec![StateKey::new("access-domain", "declared")],
4601                },
4602                missing_entity_write,
4603            )?;
4604            registrar.register_command(
4605                PluginActionDescriptor {
4606                    name: "read".to_owned(),
4607                    description: "Attempt an undeclared read".to_owned(),
4608                    payload_schema: PayloadSchema::Null,
4609                    reads: Vec::new(),
4610                    writes: Vec::new(),
4611                },
4612                undeclared_read,
4613            )?;
4614            registrar.register_command(
4615                PluginActionDescriptor {
4616                    name: "write".to_owned(),
4617                    description: "Attempt an undeclared write".to_owned(),
4618                    payload_schema: PayloadSchema::Null,
4619                    reads: Vec::new(),
4620                    writes: vec![StateKey::new("access-domain", "declared")],
4621                },
4622                undeclared_write,
4623            )
4624        }
4625    }
4626
4627    fn collision_a(
4628        _view: &SimulationView<'_>,
4629        _context: &CommandContext,
4630        _payload: &Value,
4631    ) -> Result<Vec<SystemDirective>, CanwuError> {
4632        Ok(vec![SystemDirective::SetComponent {
4633            state: StateKey::new("collision-a", "b/person:1/c"),
4634            entity: EntityRef::Person(PersonId::new(1)),
4635            component: "b/person:1/c".to_owned(),
4636            value: Value::String("first".to_owned()),
4637            summary: "Write the first adversarial key".to_owned(),
4638        }])
4639    }
4640
4641    fn collision_b(
4642        _view: &SimulationView<'_>,
4643        _context: &CommandContext,
4644        _payload: &Value,
4645    ) -> Result<Vec<SystemDirective>, CanwuError> {
4646        Ok(vec![SystemDirective::SetComponent {
4647            state: StateKey::new("collision-b", "c"),
4648            entity: EntityRef::Person(PersonId::new(1)),
4649            component: "c".to_owned(),
4650            value: Value::String("second".to_owned()),
4651            summary: "Write the second adversarial key".to_owned(),
4652        }])
4653    }
4654
4655    struct CollisionPluginA;
4656
4657    struct CollisionPluginB;
4658
4659    impl SimulationPlugin for CollisionPluginA {
4660        fn name(&self) -> &'static str {
4661            "a"
4662        }
4663
4664        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000009");
4665
4666        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4667            registrar.register_command(
4668                PluginActionDescriptor {
4669                    name: "write".to_owned(),
4670                    description: "Write an adversarial component key".to_owned(),
4671                    payload_schema: PayloadSchema::Null,
4672                    reads: Vec::new(),
4673                    writes: vec![StateKey::new("collision-a", "b/person:1/c")],
4674                },
4675                collision_a,
4676            )
4677        }
4678    }
4679
4680    impl SimulationPlugin for CollisionPluginB {
4681        fn name(&self) -> &'static str {
4682            "a/person:1/b"
4683        }
4684
4685        test_plugin_identity!("000000000000000000000000000000000000000000000000000000000000000a");
4686
4687        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4688            registrar.register_command(
4689                PluginActionDescriptor {
4690                    name: "write".to_owned(),
4691                    description: "Write a second adversarial component key".to_owned(),
4692                    payload_schema: PayloadSchema::Null,
4693                    reads: Vec::new(),
4694                    writes: vec![StateKey::new("collision-b", "c")],
4695                },
4696                collision_b,
4697            )
4698        }
4699    }
4700
4701    fn grain_pool() -> ReservationPoolKey {
4702        ReservationPoolKey::new(
4703            StateKey::new("logistics", "grain"),
4704            EntityRef::Territory(TerritoryId::new(1)),
4705            "grain",
4706        )
4707    }
4708
4709    fn primary_random_stream() -> RandomStreamKey {
4710        RandomStreamKey::new("random-primary", "daily-roll", 1)
4711    }
4712
4713    fn noise_random_stream() -> RandomStreamKey {
4714        RandomStreamKey::new("random-noise", "daily-noise", 1)
4715    }
4716
4717    fn failure_random_stream() -> RandomStreamKey {
4718        RandomStreamKey::new("boundary-rollback", "rollback-proof", 1)
4719    }
4720
4721    fn roll_primary(
4722        view: &SimulationView<'_>,
4723        _context: &BoundaryContext,
4724    ) -> Result<BoundaryProposal, CanwuError> {
4725        let roll = view.random_range(&primary_random_stream(), 100, "daily primary roll")?;
4726        Ok(BoundaryProposal {
4727            directives: vec![BoundaryDirective::SetComponent {
4728                state: StateKey::new("random-primary", "roll"),
4729                entity: EntityRef::Territory(TerritoryId::new(1)),
4730                component: "value".to_owned(),
4731                value: Value::from(roll),
4732                summary: format!("Primary random stream rolled {roll}"),
4733            }],
4734            ..BoundaryProposal::default()
4735        })
4736    }
4737
4738    fn draw_noise(
4739        view: &SimulationView<'_>,
4740        _context: &BoundaryContext,
4741    ) -> Result<BoundaryProposal, CanwuError> {
4742        let _ = view.random_range(&noise_random_stream(), 10_000, "unrelated daily noise")?;
4743        Ok(BoundaryProposal::default())
4744    }
4745
4746    struct PrimaryRandomPlugin;
4747    struct ChangedPrimaryRandomPlugin;
4748    struct NoiseRandomPlugin;
4749
4750    fn register_primary_random(registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4751        let mut contract = BoundarySystemContract::new(
4752            "roll",
4753            BoundaryPhase::DomainDeltaProposal,
4754            SystemCadence::Daily,
4755        );
4756        contract.writes = vec![StateKey::new("random-primary", "roll")];
4757        contract.random_streams = vec![primary_random_stream()];
4758        registrar.register_boundary_system(contract, roll_primary)
4759    }
4760
4761    impl SimulationPlugin for PrimaryRandomPlugin {
4762        fn name(&self) -> &'static str {
4763            "random-primary"
4764        }
4765
4766        test_plugin_identity!("000000000000000000000000000000000000000000000000000000000000000b");
4767
4768        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4769            register_primary_random(registrar)
4770        }
4771    }
4772
4773    impl SimulationPlugin for ChangedPrimaryRandomPlugin {
4774        fn name(&self) -> &'static str {
4775            "random-primary"
4776        }
4777
4778        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000012");
4779
4780        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4781            register_primary_random(registrar)
4782        }
4783    }
4784
4785    impl SimulationPlugin for NoiseRandomPlugin {
4786        fn name(&self) -> &'static str {
4787            "random-noise"
4788        }
4789
4790        test_plugin_identity!("000000000000000000000000000000000000000000000000000000000000000c");
4791
4792        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4793            let mut contract = BoundarySystemContract::new(
4794                "draw",
4795                BoundaryPhase::DerivedFieldSolve,
4796                SystemCadence::Daily,
4797            );
4798            contract.random_streams = vec![noise_random_stream()];
4799            registrar.register_boundary_system(contract, draw_noise)
4800        }
4801    }
4802
4803    fn offer_grain(
4804        _view: &SimulationView<'_>,
4805        _context: &BoundaryContext,
4806    ) -> Result<BoundaryProposal, CanwuError> {
4807        Ok(BoundaryProposal {
4808            offers: vec![ReservationOffer {
4809                pool: grain_pool(),
4810                capacity: 10,
4811            }],
4812            ..BoundaryProposal::default()
4813        })
4814    }
4815
4816    fn high_request(
4817        _view: &SimulationView<'_>,
4818        _context: &BoundaryContext,
4819    ) -> Result<BoundaryProposal, CanwuError> {
4820        Ok(BoundaryProposal {
4821            requests: vec![ReservationRequest {
4822                request: "grain".to_owned(),
4823                pool: grain_pool(),
4824                quantity: 7,
4825                priority: 10,
4826                tie_break: "high".to_owned(),
4827            }],
4828            ..BoundaryProposal::default()
4829        })
4830    }
4831
4832    fn low_request(
4833        _view: &SimulationView<'_>,
4834        _context: &BoundaryContext,
4835    ) -> Result<BoundaryProposal, CanwuError> {
4836        Ok(BoundaryProposal {
4837            requests: vec![ReservationRequest {
4838                request: "grain".to_owned(),
4839                pool: grain_pool(),
4840                quantity: 7,
4841                priority: 0,
4842                tie_break: "low".to_owned(),
4843            }],
4844            ..BoundaryProposal::default()
4845        })
4846    }
4847
4848    fn record_grant(
4849        view: &SimulationView<'_>,
4850        context: &BoundaryContext,
4851        plugin: &str,
4852        state: StateKey,
4853        component: &str,
4854    ) -> Result<BoundaryProposal, CanwuError> {
4855        let reservation = ReservationRef::new(plugin, "request", "grain");
4856        let allocation = view.reservation(&reservation)?.ok_or_else(|| {
4857            CanwuError::new(
4858                ErrorCode::InvalidBoundary,
4859                format!(
4860                    "{} could not find allocation {reservation:?}",
4861                    context.system
4862                ),
4863            )
4864        })?;
4865        Ok(BoundaryProposal {
4866            directives: vec![BoundaryDirective::SetComponent {
4867                state,
4868                entity: EntityRef::Territory(TerritoryId::new(1)),
4869                component: component.to_owned(),
4870                value: Value::from(allocation.granted),
4871                summary: format!("Recorded a grant of {} grain", allocation.granted),
4872            }],
4873            ..BoundaryProposal::default()
4874        })
4875    }
4876
4877    fn record_high_grant(
4878        view: &SimulationView<'_>,
4879        context: &BoundaryContext,
4880    ) -> Result<BoundaryProposal, CanwuError> {
4881        record_grant(
4882            view,
4883            context,
4884            "high-claim",
4885            StateKey::new("allocation", "high"),
4886            "high",
4887        )
4888    }
4889
4890    fn record_low_grant(
4891        view: &SimulationView<'_>,
4892        context: &BoundaryContext,
4893    ) -> Result<BoundaryProposal, CanwuError> {
4894        record_grant(
4895            view,
4896            context,
4897            "low-claim",
4898            StateKey::new("allocation", "low"),
4899            "low",
4900        )
4901    }
4902
4903    fn validate_visibility(
4904        view: &SimulationView<'_>,
4905        context: &BoundaryContext,
4906    ) -> Result<BoundaryProposal, CanwuError> {
4907        let entity = EntityRef::Territory(TerritoryId::new(1));
4908        let high = view
4909            .component(&StateKey::new("allocation", "high"), &entity, "high")?
4910            .and_then(Value::as_u64);
4911        let low = view
4912            .component(&StateKey::new("allocation", "low"), &entity, "low")?
4913            .and_then(Value::as_u64);
4914        let proposed_high = view
4915            .proposed_component(&StateKey::new("allocation", "high"), &entity, "high")?
4916            .and_then(Value::as_u64);
4917        let proposed_low = view
4918            .proposed_component(&StateKey::new("allocation", "low"), &entity, "low")?
4919            .and_then(Value::as_u64);
4920        let expected_current_low = (context.boundary_id.get() > 1).then_some(3);
4921        if high != Some(7)
4922            || low != expected_current_low
4923            || proposed_high != Some(7)
4924            || proposed_low != Some(3)
4925        {
4926            return Err(CanwuError::new(
4927                ErrorCode::InvalidBoundary,
4928                "validators must see all proposals without exposing next-boundary state as current",
4929            ));
4930        }
4931        Ok(BoundaryProposal::default())
4932    }
4933
4934    struct GrainSupplyPlugin;
4935    struct HighClaimPlugin;
4936    struct LowClaimPlugin;
4937    struct VisibilityValidatorPlugin;
4938
4939    impl SimulationPlugin for GrainSupplyPlugin {
4940        fn name(&self) -> &'static str {
4941            "grain-supply"
4942        }
4943
4944        test_plugin_identity!("000000000000000000000000000000000000000000000000000000000000000d");
4945
4946        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4947            let mut contract = BoundarySystemContract::new(
4948                "offer",
4949                BoundaryPhase::ReservationAndAllocation,
4950                SystemCadence::Daily,
4951            );
4952            contract.reservation_offers = vec![StateKey::new("logistics", "grain")];
4953            registrar.register_boundary_system(contract, offer_grain)
4954        }
4955    }
4956
4957    impl SimulationPlugin for HighClaimPlugin {
4958        fn name(&self) -> &'static str {
4959            "high-claim"
4960        }
4961
4962        test_plugin_identity!("000000000000000000000000000000000000000000000000000000000000000e");
4963
4964        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4965            let mut request = BoundarySystemContract::new(
4966                "request",
4967                BoundaryPhase::ReservationAndAllocation,
4968                SystemCadence::Daily,
4969            );
4970            request.reservation_requests = vec![StateKey::new("logistics", "grain")];
4971            registrar.register_boundary_system(request, high_request)?;
4972            let mut apply = BoundarySystemContract::new(
4973                "apply",
4974                BoundaryPhase::DomainDeltaProposal,
4975                SystemCadence::Daily,
4976            );
4977            apply.writes = vec![StateKey::new("allocation", "high")];
4978            apply.reservation_reads = vec![ReservationRef::new("high-claim", "request", "grain")];
4979            apply.visibility = StateVisibility::SameBoundary;
4980            registrar.register_boundary_system(apply, record_high_grant)
4981        }
4982    }
4983
4984    impl SimulationPlugin for LowClaimPlugin {
4985        fn name(&self) -> &'static str {
4986            "low-claim"
4987        }
4988
4989        test_plugin_identity!("000000000000000000000000000000000000000000000000000000000000000f");
4990
4991        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
4992            let mut request = BoundarySystemContract::new(
4993                "request",
4994                BoundaryPhase::ReservationAndAllocation,
4995                SystemCadence::Daily,
4996            );
4997            request.reservation_requests = vec![StateKey::new("logistics", "grain")];
4998            registrar.register_boundary_system(request, low_request)?;
4999            let mut apply = BoundarySystemContract::new(
5000                "apply",
5001                BoundaryPhase::DomainDeltaProposal,
5002                SystemCadence::Daily,
5003            );
5004            apply.writes = vec![StateKey::new("allocation", "low")];
5005            apply.reservation_reads = vec![ReservationRef::new("low-claim", "request", "grain")];
5006            registrar.register_boundary_system(apply, record_low_grant)
5007        }
5008    }
5009
5010    impl SimulationPlugin for VisibilityValidatorPlugin {
5011        fn name(&self) -> &'static str {
5012            "visibility-validator"
5013        }
5014
5015        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000010");
5016
5017        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
5018            let mut contract = BoundarySystemContract::new(
5019                "validate",
5020                BoundaryPhase::InvariantValidation,
5021                SystemCadence::Daily,
5022            );
5023            contract.reads = vec![
5024                StateKey::new("allocation", "high"),
5025                StateKey::new("allocation", "low"),
5026            ];
5027            registrar.register_boundary_system(contract, validate_visibility)
5028        }
5029    }
5030
5031    fn stage_boundary_rollback_mutations(
5032        view: &SimulationView<'_>,
5033        context: &BoundaryContext,
5034    ) -> Result<BoundaryProposal, CanwuError> {
5035        if context.boundary_id.get() != 2 {
5036            return Ok(BoundaryProposal::default());
5037        }
5038        let _ = view.random_range(&failure_random_stream(), 100, "rollback proof")?;
5039        Ok(BoundaryProposal {
5040            directives: vec![
5041                BoundaryDirective::SetComponent {
5042                    state: StateKey::new("boundary-rollback", "value"),
5043                    entity: EntityRef::Army(ArmyId::new(1)),
5044                    component: "value".to_owned(),
5045                    value: Value::Bool(true),
5046                    summary: "Stage a value before transaction failure".to_owned(),
5047                },
5048                BoundaryDirective::ScheduleIngress {
5049                    after: SimDuration::hours(1),
5050                    packet_type: "follow-up".to_owned(),
5051                    priority: 0,
5052                    payload: serde_json::json!({ "label": "rollback proof" }),
5053                    affected: vec![EntityRef::Army(ArmyId::new(1))],
5054                },
5055            ],
5056            ..BoundaryProposal::default()
5057        })
5058    }
5059
5060    struct BoundaryRollbackPlugin;
5061
5062    impl SimulationPlugin for BoundaryRollbackPlugin {
5063        fn name(&self) -> &'static str {
5064            "boundary-rollback"
5065        }
5066
5067        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000011");
5068
5069        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
5070            registrar.register_ingress(PluginIngressDescriptor {
5071                name: "follow-up".to_owned(),
5072                description: "A rollback fixture packet".to_owned(),
5073                class: IngressClass::Information,
5074                payload_schema: object_payload_schema("label"),
5075            })?;
5076            let mut propose = BoundarySystemContract::new(
5077                "propose",
5078                BoundaryPhase::DomainDeltaProposal,
5079                SystemCadence::Daily,
5080            );
5081            propose.writes = vec![StateKey::new("boundary-rollback", "value")];
5082            propose.random_streams = vec![failure_random_stream()];
5083            propose.visibility = StateVisibility::SameBoundary;
5084            registrar.register_boundary_system(propose, stage_boundary_rollback_mutations)
5085        }
5086    }
5087
5088    struct RecordLifecyclePlugin;
5089    struct RecordDeleteOnlyPlugin;
5090    struct RecordCyclePlugin;
5091    struct RecordSeatDeletionPlugin;
5092
5093    fn office_kind() -> DomainRecordKind {
5094        DomainRecordKind::new("fixture.governance", "office")
5095    }
5096
5097    fn obligation_kind() -> DomainRecordKind {
5098        DomainRecordKind::new("fixture.governance", "obligation")
5099    }
5100
5101    fn office_reference(id: &str) -> DomainRecordRef {
5102        DomainRecordRef::new("fixture.governance", "office", id)
5103    }
5104
5105    fn obligation_reference() -> DomainRecordRef {
5106        DomainRecordRef::new("fixture.governance", "obligation", "standing-order")
5107    }
5108
5109    fn object_payload_schema(field: &str) -> PayloadSchema {
5110        PayloadSchema::Object {
5111            properties: BTreeMap::from([(
5112                field.to_owned(),
5113                PayloadProperty {
5114                    value_type: PayloadValueType::String,
5115                    required: true,
5116                },
5117            )]),
5118            allow_additional: false,
5119        }
5120    }
5121
5122    fn office_draft(id: &str, name: &str) -> DomainRecordDraft {
5123        DomainRecordDraft {
5124            reference: office_reference(id),
5125            payload: serde_json::json!({ "name": name }),
5126            references: vec![DomainReference {
5127                role: "holder".to_owned(),
5128                target: DomainReferenceTarget::Core(EntityRef::Person(PersonId::new(1))),
5129            }],
5130        }
5131    }
5132
5133    fn obligation_draft(office: &str, status: &str) -> DomainRecordDraft {
5134        DomainRecordDraft {
5135            reference: obligation_reference(),
5136            payload: serde_json::json!({ "status": status }),
5137            references: vec![DomainReference {
5138                role: "office".to_owned(),
5139                target: DomainReferenceTarget::Domain(office_reference(office)),
5140            }],
5141        }
5142    }
5143
5144    fn initial_record(
5145        owner: &str,
5146        class: DomainRecordClass,
5147        draft: DomainRecordDraft,
5148    ) -> DomainRecord {
5149        DomainRecord {
5150            reference: draft.reference,
5151            owner: owner.to_owned(),
5152            class,
5153            version: 1,
5154            lifecycle: DomainRecordLifecycle::Active,
5155            payload: draft.payload,
5156            references: draft.references,
5157        }
5158    }
5159
5160    fn rehash_tampered_snapshot(snapshot: &mut SimulationSnapshot) {
5161        let mut previous_hash = GENESIS_BOUNDARY_HASH.to_owned();
5162        for boundary in &mut snapshot.boundaries {
5163            boundary.previous_hash.clone_from(&previous_hash);
5164            boundary.hash =
5165                compute_boundary_hash(boundary).expect("tampered boundary should still hash");
5166            previous_hash.clone_from(&boundary.hash);
5167        }
5168        refresh_snapshot_commitments_and_checkpoint(snapshot);
5169    }
5170
5171    fn refresh_snapshot_commitments_and_checkpoint(snapshot: &mut SimulationSnapshot) {
5172        if snapshot.commitment_format_version == COMMITMENT_FORMAT_VERSION {
5173            snapshot.commitment_roots = Some(
5174                snapshot_commitment_roots(snapshot)
5175                    .expect("snapshot domains should produce commitment roots"),
5176            );
5177        }
5178        snapshot.checkpoint_hash = snapshot_checkpoint_hash(snapshot)
5179            .expect("tampered snapshot should still have a coherent outer commitment");
5180    }
5181
5182    fn downgrade_snapshot_commitments(snapshot: &mut SimulationSnapshot) {
5183        snapshot.commitment_format_version = 0;
5184        snapshot.commitment_roots = None;
5185    }
5186
5187    fn record_lifecycle_proposal(context: &BoundaryContext, delete_only: bool) -> BoundaryProposal {
5188        let directives = match context.boundary_id.get() {
5189            1 => vec![
5190                BoundaryDirective::MutateRecord {
5191                    mutation: DomainRecordMutation::Create {
5192                        record: office_draft("office-a", "Primary Office"),
5193                    },
5194                    summary: "Create the original office".to_owned(),
5195                },
5196                BoundaryDirective::MutateRecord {
5197                    mutation: DomainRecordMutation::Create {
5198                        record: office_draft("office-b", "Successor Office"),
5199                    },
5200                    summary: "Create the successor office".to_owned(),
5201                },
5202                BoundaryDirective::MutateRecord {
5203                    mutation: DomainRecordMutation::Create {
5204                        record: obligation_draft("office-a", "open"),
5205                    },
5206                    summary: "Create an obligation assigned to the original office".to_owned(),
5207                },
5208                BoundaryDirective::SetComponent {
5209                    state: StateKey::new("fixture.governance", "marker"),
5210                    entity: EntityRef::Domain(office_reference("office-b")),
5211                    component: "status".to_owned(),
5212                    value: Value::String("created".to_owned()),
5213                    summary: "Mark the successor office as created".to_owned(),
5214                },
5215            ],
5216            2 => vec![
5217                BoundaryDirective::MutateRecord {
5218                    mutation: DomainRecordMutation::Create {
5219                        record: office_draft("office-c", "Later Office"),
5220                    },
5221                    summary: "Create the later successor office".to_owned(),
5222                },
5223                BoundaryDirective::MutateRecord {
5224                    mutation: DomainRecordMutation::Retire {
5225                        record: office_reference("office-a"),
5226                        expected_version: 1,
5227                        successor: Some(office_reference("office-b")),
5228                    },
5229                    summary: "Retire the original office with a stable successor".to_owned(),
5230                },
5231            ],
5232            3 if delete_only => vec![BoundaryDirective::MutateRecord {
5233                mutation: DomainRecordMutation::Delete {
5234                    record: office_reference("office-a"),
5235                    expected_version: 2,
5236                },
5237                summary: "Attempt to delete a still-referenced office".to_owned(),
5238            }],
5239            3 => vec![BoundaryDirective::MutateRecord {
5240                mutation: DomainRecordMutation::Retire {
5241                    record: office_reference("office-b"),
5242                    expected_version: 1,
5243                    successor: Some(office_reference("office-c")),
5244                },
5245                summary: "Extend the persisted office succession chain".to_owned(),
5246            }],
5247            4 => vec![
5248                BoundaryDirective::MutateRecord {
5249                    mutation: DomainRecordMutation::Update {
5250                        record: obligation_draft("office-c", "transferred"),
5251                        expected_version: 1,
5252                    },
5253                    summary: "Transfer the obligation to the successor office".to_owned(),
5254                },
5255                BoundaryDirective::MutateRecord {
5256                    mutation: DomainRecordMutation::Delete {
5257                        record: office_reference("office-a"),
5258                        expected_version: 2,
5259                    },
5260                    summary: "Delete the unreferenced retired office".to_owned(),
5261                },
5262            ],
5263            5 => vec![BoundaryDirective::MutateRecord {
5264                mutation: DomainRecordMutation::Update {
5265                    record: office_draft("office-c", "Stale Office"),
5266                    expected_version: 99,
5267                },
5268                summary: "Attempt a stale office update".to_owned(),
5269            }],
5270            _ => Vec::new(),
5271        };
5272        BoundaryProposal {
5273            directives,
5274            ..BoundaryProposal::default()
5275        }
5276    }
5277
5278    fn apply_record_lifecycle(
5279        _view: &SimulationView<'_>,
5280        context: &BoundaryContext,
5281    ) -> Result<BoundaryProposal, CanwuError> {
5282        Ok(record_lifecycle_proposal(context, false))
5283    }
5284
5285    fn apply_invalid_record_delete(
5286        _view: &SimulationView<'_>,
5287        context: &BoundaryContext,
5288    ) -> Result<BoundaryProposal, CanwuError> {
5289        Ok(record_lifecycle_proposal(context, true))
5290    }
5291
5292    fn observe_record_proposal(
5293        _view: &SimulationView<'_>,
5294        context: &BoundaryContext,
5295    ) -> Result<BoundaryProposal, CanwuError> {
5296        let directives = (context.boundary_id.get() == 1)
5297            .then(|| BoundaryDirective::Emit {
5298                event_type: "proposal_probe".to_owned(),
5299                affected: vec![EntityRef::Person(PersonId::new(1))],
5300                summary: "Observe the record proposal boundary".to_owned(),
5301            })
5302            .into_iter()
5303            .collect();
5304        Ok(BoundaryProposal {
5305            directives,
5306            ..BoundaryProposal::default()
5307        })
5308    }
5309
5310    fn validate_record_lifecycle_view(
5311        view: &SimulationView<'_>,
5312        context: &BoundaryContext,
5313    ) -> Result<BoundaryProposal, CanwuError> {
5314        let original = view.domain_record(&office_reference("office-a"))?;
5315        let proposed_successor = view.proposed_domain_record(&office_reference("office-b"))?;
5316        let obligation = view.domain_record(&obligation_reference())?;
5317        let valid = match context.boundary_id.get() {
5318            1 => {
5319                original.is_some_and(DomainRecord::is_active)
5320                    && obligation.is_some_and(DomainRecord::is_active)
5321            }
5322            2 => original.is_some_and(|record| {
5323                matches!(
5324                    &record.lifecycle,
5325                    DomainRecordLifecycle::Retired {
5326                        successor: Some(successor),
5327                        ..
5328                    } if successor == &office_reference("office-b")
5329                )
5330            }),
5331            3 => {
5332                original.is_some_and(|record| {
5333                    matches!(
5334                        &record.lifecycle,
5335                        DomainRecordLifecycle::Retired {
5336                            successor: Some(successor),
5337                            ..
5338                        } if successor == &office_reference("office-b")
5339                    )
5340                }) && proposed_successor.is_some_and(|record| {
5341                    matches!(
5342                        &record.lifecycle,
5343                        DomainRecordLifecycle::Retired {
5344                            successor: Some(successor),
5345                            ..
5346                        } if successor == &office_reference("office-c")
5347                    )
5348                })
5349            }
5350            4 => {
5351                original.is_some_and(DomainRecord::is_deleted)
5352                    && obligation.is_some_and(|record| {
5353                        record.references.iter().any(|reference| {
5354                            reference.target
5355                                == DomainReferenceTarget::Domain(office_reference("office-c"))
5356                        })
5357                    })
5358            }
5359            _ => true,
5360        };
5361        if !valid {
5362            return Err(CanwuError::new(
5363                ErrorCode::InvalidBoundary,
5364                "invariant systems did not receive the deterministic domain-record proposal",
5365            ));
5366        }
5367        Ok(BoundaryProposal::default())
5368    }
5369
5370    fn register_record_fixture(
5371        registrar: &mut PluginRegistrar<'_>,
5372        handler: BoundarySystemHandler,
5373    ) -> Result<(), CanwuError> {
5374        let mut writes = register_record_schemas(registrar)?;
5375        writes.push(StateKey::new("fixture.governance", "marker"));
5376
5377        let mut lifecycle = BoundarySystemContract::new(
5378            "lifecycle",
5379            BoundaryPhase::DomainDeltaProposal,
5380            SystemCadence::Daily,
5381        );
5382        lifecycle.writes.clone_from(&writes);
5383        lifecycle.emits = vec!["record_probe".to_owned()];
5384        lifecycle.visibility = StateVisibility::SameBoundary;
5385        registrar.register_boundary_system(lifecycle, handler)?;
5386
5387        let mut observer = BoundarySystemContract::new(
5388            "observer",
5389            BoundaryPhase::DomainDeltaProposal,
5390            SystemCadence::Daily,
5391        );
5392        observer.emits = vec!["proposal_probe".to_owned()];
5393        observer.visibility = StateVisibility::SameBoundary;
5394        registrar.register_boundary_system(observer, observe_record_proposal)?;
5395
5396        let mut invariant = BoundarySystemContract::new(
5397            "validate-lifecycle",
5398            BoundaryPhase::InvariantValidation,
5399            SystemCadence::Daily,
5400        );
5401        invariant.reads = writes;
5402        registrar.register_boundary_system(invariant, validate_record_lifecycle_view)
5403    }
5404
5405    fn register_record_schemas(
5406        registrar: &mut PluginRegistrar<'_>,
5407    ) -> Result<Vec<StateKey>, CanwuError> {
5408        let mut office = DomainRecordSchema::new(office_kind(), DomainRecordClass::Entity);
5409        office.payload_schema = object_payload_schema("name");
5410        office.references = vec![DomainReferenceSchema {
5411            role: "holder".to_owned(),
5412            targets: vec![DomainReferenceTargetKind::Core(
5413                canwu_core::CoreEntityKind::Person,
5414            )],
5415            required: true,
5416            multiple: false,
5417            allow_retired: false,
5418        }];
5419        let office_state = office.state_key();
5420        registrar.register_record_schema(office)?;
5421
5422        let mut obligation = DomainRecordSchema::new(obligation_kind(), DomainRecordClass::Record);
5423        obligation.payload_schema = object_payload_schema("status");
5424        obligation.references = vec![DomainReferenceSchema {
5425            role: "office".to_owned(),
5426            targets: vec![DomainReferenceTargetKind::Domain(office_kind())],
5427            required: true,
5428            multiple: false,
5429            allow_retired: true,
5430        }];
5431        let obligation_state = obligation.state_key();
5432        registrar.register_record_schema(obligation)?;
5433        Ok(vec![office_state, obligation_state])
5434    }
5435
5436    impl SimulationPlugin for RecordLifecyclePlugin {
5437        fn name(&self) -> &'static str {
5438            "fixture-record-lifecycle"
5439        }
5440
5441        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000021");
5442
5443        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
5444            register_record_fixture(registrar, apply_record_lifecycle)
5445        }
5446    }
5447
5448    impl SimulationPlugin for RecordDeleteOnlyPlugin {
5449        fn name(&self) -> &'static str {
5450            "fixture-record-delete-only"
5451        }
5452
5453        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000022");
5454
5455        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
5456            register_record_fixture(registrar, apply_invalid_record_delete)
5457        }
5458    }
5459
5460    fn apply_record_cycle(
5461        _view: &SimulationView<'_>,
5462        context: &BoundaryContext,
5463    ) -> Result<BoundaryProposal, CanwuError> {
5464        let directives = match context.boundary_id.get() {
5465            1 => vec![
5466                BoundaryDirective::MutateRecord {
5467                    mutation: DomainRecordMutation::Create {
5468                        record: office_draft("office-a", "First Office"),
5469                    },
5470                    summary: "Create the first office".to_owned(),
5471                },
5472                BoundaryDirective::MutateRecord {
5473                    mutation: DomainRecordMutation::Create {
5474                        record: office_draft("office-b", "Second Office"),
5475                    },
5476                    summary: "Create the second office".to_owned(),
5477                },
5478            ],
5479            2 => vec![
5480                BoundaryDirective::MutateRecord {
5481                    mutation: DomainRecordMutation::Retire {
5482                        record: office_reference("office-a"),
5483                        expected_version: 1,
5484                        successor: Some(office_reference("office-b")),
5485                    },
5486                    summary: "Attempt the first half of a successor cycle".to_owned(),
5487                },
5488                BoundaryDirective::MutateRecord {
5489                    mutation: DomainRecordMutation::Retire {
5490                        record: office_reference("office-b"),
5491                        expected_version: 1,
5492                        successor: Some(office_reference("office-a")),
5493                    },
5494                    summary: "Attempt the second half of a successor cycle".to_owned(),
5495                },
5496            ],
5497            _ => Vec::new(),
5498        };
5499        Ok(BoundaryProposal {
5500            directives,
5501            ..BoundaryProposal::default()
5502        })
5503    }
5504
5505    impl SimulationPlugin for RecordCyclePlugin {
5506        fn name(&self) -> &'static str {
5507            "fixture-record-cycle"
5508        }
5509
5510        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000023");
5511
5512        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
5513            let Some(office_state) = register_record_schemas(registrar)?.into_iter().next() else {
5514                return Err(CanwuError::new(
5515                    ErrorCode::InvalidPluginRegistration,
5516                    "record cycle fixture is missing its office state",
5517                ));
5518            };
5519            let mut cycle = BoundarySystemContract::new(
5520                "cycle",
5521                BoundaryPhase::DomainDeltaProposal,
5522                SystemCadence::Daily,
5523            );
5524            cycle.writes = vec![office_state];
5525            cycle.visibility = StateVisibility::SameBoundary;
5526            registrar.register_boundary_system(cycle, apply_record_cycle)
5527        }
5528    }
5529
5530    fn apply_record_seat_deletion(
5531        _view: &SimulationView<'_>,
5532        context: &BoundaryContext,
5533    ) -> Result<BoundaryProposal, CanwuError> {
5534        let directives = match context.boundary_id.get() {
5535            1 => vec![BoundaryDirective::MutateRecord {
5536                mutation: DomainRecordMutation::Retire {
5537                    record: office_reference("office-a"),
5538                    expected_version: 1,
5539                    successor: None,
5540                },
5541                summary: "Retire the institution-bound office".to_owned(),
5542            }],
5543            2 => vec![BoundaryDirective::MutateRecord {
5544                mutation: DomainRecordMutation::Delete {
5545                    record: office_reference("office-a"),
5546                    expected_version: 2,
5547                },
5548                summary: "Delete the retired institution-bound office".to_owned(),
5549            }],
5550            _ => Vec::new(),
5551        };
5552        Ok(BoundaryProposal {
5553            directives,
5554            ..BoundaryProposal::default()
5555        })
5556    }
5557
5558    impl SimulationPlugin for RecordSeatDeletionPlugin {
5559        fn name(&self) -> &'static str {
5560            "fixture-record-seat-deletion"
5561        }
5562
5563        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000024");
5564
5565        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
5566            let Some(office_state) = register_record_schemas(registrar)?.into_iter().next() else {
5567                return Err(CanwuError::new(
5568                    ErrorCode::InvalidPluginRegistration,
5569                    "seat deletion fixture is missing its office state",
5570                ));
5571            };
5572            let mut lifecycle = BoundarySystemContract::new(
5573                "seat-deletion",
5574                BoundaryPhase::DomainDeltaProposal,
5575                SystemCadence::Daily,
5576            );
5577            lifecycle.writes = vec![office_state];
5578            lifecycle.visibility = StateVisibility::SameBoundary;
5579            registrar.register_boundary_system(lifecycle, apply_record_seat_deletion)
5580        }
5581    }
5582
5583    struct CanonicalIngressPlugin;
5584
5585    fn ingress_class_name(class: IngressClass) -> &'static str {
5586        match class {
5587            IngressClass::Command => "command",
5588            IngressClass::Communication => "communication",
5589            IngressClass::Acknowledgement => "acknowledgement",
5590            IngressClass::Information => "information",
5591            IngressClass::ScheduledSystem => "scheduled_system",
5592        }
5593    }
5594
5595    fn consume_canonical_ingress(
5596        view: &SimulationView<'_>,
5597        context: &BoundaryContext,
5598    ) -> Result<BoundaryProposal, CanwuError> {
5599        for command_id in &context.admitted_commands {
5600            if view.command(*command_id)?.is_none() {
5601                return Err(CanwuError::new(
5602                    ErrorCode::InvalidBoundary,
5603                    "boundary systems must resolve every admitted command",
5604                ));
5605            }
5606        }
5607        for event_id in &context.admitted_events {
5608            if view.event(*event_id)?.is_none() {
5609                return Err(CanwuError::new(
5610                    ErrorCode::InvalidBoundary,
5611                    "boundary systems must resolve every admitted event",
5612                ));
5613            }
5614        }
5615        if !context.emitted_events.is_empty() {
5616            return Err(CanwuError::new(
5617                ErrorCode::InvalidBoundary,
5618                "pre-commit boundary systems must not observe uncommitted emissions",
5619            ));
5620        }
5621        if context.boundary_id.get() == 1
5622            && view
5623                .component(
5624                    &StateKey::new("ingress-fixture", "received"),
5625                    &EntityRef::Person(PersonId::new(1)),
5626                    "canonical-order",
5627                )?
5628                .is_some()
5629        {
5630            return Err(CanwuError::new(
5631                ErrorCode::InvalidBoundary,
5632                "pre-commit boundary systems must read the stable current-state snapshot",
5633            ));
5634        }
5635        for value in 1..=32 {
5636            let id = IngressId::new(value);
5637            if !context.admitted_ingress.contains(&id) && view.ingress(id)?.is_some() {
5638                return Err(CanwuError::new(
5639                    ErrorCode::InvalidBoundary,
5640                    "boundary systems must not observe ingress before admission",
5641                ));
5642            }
5643        }
5644        let mut order = Vec::new();
5645        for ingress_id in &context.admitted_ingress {
5646            let Some(record) = view.ingress(*ingress_id)? else {
5647                continue;
5648            };
5649            let IngressPayload::Plugin {
5650                plugin,
5651                packet_type,
5652                ..
5653            } = &record.payload
5654            else {
5655                continue;
5656            };
5657            if plugin == "canonical-ingress" {
5658                order.push(format!(
5659                    "{}:{packet_type}:{}",
5660                    ingress_class_name(record.class),
5661                    record.priority
5662                ));
5663            }
5664        }
5665        if order.is_empty() {
5666            return Ok(BoundaryProposal::default());
5667        }
5668        Ok(BoundaryProposal {
5669            directives: vec![BoundaryDirective::SetComponent {
5670                state: StateKey::new("ingress-fixture", "received"),
5671                entity: EntityRef::Person(PersonId::new(1)),
5672                component: "canonical-order".to_owned(),
5673                value: serde_json::json!(order),
5674                summary: "Record canonical ingress order".to_owned(),
5675            }],
5676            ..BoundaryProposal::default()
5677        })
5678    }
5679
5680    fn validate_committed_canonical_evidence(
5681        view: &SimulationView<'_>,
5682        context: &BoundaryContext,
5683    ) -> Result<BoundaryProposal, CanwuError> {
5684        let received = view.component(
5685            &StateKey::new("ingress-fixture", "received"),
5686            &EntityRef::Person(PersonId::new(1)),
5687            "canonical-order",
5688        )?;
5689        if received.is_none() {
5690            return Err(CanwuError::new(
5691                ErrorCode::InvalidBoundary,
5692                "post-commit boundary systems must observe committed current state",
5693            ));
5694        }
5695        if context.emitted_events.is_empty() {
5696            return Err(CanwuError::new(
5697                ErrorCode::InvalidBoundary,
5698                "post-commit boundary systems must observe committed emission identifiers",
5699            ));
5700        }
5701        for event_id in &context.emitted_events {
5702            if view.event(*event_id)?.is_none() {
5703                return Err(CanwuError::new(
5704                    ErrorCode::InvalidBoundary,
5705                    "post-commit boundary systems must resolve committed emissions",
5706                ));
5707            }
5708        }
5709        Ok(BoundaryProposal::default())
5710    }
5711
5712    fn mark_daily_calendar(
5713        _view: &SimulationView<'_>,
5714        _context: &BoundaryContext,
5715    ) -> Result<BoundaryProposal, CanwuError> {
5716        Ok(BoundaryProposal {
5717            directives: vec![BoundaryDirective::SetComponent {
5718                state: StateKey::new("ingress-fixture", "calendar"),
5719                entity: EntityRef::Person(PersonId::new(1)),
5720                component: "daily".to_owned(),
5721                value: Value::Bool(true),
5722                summary: "Run the queued daily calendar boundary".to_owned(),
5723            }],
5724            ..BoundaryProposal::default()
5725        })
5726    }
5727
5728    impl SimulationPlugin for CanonicalIngressPlugin {
5729        fn name(&self) -> &'static str {
5730            "canonical-ingress"
5731        }
5732
5733        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000025");
5734
5735        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
5736            for (name, description, class) in [
5737                (
5738                    "dispatch",
5739                    "A command or communication packet in transit",
5740                    IngressClass::Communication,
5741                ),
5742                (
5743                    "ack",
5744                    "A deterministic command acknowledgement",
5745                    IngressClass::Acknowledgement,
5746                ),
5747                (
5748                    "report",
5749                    "A deterministic information packet",
5750                    IngressClass::Information,
5751                ),
5752            ] {
5753                registrar.register_ingress(PluginIngressDescriptor {
5754                    name: name.to_owned(),
5755                    description: description.to_owned(),
5756                    class,
5757                    payload_schema: object_payload_schema("label"),
5758                })?;
5759            }
5760            let mut consumer = BoundarySystemContract::new(
5761                "consume-ingress",
5762                BoundaryPhase::DomainDeltaProposal,
5763                SystemCadence::EventDriven,
5764            );
5765            consumer.reads = vec![
5766                StateKey::core_commands(),
5767                StateKey::core_events(),
5768                StateKey::core_ingress(),
5769                StateKey::new("ingress-fixture", "received"),
5770            ];
5771            consumer.writes = vec![StateKey::new("ingress-fixture", "received")];
5772            consumer.visibility = StateVisibility::SameBoundary;
5773            registrar.register_boundary_system(consumer, consume_canonical_ingress)?;
5774
5775            let mut committed = BoundarySystemContract::new(
5776                "validate-committed-evidence",
5777                BoundaryPhase::HistoricalCandidateEvaluation,
5778                SystemCadence::EventDriven,
5779            );
5780            committed.reads = vec![
5781                StateKey::core_events(),
5782                StateKey::new("ingress-fixture", "received"),
5783            ];
5784            registrar.register_boundary_system(committed, validate_committed_canonical_evidence)?;
5785
5786            let mut calendar = BoundarySystemContract::new(
5787                "daily-calendar",
5788                BoundaryPhase::DomainDeltaProposal,
5789                SystemCadence::Daily,
5790            );
5791            calendar.writes = vec![StateKey::new("ingress-fixture", "calendar")];
5792            calendar.visibility = StateVisibility::SameBoundary;
5793            registrar.register_boundary_system(calendar, mark_daily_calendar)
5794        }
5795    }
5796
5797    struct GeneratedIngressPlugin;
5798
5799    fn relay_generated_ingress(
5800        view: &SimulationView<'_>,
5801        context: &BoundaryContext,
5802    ) -> Result<BoundaryProposal, CanwuError> {
5803        let mut directives = Vec::new();
5804        for ingress_id in &context.admitted_ingress {
5805            let Some(record) = view.ingress(*ingress_id)? else {
5806                return Err(CanwuError::new(
5807                    ErrorCode::InvalidBoundary,
5808                    "generated-ingress context references a missing record",
5809                ));
5810            };
5811            let IngressPayload::Plugin {
5812                plugin,
5813                packet_type,
5814                affected_entities,
5815                ..
5816            } = &record.payload
5817            else {
5818                continue;
5819            };
5820            if plugin != "generated-ingress" {
5821                continue;
5822            }
5823            match packet_type.as_str() {
5824                "dispatch" => directives.push(BoundaryDirective::ScheduleIngress {
5825                    after: SimDuration::ZERO,
5826                    packet_type: "ack".to_owned(),
5827                    priority: 5,
5828                    payload: serde_json::json!({ "label": "automatic acknowledgement" }),
5829                    affected: affected_entities.clone(),
5830                }),
5831                "ack" => directives.push(BoundaryDirective::SetComponent {
5832                    state: StateKey::new("generated-ingress-fixture", "received"),
5833                    entity: EntityRef::Person(PersonId::new(1)),
5834                    component: "acknowledged".to_owned(),
5835                    value: Value::Bool(true),
5836                    summary: "Record the automatically generated acknowledgement".to_owned(),
5837                }),
5838                _ => {}
5839            }
5840        }
5841        Ok(BoundaryProposal {
5842            directives,
5843            ..BoundaryProposal::default()
5844        })
5845    }
5846
5847    impl SimulationPlugin for GeneratedIngressPlugin {
5848        fn name(&self) -> &'static str {
5849            "generated-ingress"
5850        }
5851
5852        test_plugin_identity!("0000000000000000000000000000000000000000000000000000000000000026");
5853
5854        fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
5855            registrar.register_ingress(PluginIngressDescriptor {
5856                name: "dispatch".to_owned(),
5857                description: "A communication packet that requires acknowledgement".to_owned(),
5858                class: IngressClass::Communication,
5859                payload_schema: object_payload_schema("label"),
5860            })?;
5861            registrar.register_ingress(PluginIngressDescriptor {
5862                name: "ack".to_owned(),
5863                description: "A boundary-generated acknowledgement".to_owned(),
5864                class: IngressClass::Acknowledgement,
5865                payload_schema: object_payload_schema("label"),
5866            })?;
5867            let mut relay = BoundarySystemContract::new(
5868                "relay-ingress",
5869                BoundaryPhase::DomainDeltaProposal,
5870                SystemCadence::EventDriven,
5871            );
5872            relay.reads = vec![StateKey::core_ingress()];
5873            relay.writes = vec![StateKey::new("generated-ingress-fixture", "received")];
5874            relay.visibility = StateVisibility::SameBoundary;
5875            registrar.register_boundary_system(relay, relay_generated_ingress)
5876        }
5877    }
5878
5879    fn move_order(ids: &DemoIds) -> CommandEnvelope {
5880        CommandEnvelope::new(
5881            Issuer::Actor(ids.commander),
5882            Command::MoveArmy {
5883                army: ids.army,
5884                destination: ids.eastern_territory,
5885            },
5886        )
5887    }
5888
5889    fn manifest_for_configuration(
5890        scenario: &Scenario,
5891        configuration: &RunConfiguration,
5892    ) -> RunManifest {
5893        let scenario_manifest =
5894            ArtifactManifest::for_scenario("fixture", "policy-fixture", "1", scenario)
5895                .expect("scenario identity should hash");
5896        let configuration_manifest = ArtifactManifest::for_run_configuration(
5897            "fixture",
5898            "run-configuration",
5899            "1",
5900            configuration,
5901        )
5902        .expect("run configuration identity should hash");
5903        RunManifest::declared(scenario_manifest, configuration_manifest)
5904    }
5905
5906    fn character_authority(
5907        actor: PersonId,
5908        army: ArmyId,
5909        seat_id: &str,
5910        permission_profile_id: &str,
5911    ) -> CommandAuthority {
5912        CommandAuthority {
5913            decision_origin: DecisionOrigin::Actor { actor },
5914            seat_id: Some(seat_id.to_owned()),
5915            permission_profile_id: Some(permission_profile_id.to_owned()),
5916            command_subject: Some(EntityRef::Army(army)),
5917        }
5918    }
5919
5920    #[test]
5921    fn deterministic_seed_and_event_order_survive_equal_runs() {
5922        let (scenario, ids) = demo_scenario();
5923        let mut first = Simulation::new(35, scenario.clone()).expect("demo should load");
5924        first
5925            .submit(move_order(&ids))
5926            .expect("order should validate");
5927        first
5928            .advance(SimDuration::days(4))
5929            .expect("time should advance");
5930        let second = Simulation::replay(35, scenario, first.command_log(), first.time())
5931            .expect("journal should replay");
5932        assert_eq!(first.snapshot(), second.snapshot());
5933    }
5934
5935    #[test]
5936    fn typed_ingress_is_idempotent_revision_guarded_and_replayable() {
5937        let (scenario, ids) = demo_scenario();
5938        let configuration = RunConfiguration::play_as_character(
5939            "seat.commander",
5940            "controller.human",
5941            ids.commander,
5942            "permission.military-command",
5943        );
5944        let manifest = manifest_for_configuration(&scenario, &configuration);
5945        let mut simulation = Simulation::new_with_run_configuration(
5946            35,
5947            scenario.clone(),
5948            manifest.clone(),
5949            configuration.clone(),
5950        )
5951        .expect("declared character run should load");
5952        let envelope = CommandEnvelope::new(
5953            Issuer::Human("controller.human".to_owned()),
5954            Command::MoveArmy {
5955                army: ids.army,
5956                destination: ids.eastern_territory,
5957            },
5958        )
5959        .with_authority(character_authority(
5960            ids.commander,
5961            ids.army,
5962            "seat.commander",
5963            "permission.military-command",
5964        ))
5965        .at_time(SimTime::EPOCH);
5966        let request = CommandRequest::new(CommandRequestId::new(1), 0, envelope.clone());
5967        let accepted = simulation
5968            .process_command(request.clone())
5969            .expect("typed request should produce an outcome");
5970        let CommandOutcome::Accepted { receipt } = &accepted else {
5971            panic!("matching controller and seat should be accepted");
5972        };
5973        assert_eq!(receipt.attempt_id, Some(CommandAttemptId::new(1)));
5974        assert_eq!(receipt.command_id, CommandId::new(1));
5975        assert_eq!(receipt.request_id, Some(CommandRequestId::new(1)));
5976        assert_eq!(receipt.revision, 1);
5977        assert_eq!(simulation.revision(), 1);
5978
5979        let after_accept = simulation.snapshot();
5980        assert_eq!(
5981            simulation
5982                .process_command(request)
5983                .expect("an exact retry should be served from idempotency evidence"),
5984            accepted
5985        );
5986        assert_eq!(simulation.snapshot(), after_accept);
5987
5988        let mut collision_envelope = envelope.clone();
5989        collision_envelope.command = Command::MoveArmy {
5990            army: ids.army,
5991            destination: ids.western_territory,
5992        };
5993        let collision = simulation
5994            .process_command(CommandRequest::new(
5995                CommandRequestId::new(1),
5996                1,
5997                collision_envelope,
5998            ))
5999            .expect("request-ID collision should be a structured outcome");
6000        let CommandOutcome::Rejected { rejection } = collision else {
6001            panic!("request-ID reuse with different input must be rejected");
6002        };
6003        assert_eq!(rejection.attempt_id, None);
6004        assert_eq!(rejection.retained_revision, 1);
6005        assert_eq!(rejection.error.code, ErrorCode::IdempotencyConflict);
6006        assert_eq!(simulation.snapshot(), after_accept);
6007
6008        let stale_request = CommandRequest::new(CommandRequestId::new(2), 0, envelope);
6009        let stale = simulation
6010            .process_command(stale_request.clone())
6011            .expect("a stale request should remain structured evidence");
6012        let CommandOutcome::Rejected { rejection } = &stale else {
6013            panic!("stale revisions must be rejected");
6014        };
6015        assert_eq!(rejection.attempt_id, Some(CommandAttemptId::new(2)));
6016        assert_eq!(rejection.retained_revision, 2);
6017        assert_eq!(rejection.error.code, ErrorCode::SimulationRevisionConflict);
6018        assert_eq!(simulation.revision(), 2);
6019        assert_eq!(simulation.command_log().len(), 1);
6020        assert_eq!(simulation.command_attempts().len(), 2);
6021
6022        let after_stale = simulation.snapshot();
6023        assert_eq!(
6024            simulation
6025                .process_command(stale_request)
6026                .expect("an exact rejected retry should be cached"),
6027            stale
6028        );
6029        assert_eq!(simulation.snapshot(), after_stale);
6030        let restored = Simulation::from_snapshot(after_stale.clone())
6031            .expect("typed ingress evidence should survive save/load");
6032        assert_eq!(restored.snapshot(), after_stale);
6033
6034        let mut cyclic_cause = after_stale.clone();
6035        let event_id = cyclic_cause.events[0].id;
6036        cyclic_cause.events[0].cause = Some(CauseRef::Event(event_id));
6037        refresh_snapshot_commitments_and_checkpoint(&mut cyclic_cause);
6038        let Err(error) = Simulation::from_snapshot(cyclic_cause) else {
6039            panic!("event cause cycles must be rejected without unbounded traversal");
6040        };
6041        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
6042        assert!(error.message.contains("parent event"));
6043
6044        let mut forged = after_stale;
6045        forged.command_attempts[0].envelope.issuer = Issuer::Human("controller.other".to_owned());
6046        forged.commands[0].envelope = forged.command_attempts[0].envelope.clone();
6047        refresh_snapshot_commitments_and_checkpoint(&mut forged);
6048        let Err(error) = Simulation::from_snapshot(forged) else {
6049            panic!("accepted attempts that violate recorded policy must not load");
6050        };
6051        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
6052        assert!(error.message.contains("ingress policy"));
6053
6054        simulation
6055            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
6056            .expect("attempt evidence should enter the next boundary");
6057        let boundary = simulation
6058            .boundaries()
6059            .last()
6060            .expect("the boundary should be recorded");
6061        assert_eq!(
6062            boundary.admitted_attempts,
6063            vec![CommandAttemptId::new(1), CommandAttemptId::new(2)]
6064        );
6065        assert_eq!(boundary.admitted_commands, vec![CommandId::new(1)]);
6066        let journal = simulation.replay_journal();
6067        let replayed_fixture = Simulation::replay_with_run_configuration(
6068            35,
6069            scenario.clone(),
6070            manifest,
6071            configuration,
6072            &[],
6073            simulation.command_log(),
6074            simulation.command_attempts(),
6075            simulation.boundaries(),
6076            simulation.time(),
6077        )
6078        .expect("declared caller-supplied request journal should replay");
6079        assert_eq!(simulation.snapshot(), replayed_fixture.snapshot());
6080        let replayed = Simulation::replay_from_journal(scenario, &[], &journal)
6081            .expect("accepted and rejected request evidence should replay exactly");
6082        assert_eq!(simulation.snapshot(), replayed.snapshot());
6083    }
6084
6085    #[test]
6086    fn legacy_direct_and_tracked_request_ingress_cannot_mix() {
6087        let (scenario, ids) = demo_scenario();
6088        let mut legacy_first =
6089            Simulation::new(35, scenario.clone()).expect("compatibility run should load");
6090        legacy_first
6091            .submit(move_order(&ids))
6092            .expect("legacy direct command should be accepted");
6093        let after_legacy = legacy_first.snapshot();
6094        let error = legacy_first
6095            .process_command(CommandRequest::new(
6096                CommandRequestId::new(1),
6097                1,
6098                move_order(&ids),
6099            ))
6100            .expect_err("tracked requests cannot follow legacy-direct commands");
6101        assert_eq!(error.code, ErrorCode::MixedCommandIngress);
6102        assert_eq!(legacy_first.snapshot(), after_legacy);
6103
6104        let mut tracked_first =
6105            Simulation::new(35, scenario.clone()).expect("compatibility run should load");
6106        let outcome = tracked_first
6107            .process_command(CommandRequest::new(
6108                CommandRequestId::new(1),
6109                0,
6110                CommandEnvelope::new(
6111                    Issuer::Actor(ids.observer),
6112                    Command::MoveArmy {
6113                        army: ids.army,
6114                        destination: ids.eastern_territory,
6115                    },
6116                ),
6117            ))
6118            .expect("domain rejection should remain tracked evidence");
6119        assert!(matches!(outcome, CommandOutcome::Rejected { .. }));
6120        let after_tracked = tracked_first.snapshot();
6121        let error = tracked_first
6122            .submit(move_order(&ids))
6123            .expect_err("legacy direct commands cannot follow tracked attempts");
6124        assert_eq!(error.code, ErrorCode::MixedCommandIngress);
6125        assert_eq!(tracked_first.snapshot(), after_tracked);
6126        Simulation::from_snapshot(after_tracked.clone())
6127            .expect("a rejection-only tracked journal should remain loadable");
6128
6129        let error = tracked_first
6130            .schedule_calendar_boundary(SimTime::EPOCH, vec![SystemCadence::Daily])
6131            .expect_err("canonical ingress cannot begin after a direct tracked attempt");
6132        assert_eq!(error.code, ErrorCode::MixedCommandIngress);
6133        assert_eq!(tracked_first.snapshot(), after_tracked);
6134
6135        tracked_first
6136            .append_ingress(
6137                SimTime::EPOCH,
6138                IngressClass::ScheduledSystem,
6139                0,
6140                IngressPayload::Calendar {
6141                    cadences: vec![SystemCadence::Daily],
6142                },
6143                Some(CauseRef::System("canwu.core.calendar".to_owned())),
6144                false,
6145            )
6146            .expect("the fixture should construct coherent mixed ingress evidence");
6147        let error = Simulation::from_snapshot(tracked_first.snapshot())
6148            .err()
6149            .expect("snapshot validation must reject mixed direct and canonical history");
6150        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
6151
6152        let mut canonical_first =
6153            Simulation::new(35, scenario).expect("canonical compatibility run should load");
6154        canonical_first
6155            .schedule_calendar_boundary(SimTime::EPOCH, vec![SystemCadence::Daily])
6156            .expect("calendar ingress should establish the canonical family");
6157        let after_canonical = canonical_first.snapshot();
6158        let error = canonical_first
6159            .process_command(CommandRequest::new(
6160                CommandRequestId::new(2),
6161                0,
6162                move_order(&ids),
6163            ))
6164            .expect_err("direct tracked requests cannot bypass canonical ingress");
6165        assert_eq!(error.code, ErrorCode::MixedCommandIngress);
6166        assert_eq!(canonical_first.snapshot(), after_canonical);
6167    }
6168
6169    #[test]
6170    fn declared_runs_reject_untracked_legacy_command_history() {
6171        let (scenario, ids) = demo_scenario();
6172        let configuration = RunConfiguration::play_as_character(
6173            "seat.commander",
6174            "controller.human",
6175            ids.commander,
6176            "permission.military-command",
6177        );
6178        let manifest = manifest_for_configuration(&scenario, &configuration);
6179        let mut declared = Simulation::new_with_run_configuration(
6180            35,
6181            scenario.clone(),
6182            manifest.clone(),
6183            configuration.clone(),
6184        )
6185        .expect("declared run should load");
6186        let envelope = CommandEnvelope::new(
6187            Issuer::Human("controller.human".to_owned()),
6188            Command::MoveArmy {
6189                army: ids.army,
6190                destination: ids.eastern_territory,
6191            },
6192        )
6193        .with_authority(character_authority(
6194            ids.commander,
6195            ids.army,
6196            "seat.commander",
6197            "permission.military-command",
6198        ))
6199        .at_time(SimTime::EPOCH);
6200        let before = declared.snapshot();
6201        let error = declared
6202            .submit(envelope)
6203            .expect_err("declared runs must not accept compatibility-only ingress");
6204        assert_eq!(error.code, ErrorCode::InvalidAuthority);
6205        assert_eq!(declared.snapshot(), before);
6206
6207        let mut compatibility =
6208            Simulation::new(35, scenario.clone()).expect("compatibility run should load");
6209        compatibility
6210            .submit(move_order(&ids))
6211            .expect("legacy command fixture should be accepted");
6212        let mut forged = compatibility.snapshot();
6213        forged.run_manifest = before.run_manifest;
6214        forged.run_manifest_hash = before.run_manifest_hash;
6215        forged.run_configuration = before.run_configuration;
6216        refresh_snapshot_commitments_and_checkpoint(&mut forged);
6217        let Err(error) = Simulation::from_snapshot(forged) else {
6218            panic!("declared snapshots cannot smuggle untracked accepted commands");
6219        };
6220        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
6221        assert!(error.message.contains("tracked attempt evidence"));
6222
6223        let Err(error) = Simulation::replay_with_run_configuration(
6224            35,
6225            scenario,
6226            manifest,
6227            configuration,
6228            &[],
6229            compatibility.command_log(),
6230            &[],
6231            &[],
6232            compatibility.time(),
6233        ) else {
6234            panic!("declared fixture replay cannot reinterpret legacy command input");
6235        };
6236        assert_eq!(error.code, ErrorCode::InvalidAuthority);
6237    }
6238
6239    #[test]
6240    fn declared_revision_and_time_guards_cover_boundaries_and_clock() {
6241        let (scenario, ids) = demo_scenario();
6242        let configuration = RunConfiguration::play_as_character(
6243            "seat.commander",
6244            "controller.human",
6245            ids.commander,
6246            "permission.military-command",
6247        );
6248        let manifest = manifest_for_configuration(&scenario, &configuration);
6249        let command_at = |time| {
6250            CommandEnvelope::new(
6251                Issuer::Human("controller.human".to_owned()),
6252                Command::MoveArmy {
6253                    army: ids.army,
6254                    destination: ids.eastern_territory,
6255                },
6256            )
6257            .with_authority(character_authority(
6258                ids.commander,
6259                ids.army,
6260                "seat.commander",
6261                "permission.military-command",
6262            ))
6263            .at_time(time)
6264        };
6265
6266        let mut after_boundary = Simulation::new_with_run_configuration(
6267            35,
6268            scenario.clone(),
6269            manifest.clone(),
6270            configuration.clone(),
6271        )
6272        .expect("declared run should load");
6273        after_boundary
6274            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
6275            .expect("boundary should publish");
6276        assert_eq!(after_boundary.revision(), 1);
6277        let stale = after_boundary
6278            .process_command(CommandRequest::new(
6279                CommandRequestId::new(1),
6280                0,
6281                command_at(SimTime::EPOCH),
6282            ))
6283            .expect("stale boundary revision should be retained as evidence");
6284        let CommandOutcome::Rejected { rejection } = stale else {
6285            panic!("a pre-boundary revision must be stale");
6286        };
6287        assert_eq!(rejection.error.code, ErrorCode::SimulationRevisionConflict);
6288        assert_eq!(rejection.retained_revision, 2);
6289        let accepted = after_boundary
6290            .process_command(CommandRequest::new(
6291                CommandRequestId::new(2),
6292                2,
6293                command_at(SimTime::EPOCH),
6294            ))
6295            .expect("current revision should be accepted");
6296        let CommandOutcome::Accepted { receipt } = accepted else {
6297            panic!("current revision and time should admit the command");
6298        };
6299        assert_eq!(receipt.revision, 3);
6300        assert_eq!(after_boundary.revision(), 3);
6301        let boundary_journal = after_boundary.replay_journal();
6302        let boundary_replay =
6303            Simulation::replay_from_journal(scenario.clone(), &[], &boundary_journal)
6304                .expect("boundary-relative revision evidence should replay exactly");
6305        assert_eq!(after_boundary.snapshot(), boundary_replay.snapshot());
6306
6307        let mut after_clock =
6308            Simulation::new_with_run_configuration(35, scenario.clone(), manifest, configuration)
6309                .expect("declared run should load");
6310        after_clock
6311            .advance(SimDuration::hours(1))
6312            .expect("clock should advance");
6313        assert_eq!(after_clock.revision(), 0);
6314        let stale = after_clock
6315            .process_command(CommandRequest::new(
6316                CommandRequestId::new(1),
6317                0,
6318                command_at(SimTime::EPOCH),
6319            ))
6320            .expect("stale time should be retained as evidence");
6321        let CommandOutcome::Rejected { rejection } = stale else {
6322            panic!("a pre-advance simulation time must be stale");
6323        };
6324        assert_eq!(rejection.error.code, ErrorCode::SimulationTimeConflict);
6325        assert_eq!(rejection.retained_revision, 1);
6326        let accepted = after_clock
6327            .process_command(CommandRequest::new(
6328                CommandRequestId::new(2),
6329                1,
6330                command_at(after_clock.time()),
6331            ))
6332            .expect("current revision and time should be accepted");
6333        let CommandOutcome::Accepted { receipt } = accepted else {
6334            panic!("current clock guard should admit the command");
6335        };
6336        assert_eq!(receipt.revision, 2);
6337        let clock_journal = after_clock.replay_journal();
6338        let clock_replay = Simulation::replay_from_journal(scenario, &[], &clock_journal)
6339            .expect("clock-relative time evidence should replay exactly");
6340        assert_eq!(after_clock.snapshot(), clock_replay.snapshot());
6341    }
6342
6343    #[test]
6344    fn authoritative_revision_is_persisted_migrated_and_rollback_safe() {
6345        let (scenario, ids) = demo_scenario();
6346        let invalid_morale = |morale| {
6347            CommandEnvelope::new(
6348                Issuer::Debug,
6349                Command::DebugSetArmyMorale {
6350                    army: ids.army,
6351                    morale,
6352                },
6353            )
6354        };
6355        let first_request = CommandRequest::new(CommandRequestId::new(1), 0, invalid_morale(101));
6356        let mut simulation =
6357            Simulation::new(35, scenario.clone()).expect("compatibility run should load");
6358
6359        let first = simulation
6360            .process_command(first_request.clone())
6361            .expect("expected rejection should persist");
6362        let CommandOutcome::Rejected { rejection } = &first else {
6363            panic!("invalid morale must be rejected");
6364        };
6365        assert_eq!(rejection.retained_revision, 1);
6366        assert_eq!(simulation.revision(), 1);
6367
6368        let after_first = simulation.snapshot();
6369        assert_eq!(
6370            simulation
6371                .process_command(first_request)
6372                .expect("an exact retry should return the recorded outcome"),
6373            first
6374        );
6375        assert_eq!(simulation.snapshot(), after_first);
6376
6377        let second = simulation
6378            .process_command(CommandRequest::new(
6379                CommandRequestId::new(2),
6380                1,
6381                invalid_morale(102),
6382            ))
6383            .expect("a second expected rejection should persist");
6384        let CommandOutcome::Rejected { rejection } = second else {
6385            panic!("invalid morale must be rejected");
6386        };
6387        assert_eq!(rejection.retained_revision, 2);
6388        assert_eq!(simulation.revision(), 2);
6389
6390        let before_conflict = simulation.snapshot();
6391        let conflict = simulation
6392            .process_command(CommandRequest::new(
6393                CommandRequestId::new(2),
6394                2,
6395                invalid_morale(103),
6396            ))
6397            .expect("a request-ID collision should return a non-persisted rejection");
6398        let CommandOutcome::Rejected { rejection } = conflict else {
6399            panic!("a request-ID collision must not be accepted");
6400        };
6401        assert_eq!(rejection.attempt_id, None);
6402        assert_eq!(rejection.error.code, ErrorCode::IdempotencyConflict);
6403        assert_eq!(rejection.retained_revision, 2);
6404        assert_eq!(simulation.snapshot(), before_conflict);
6405
6406        simulation
6407            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
6408            .expect("an empty boundary should publish");
6409        assert_eq!(simulation.revision(), 3);
6410        let first_boundary_snapshot = simulation.snapshot();
6411        let third = simulation
6412            .process_command(CommandRequest::new(
6413                CommandRequestId::new(3),
6414                3,
6415                invalid_morale(103),
6416            ))
6417            .expect("a post-boundary expected rejection should persist");
6418        let CommandOutcome::Rejected { rejection } = third else {
6419            panic!("invalid morale must be rejected");
6420        };
6421        assert_eq!(rejection.retained_revision, 4);
6422        simulation
6423            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH + SimDuration::hours(1)))
6424            .expect("a second empty boundary should publish");
6425        assert_eq!(simulation.revision(), 5);
6426        let current_snapshot = simulation.snapshot();
6427        let restored = Simulation::from_snapshot(current_snapshot.clone())
6428            .expect("current revision evidence should survive load");
6429        assert_eq!(restored.revision(), 5);
6430        assert_eq!(restored.snapshot(), current_snapshot);
6431        let replayed =
6432            Simulation::replay_from_journal(scenario.clone(), &[], &simulation.replay_journal())
6433                .expect("current revision evidence should replay exactly");
6434        assert_eq!(replayed.snapshot(), current_snapshot);
6435
6436        let mut inconsistent_revision = current_snapshot.clone();
6437        inconsistent_revision.state_revision = 6;
6438        inconsistent_revision.checkpoint_hash = snapshot_checkpoint_hash(&inconsistent_revision)
6439            .expect("the inconsistent revision fixture should remain coherently hashed");
6440        let error = Simulation::from_snapshot(inconsistent_revision)
6441            .err()
6442            .expect("a rehashed revision without evidence must not load");
6443        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
6444        assert!(error.message.contains("state revision"));
6445
6446        let mut legacy_first_boundary = first_boundary_snapshot;
6447        legacy_first_boundary.command_attempts[1].revision_before = 0;
6448        legacy_first_boundary.command_attempts[1].expected_revision = Some(0);
6449        legacy_first_boundary.revision_format_version = 0;
6450        legacy_first_boundary.state_revision = 0;
6451        legacy_first_boundary.replay_revision_format_version = 0;
6452        let legacy_first_boundary_state_hash = snapshot_state_hash(&legacy_first_boundary)
6453            .expect("legacy first-boundary state should hash canonically");
6454
6455        let mut legacy_snapshot = current_snapshot.clone();
6456        legacy_snapshot.command_attempts[1].revision_before = 0;
6457        legacy_snapshot.command_attempts[1].expected_revision = Some(0);
6458        legacy_snapshot.command_attempts[2].revision_before = 1;
6459        legacy_snapshot.command_attempts[2].expected_revision = Some(u64::MAX);
6460        legacy_snapshot.command_attempts[2].outcome = CommandAttemptOutcome::Rejected {
6461            error: CanwuError::new(
6462                ErrorCode::SimulationRevisionConflict,
6463                format!(
6464                    "command expected revision {}, but simulation is at revision 1",
6465                    u64::MAX
6466                ),
6467            ),
6468        };
6469        legacy_snapshot.revision_format_version = 0;
6470        legacy_snapshot.state_revision = 0;
6471        legacy_snapshot.replay_revision_format_version = 0;
6472        legacy_snapshot.boundaries[0].state_hash = Some(legacy_first_boundary_state_hash);
6473        let legacy_state_hash = snapshot_state_hash(&legacy_snapshot)
6474            .expect("legacy revision state should hash canonically");
6475        legacy_snapshot
6476            .boundaries
6477            .last_mut()
6478            .expect("the migration fixture has a boundary head")
6479            .state_hash = Some(legacy_state_hash);
6480        migration::rehash_snapshot_boundaries(&mut legacy_snapshot)
6481            .expect("legacy boundary evidence should hash canonically");
6482        downgrade_snapshot_commitments(&mut legacy_snapshot);
6483        legacy_snapshot.checkpoint_hash = snapshot_checkpoint_hash(&legacy_snapshot)
6484            .expect("legacy checkpoint should bind its pre-migration state");
6485        let mut legacy_value =
6486            serde_json::to_value(legacy_snapshot).expect("legacy fixture should serialize");
6487        let legacy_object = legacy_value
6488            .as_object_mut()
6489            .expect("legacy snapshot JSON should be an object");
6490        legacy_object.remove("revision_format_version");
6491        legacy_object.remove("state_revision");
6492        legacy_object.remove("replay_revision_format_version");
6493        legacy_object.remove("admission_cursor_format_version");
6494        legacy_object.remove("admitted_attempt_count");
6495        legacy_object.remove("admitted_command_count");
6496        legacy_object.remove("admitted_event_count");
6497        let mut broken_chain = legacy_value.clone();
6498        broken_chain["boundaries"][0]["correlation_id"] = Value::from(999_u64);
6499        let error = Simulation::from_snapshot_json(
6500            &serde_json::to_string(&broken_chain).expect("tampered legacy fixture should encode"),
6501        )
6502        .err()
6503        .expect("migration must not launder a broken legacy boundary hash chain");
6504        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
6505        assert!(error.message.contains("legacy boundary hash chain"));
6506
6507        let migrated = Simulation::from_snapshot_json(
6508            &serde_json::to_string(&legacy_value).expect("legacy fixture should encode"),
6509        )
6510        .expect("legacy command revisions should migrate deterministically");
6511        assert_eq!(migrated.revision(), 5);
6512        assert_eq!(migrated.command_attempts()[1].revision_before, 1);
6513        assert_eq!(migrated.command_attempts()[2].revision_before, 3);
6514        assert_eq!(
6515            migrated.command_attempts()[2].expected_revision,
6516            Some(u64::MAX)
6517        );
6518        assert_eq!(migrated.snapshot().replay_revision_format_version, 0);
6519        let reloaded = Simulation::from_snapshot(migrated.snapshot())
6520            .expect("migration-only replay provenance should survive save and load");
6521        assert_eq!(reloaded.revision(), 5);
6522
6523        let migrated_journal = reloaded.replay_journal();
6524        assert_eq!(migrated_journal.revision_format_version, 0);
6525        let error = Simulation::replay_from_journal(scenario, &[], &migrated_journal)
6526            .err()
6527            .expect("revision-migrated histories must not claim current exact replay");
6528        assert_eq!(error.code, ErrorCode::LegacyReplayUnavailable);
6529
6530        let mut continued = reloaded;
6531        continued
6532            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH + SimDuration::hours(2)))
6533            .expect("a revision-migrated snapshot should remain continuable");
6534        assert_eq!(continued.revision(), 6);
6535    }
6536
6537    #[test]
6538    fn legacy_revision_migration_rebases_admitted_and_pending_command_ingress() {
6539        let (scenario, ids) = demo_scenario();
6540        let invalid_request = |request_id, revision, morale| {
6541            CommandRequest::new(
6542                CommandRequestId::new(request_id),
6543                revision,
6544                CommandEnvelope::new(
6545                    Issuer::Debug,
6546                    Command::DebugSetArmyMorale {
6547                        army: ids.army,
6548                        morale,
6549                    },
6550                ),
6551            )
6552        };
6553        let mut simulation =
6554            Simulation::new(47, scenario).expect("canonical migration run should load");
6555        simulation
6556            .enqueue_command(SimTime::EPOCH, 0, invalid_request(1, 0, 101))
6557            .expect("first invalid command should queue");
6558        simulation
6559            .step_canonical()
6560            .expect("first command boundary should settle")
6561            .expect("first command should create a boundary");
6562        assert_eq!(simulation.revision(), 2);
6563        let after_first_boundary = simulation.snapshot();
6564
6565        let second_at = SimTime::EPOCH + SimDuration::hours(1);
6566        simulation
6567            .enqueue_command(second_at, 0, invalid_request(2, 2, 102))
6568            .expect("second invalid command should queue");
6569        simulation
6570            .advance_canonical(SimDuration::hours(1))
6571            .expect("second command boundary should settle");
6572        assert_eq!(simulation.revision(), 4);
6573        let after_second_boundary = simulation.snapshot();
6574
6575        let pending_at = second_at + SimDuration::hours(1);
6576        simulation
6577            .enqueue_command(pending_at, 0, invalid_request(3, 4, 103))
6578            .expect("pending invalid command should queue");
6579        let current_snapshot = simulation.snapshot();
6580
6581        let legacyize = |snapshot: &mut SimulationSnapshot| {
6582            snapshot.revision_format_version = 0;
6583            snapshot.state_revision = 0;
6584            snapshot.replay_revision_format_version = 0;
6585            for (index, attempt) in snapshot.command_attempts.iter_mut().enumerate() {
6586                let legacy_revision = u64::try_from(index).expect("fixture index should fit");
6587                attempt.revision_before = legacy_revision;
6588                attempt.expected_revision = Some(legacy_revision);
6589            }
6590            for (index, record) in snapshot.ingress.iter_mut().enumerate() {
6591                let IngressPayload::Command { request } = &mut record.payload else {
6592                    continue;
6593                };
6594                request.expected_revision = u64::try_from(index).expect("fixture index should fit");
6595            }
6596        };
6597
6598        let mut legacy_first = after_first_boundary;
6599        legacyize(&mut legacy_first);
6600        let first_state_hash =
6601            snapshot_state_hash(&legacy_first).expect("legacy first command boundary should hash");
6602
6603        let mut legacy_second = after_second_boundary;
6604        legacyize(&mut legacy_second);
6605        legacy_second.boundaries[0].state_hash = Some(first_state_hash.clone());
6606        let second_state_hash = snapshot_state_hash(&legacy_second)
6607            .expect("legacy second command boundary should hash");
6608
6609        let mut legacy_snapshot = current_snapshot;
6610        legacyize(&mut legacy_snapshot);
6611        legacy_snapshot.boundaries[0].state_hash = Some(first_state_hash);
6612        legacy_snapshot.boundaries[1].state_hash = Some(second_state_hash);
6613        migration::rehash_snapshot_boundaries(&mut legacy_snapshot)
6614            .expect("legacy ingress boundary chain should hash");
6615        downgrade_snapshot_commitments(&mut legacy_snapshot);
6616        legacy_snapshot.checkpoint_hash = snapshot_checkpoint_hash(&legacy_snapshot)
6617            .expect("legacy ingress checkpoint should hash");
6618        let mut legacy_value =
6619            serde_json::to_value(legacy_snapshot).expect("legacy ingress fixture should serialize");
6620        let legacy_object = legacy_value
6621            .as_object_mut()
6622            .expect("legacy ingress snapshot should be an object");
6623        legacy_object.remove("revision_format_version");
6624        legacy_object.remove("state_revision");
6625        legacy_object.remove("replay_revision_format_version");
6626        legacy_object.remove("admission_cursor_format_version");
6627        legacy_object.remove("admitted_attempt_count");
6628        legacy_object.remove("admitted_command_count");
6629        legacy_object.remove("admitted_event_count");
6630
6631        let mut migrated = Simulation::from_snapshot_json(
6632            &serde_json::to_string(&legacy_value).expect("legacy ingress fixture should encode"),
6633        )
6634        .expect("admitted and pending command guards should migrate coherently");
6635        assert_eq!(migrated.revision(), 4);
6636        assert_eq!(
6637            migrated
6638                .command_attempts()
6639                .iter()
6640                .map(|attempt| (attempt.revision_before, attempt.expected_revision))
6641                .collect::<Vec<_>>(),
6642            vec![(0, Some(0)), (2, Some(2))]
6643        );
6644        assert_eq!(
6645            migrated
6646                .ingress_log()
6647                .iter()
6648                .filter_map(|record| match &record.payload {
6649                    IngressPayload::Command { request } => Some(request.expected_revision),
6650                    IngressPayload::Plugin { .. } | IngressPayload::Calendar { .. } => None,
6651                })
6652                .collect::<Vec<_>>(),
6653            vec![0, 2, 4]
6654        );
6655        assert_eq!(migrated.snapshot().replay_revision_format_version, 0);
6656
6657        migrated
6658            .step_canonical()
6659            .expect("migrated pending command should settle")
6660            .expect("pending command should create a boundary");
6661        assert_eq!(migrated.revision(), 6);
6662        assert_eq!(
6663            migrated
6664                .command_attempts()
6665                .last()
6666                .expect("pending command should create an attempt")
6667                .revision_before,
6668            4
6669        );
6670    }
6671
6672    #[test]
6673    fn admission_cursors_are_persisted_migrated_and_tamper_evident() {
6674        let (scenario, ids) = demo_scenario();
6675        let morale_request = |request_id, revision, morale| {
6676            CommandRequest::new(
6677                CommandRequestId::new(request_id),
6678                revision,
6679                CommandEnvelope::new(
6680                    Issuer::Debug,
6681                    Command::DebugSetArmyMorale {
6682                        army: ids.army,
6683                        morale,
6684                    },
6685                ),
6686            )
6687        };
6688        let mut simulation =
6689            Simulation::new(59, scenario.clone()).expect("cursor fixture should load");
6690        assert!(matches!(
6691            simulation
6692                .process_command(morale_request(1, 0, 80))
6693                .expect("first command should be accepted"),
6694            CommandOutcome::Accepted { .. }
6695        ));
6696        assert!(matches!(
6697            simulation
6698                .process_command(morale_request(2, 1, 101))
6699                .expect("expected rejection should persist"),
6700            CommandOutcome::Rejected { .. }
6701        ));
6702        simulation
6703            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
6704            .expect("first cursor boundary should settle");
6705        let first_snapshot = simulation.snapshot();
6706        assert_eq!(first_snapshot.admitted_attempt_count, 2);
6707        assert_eq!(first_snapshot.admitted_command_count, 1);
6708        assert_eq!(first_snapshot.admitted_event_count, 1);
6709
6710        assert!(matches!(
6711            simulation
6712                .process_command(morale_request(3, 3, 70))
6713                .expect("second command should be accepted"),
6714            CommandOutcome::Accepted { .. }
6715        ));
6716        simulation
6717            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
6718            .expect("second cursor boundary should settle");
6719        let current_snapshot = simulation.snapshot();
6720        assert_eq!(current_snapshot.admission_cursor_format_version, 1);
6721        assert_eq!(current_snapshot.admitted_attempt_count, 3);
6722        assert_eq!(current_snapshot.admitted_command_count, 2);
6723        assert_eq!(current_snapshot.admitted_event_count, 2);
6724
6725        let restored = Simulation::from_snapshot(current_snapshot.clone())
6726            .expect("persisted admission cursors should load");
6727        assert_eq!(restored.snapshot(), current_snapshot);
6728        let replayed = Simulation::replay_from_journal(scenario, &[], &simulation.replay_journal())
6729            .expect("admission cursors should reproduce under exact replay");
6730        assert_eq!(replayed.snapshot(), current_snapshot);
6731
6732        let mut legacy_value = serde_json::to_value(current_snapshot.clone())
6733            .expect("cursor migration fixture should serialize");
6734        let legacy_object = legacy_value
6735            .as_object_mut()
6736            .expect("cursor migration snapshot should be an object");
6737        legacy_object.remove("admission_cursor_format_version");
6738        legacy_object.remove("admitted_attempt_count");
6739        legacy_object.remove("admitted_command_count");
6740        legacy_object.remove("admitted_event_count");
6741        let migrated = Simulation::from_snapshot_json(
6742            &serde_json::to_string(&legacy_value).expect("cursor migration fixture should encode"),
6743        )
6744        .expect("legacy admission cursors should derive from boundary prefixes");
6745        assert_eq!(migrated.snapshot(), current_snapshot);
6746
6747        let mut tampered_cursor = current_snapshot.clone();
6748        tampered_cursor.admitted_attempt_count -= 1;
6749        let error = Simulation::from_snapshot(tampered_cursor)
6750            .err()
6751            .expect("a cursor detached from boundary evidence must not load");
6752        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
6753        assert!(error.message.contains("admission cursors"));
6754
6755        let mut migrated_gap = current_snapshot;
6756        migrated_gap.boundaries[0].admitted_attempts.remove(0);
6757        migrated_gap.admission_cursor_format_version = 0;
6758        migrated_gap.admitted_attempt_count = 0;
6759        migrated_gap.admitted_command_count = 0;
6760        migrated_gap.admitted_event_count = 0;
6761        rehash_tampered_snapshot(&mut migrated_gap);
6762        let error = Simulation::from_snapshot(migrated_gap)
6763            .err()
6764            .expect("legacy cursor migration must reject a journal-prefix gap");
6765        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
6766    }
6767
6768    #[test]
6769    fn expected_domain_rejections_survive_load_and_exact_replay() {
6770        let (scenario, ids) = demo_scenario();
6771        let mut simulation =
6772            Simulation::new(35, scenario.clone()).expect("compatibility run should load");
6773        simulation
6774            .register_plugin(&AuthorityPlugin)
6775            .expect("payload-validation plugin should register");
6776        let requests = [
6777            (
6778                CommandRequestId::new(1),
6779                CommandEnvelope::new(
6780                    Issuer::Actor(ids.commander),
6781                    Command::MoveArmy {
6782                        army: ArmyId::new(999),
6783                        destination: ids.eastern_territory,
6784                    },
6785                ),
6786            ),
6787            (
6788                CommandRequestId::new(2),
6789                CommandEnvelope::new(
6790                    Issuer::Debug,
6791                    Command::DebugSetArmyMorale {
6792                        army: ids.army,
6793                        morale: 101,
6794                    },
6795                ),
6796            ),
6797            (
6798                CommandRequestId::new(3),
6799                CommandEnvelope::new(
6800                    Issuer::Actor(ids.commander),
6801                    Command::Plugin {
6802                        plugin: "authority-test".to_owned(),
6803                        command: "set_stance".to_owned(),
6804                        payload: serde_json::json!({}),
6805                    },
6806                ),
6807            ),
6808            (
6809                CommandRequestId::new(4),
6810                CommandEnvelope::new(
6811                    Issuer::Actor(ids.commander),
6812                    Command::Plugin {
6813                        plugin: "missing-plugin".to_owned(),
6814                        command: "missing-command".to_owned(),
6815                        payload: Value::Null,
6816                    },
6817                ),
6818            ),
6819        ];
6820        let expected = [
6821            ErrorCode::ArmyNotFound,
6822            ErrorCode::ValueOutOfRange,
6823            ErrorCode::InvalidPayload,
6824            ErrorCode::PluginCommandNotFound,
6825        ];
6826        for ((request_id, envelope), expected_code) in requests.into_iter().zip(expected) {
6827            let revision_before = simulation.revision();
6828            let outcome = simulation
6829                .process_command(CommandRequest::new(request_id, revision_before, envelope))
6830                .expect("expected domain rejection should be a command outcome");
6831            let CommandOutcome::Rejected { rejection } = outcome else {
6832                panic!("invalid command fixture must be rejected");
6833            };
6834            assert_eq!(rejection.error.code, expected_code);
6835            assert_eq!(rejection.retained_revision, revision_before + 1);
6836        }
6837        assert!(simulation.command_log().is_empty());
6838        assert_eq!(simulation.command_attempts().len(), 4);
6839        assert_eq!(simulation.revision(), 4);
6840
6841        let snapshot = simulation.snapshot();
6842        let restored = Simulation::from_snapshot(snapshot.clone())
6843            .expect("expected rejection evidence must not invalidate its own snapshot");
6844        assert_eq!(restored.snapshot(), snapshot);
6845        let journal = simulation.replay_journal();
6846        let replayed = Simulation::replay_from_journal(scenario, &[&AuthorityPlugin], &journal)
6847            .expect("expected rejection evidence should replay exactly");
6848        assert_eq!(simulation.snapshot(), replayed.snapshot());
6849    }
6850
6851    #[test]
6852    fn read_only_and_frozen_replay_ingress_are_not_interchangeable() {
6853        let (scenario, ids) = demo_scenario();
6854        let observer_configuration = RunConfiguration::read_only_observer();
6855        let observer_manifest = manifest_for_configuration(&scenario, &observer_configuration);
6856        let mut observer = Simulation::new_with_run_configuration(
6857            35,
6858            scenario.clone(),
6859            observer_manifest,
6860            observer_configuration,
6861        )
6862        .expect("read-only observer run should load");
6863        let before = observer.snapshot();
6864        let live_human = observer
6865            .process_command(CommandRequest::new(
6866                CommandRequestId::new(1),
6867                0,
6868                CommandEnvelope::new(
6869                    Issuer::Human("controller.human".to_owned()),
6870                    Command::MoveArmy {
6871                        army: ids.army,
6872                        destination: ids.eastern_territory,
6873                    },
6874                )
6875                .with_authority(CommandAuthority::for_actor(ids.commander)),
6876            ))
6877            .expect("read-only rejection should be structured");
6878        let CommandOutcome::Rejected { rejection } = live_human else {
6879            panic!("read-only observer must reject a live human command");
6880        };
6881        assert_eq!(rejection.error.code, ErrorCode::InteractionReadOnly);
6882        assert_eq!(observer.world(), before.world);
6883        assert_eq!(observer.events(), before.events);
6884        assert_eq!(observer.command_log(), before.commands);
6885        assert_eq!(observer.random_draws(), before.random_draws);
6886        assert_eq!(observer.command_attempts().len(), 1);
6887        let observer_journal = observer.replay_journal();
6888        let observer_replay =
6889            Simulation::replay_from_journal(scenario.clone(), &[], &observer_journal)
6890                .expect("read-only rejection evidence should replay exactly");
6891        assert_eq!(observer.snapshot(), observer_replay.snapshot());
6892
6893        let replay_configuration = RunConfiguration::replay_as_character(
6894            "seat.commander",
6895            "controller.recorded",
6896            ids.commander,
6897            "permission.military-command",
6898        );
6899        let replay_manifest = manifest_for_configuration(&scenario, &replay_configuration);
6900        let replay_envelope = CommandEnvelope::new(
6901            Issuer::Replay("controller.recorded".to_owned()),
6902            Command::MoveArmy {
6903                army: ids.army,
6904                destination: ids.eastern_territory,
6905            },
6906        )
6907        .with_authority(character_authority(
6908            ids.commander,
6909            ids.army,
6910            "seat.commander",
6911            "permission.military-command",
6912        ))
6913        .at_time(SimTime::EPOCH);
6914
6915        let mut live_replay = Simulation::new_with_run_configuration(
6916            35,
6917            scenario.clone(),
6918            replay_manifest.clone(),
6919            replay_configuration.clone(),
6920        )
6921        .expect("replay run should load");
6922        let outcome = live_replay
6923            .process_command(CommandRequest::new(
6924                CommandRequestId::new(1),
6925                0,
6926                replay_envelope.clone(),
6927            ))
6928            .expect("live replay forgery should be a structured rejection");
6929        let CommandOutcome::Rejected { rejection } = outcome else {
6930            panic!("a live caller cannot self-identify as frozen replay");
6931        };
6932        assert_eq!(rejection.error.code, ErrorCode::InvalidAuthority);
6933        assert!(live_replay.command_log().is_empty());
6934
6935        let mut frozen_source = Simulation::new_with_run_configuration(
6936            35,
6937            scenario.clone(),
6938            replay_manifest.clone(),
6939            replay_configuration.clone(),
6940        )
6941        .expect("frozen replay source should load");
6942        let outcome = frozen_source
6943            .admit_command(
6944                Some(CommandRequestId::new(7)),
6945                Some(0),
6946                replay_envelope,
6947                CommandIngress::FrozenReplay,
6948                true,
6949            )
6950            .expect("the trusted replay path should consume frozen input");
6951        assert!(matches!(outcome, CommandOutcome::Accepted { .. }));
6952        let Err(error) = Simulation::replay_with_run_configuration(
6953            35,
6954            scenario.clone(),
6955            replay_manifest,
6956            replay_configuration,
6957            &[],
6958            frozen_source.command_log(),
6959            frozen_source.command_attempts(),
6960            frozen_source.boundaries(),
6961            frozen_source.time(),
6962        ) else {
6963            panic!("caller-supplied fixture replay cannot consume frozen ingress");
6964        };
6965        assert_eq!(error.code, ErrorCode::ReplayEnvironmentMismatch);
6966        let frozen_journal = frozen_source.replay_journal();
6967        let frozen_replay = Simulation::replay_from_journal(scenario, &[], &frozen_journal)
6968            .expect("frozen controller input should replay exactly");
6969        assert_eq!(frozen_source.snapshot(), frozen_replay.snapshot());
6970
6971        let mut forged_live_ingress = frozen_source.snapshot();
6972        forged_live_ingress.command_attempts[0].ingress = CommandIngress::LiveRequest;
6973        refresh_snapshot_commitments_and_checkpoint(&mut forged_live_ingress);
6974        let Err(error) = Simulation::from_snapshot(forged_live_ingress) else {
6975            panic!("live ingress cannot be relabeled as an accepted replay command");
6976        };
6977        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
6978    }
6979
6980    #[test]
6981    fn observation_and_trace_policy_are_causally_inert() {
6982        let (scenario, _) = demo_scenario();
6983        let public_configuration = RunConfiguration::read_only_observer();
6984        let mut research_configuration = public_configuration.clone();
6985        research_configuration.observation = ObservationPolicy::ResearchFull;
6986        research_configuration.trace = TracePolicy::FullResearch;
6987        let mut public = Simulation::new_with_run_configuration(
6988            35,
6989            scenario.clone(),
6990            manifest_for_configuration(&scenario, &public_configuration),
6991            public_configuration,
6992        )
6993        .expect("public observer run should load");
6994        let mut research = Simulation::new_with_run_configuration(
6995            35,
6996            scenario.clone(),
6997            manifest_for_configuration(&scenario, &research_configuration),
6998            research_configuration,
6999        )
7000        .expect("research observer run should load");
7001
7002        assert_ne!(public.run_manifest_hash(), research.run_manifest_hash());
7003        assert_ne!(public.checkpoint_hash(), research.checkpoint_hash());
7004        assert_eq!(
7005            public
7006                .authoritative_state_hash()
7007                .expect("public state should hash"),
7008            research
7009                .authoritative_state_hash()
7010                .expect("research state should hash")
7011        );
7012        let public_receipt = public
7013            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
7014            .expect("public boundary should settle");
7015        let research_receipt = research
7016            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
7017            .expect("research boundary should settle");
7018        assert_eq!(public_receipt.boundary_hash, research_receipt.boundary_hash);
7019        assert_eq!(
7020            public.boundaries()[0].state_hash,
7021            research.boundaries()[0].state_hash
7022        );
7023        assert_eq!(public.world(), research.world());
7024        assert_eq!(public.random_draws(), research.random_draws());
7025        assert_eq!(
7026            public.snapshot().random_streams,
7027            research.snapshot().random_streams
7028        );
7029        assert_ne!(public.checkpoint_hash(), research.checkpoint_hash());
7030    }
7031
7032    #[test]
7033    fn invalid_command_does_not_mutate_any_serialized_state() {
7034        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
7035        let before = simulation
7036            .snapshot_json()
7037            .expect("snapshot should serialize");
7038        let result = simulation.submit(CommandEnvelope::new(
7039            Issuer::Actor(ids.observer),
7040            Command::MoveArmy {
7041                army: ids.army,
7042                destination: ids.eastern_territory,
7043            },
7044        ));
7045        assert_eq!(
7046            result.expect_err("observer cannot command army").code,
7047            ErrorCode::InvalidAuthority
7048        );
7049        assert_eq!(
7050            before,
7051            simulation
7052                .snapshot_json()
7053                .expect("snapshot should serialize")
7054        );
7055    }
7056
7057    #[test]
7058    fn movement_emits_events_and_executes_at_scheduled_time() {
7059        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
7060        let receipt = simulation
7061            .submit(move_order(&ids))
7062            .expect("order should validate");
7063        assert_eq!(receipt.emitted_events.len(), 1);
7064        simulation
7065            .advance(SimDuration::hours(17))
7066            .expect("time should advance");
7067        assert_eq!(
7068            simulation
7069                .world()
7070                .army(ids.army)
7071                .expect("army exists")
7072                .location,
7073            ids.central_territory
7074        );
7075        let events = simulation
7076            .advance(SimDuration::hours(1))
7077            .expect("arrival should execute");
7078        assert!(
7079            events
7080                .iter()
7081                .any(|event| matches!(event.kind, EventKind::ArmyArrived { .. }))
7082        );
7083        assert_eq!(
7084            simulation
7085                .world()
7086                .army(ids.army)
7087                .expect("army exists")
7088                .location,
7089            ids.eastern_territory
7090        );
7091    }
7092
7093    #[test]
7094    fn snapshot_rejects_event_correlation_drift() {
7095        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
7096        simulation
7097            .submit(move_order(&ids))
7098            .expect("movement command should commit");
7099        simulation
7100            .advance(SimDuration::hours(18))
7101            .expect("arrival work should execute");
7102
7103        let mut tampered = simulation.snapshot();
7104        let child_index = tampered
7105            .events
7106            .iter()
7107            .position(|event| matches!(event.cause, Some(CauseRef::Event(_))))
7108            .expect("the movement timeline should contain a child event");
7109        tampered.events[child_index].correlation_id = tampered.events[child_index]
7110            .correlation_id
7111            .checked_add(1)
7112            .expect("the fixture correlation should remain representable");
7113        refresh_snapshot_commitments_and_checkpoint(&mut tampered);
7114
7115        let Err(error) = Simulation::from_snapshot(tampered) else {
7116            panic!("an event child cannot silently change correlation chains");
7117        };
7118        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7119        assert!(error.message.contains("correlation"));
7120    }
7121
7122    #[test]
7123    fn snapshot_rejects_correlation_reuse_across_command_roots() {
7124        let (scenario, ids) = demo_scenario();
7125        let mut simulation = Simulation::new(35, scenario).expect("demo should load");
7126        let debug_command = |morale| {
7127            CommandEnvelope::new(
7128                Issuer::Debug,
7129                Command::DebugSetArmyMorale {
7130                    army: ids.army,
7131                    morale,
7132                },
7133            )
7134        };
7135        simulation
7136            .submit(debug_command(61))
7137            .expect("the first command should commit");
7138        simulation
7139            .submit(debug_command(62))
7140            .expect("the second command should commit");
7141
7142        let mut tampered = simulation.snapshot();
7143        tampered.events[1].correlation_id = tampered.events[0].correlation_id;
7144        refresh_snapshot_commitments_and_checkpoint(&mut tampered);
7145
7146        let Err(error) = Simulation::from_snapshot(tampered) else {
7147            panic!("one correlation cannot identify two command roots");
7148        };
7149        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7150        assert!(error.message.contains("unrelated causal roots"));
7151    }
7152
7153    #[test]
7154    fn snapshot_rejects_scheduled_plugin_correlation_drift() {
7155        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
7156        simulation
7157            .register_plugin(&FailingPlugin)
7158            .expect("the scheduling fixture plugin should register");
7159        simulation
7160            .submit(move_order(&ids))
7161            .expect("the movement command should commit");
7162        simulation
7163            .submit(CommandEnvelope::new(
7164                Issuer::Debug,
7165                Command::DebugSetArmyMorale {
7166                    army: ids.army,
7167                    morale: 61,
7168                },
7169            ))
7170            .expect("a second command should provide another committed correlation");
7171
7172        let order_event = simulation
7173            .events()
7174            .iter()
7175            .find(|event| matches!(event.kind, EventKind::MoveOrdered { .. }))
7176            .expect("the movement order event should exist");
7177        let arrival_at = SimTime::EPOCH
7178            .checked_add(SimDuration::hours(18))
7179            .expect("arrival time should be representable");
7180        simulation
7181            .schedule_at(
7182                arrival_at,
7183                ScheduledAction::PluginDirective {
7184                    plugin: "failing-test".to_owned(),
7185                    directive: Box::new(SystemDirective::SetComponent {
7186                        state: StateKey::new("failure-fixture", "flag"),
7187                        entity: EntityRef::Army(ids.army),
7188                        component: "flag".to_owned(),
7189                        value: Value::Bool(true),
7190                        summary: "A scheduled directive with forged provenance".to_owned(),
7191                    }),
7192                    allowed_writes: vec![StateKey::new("failure-fixture", "flag")],
7193                    cause: CauseRef::Event(order_event.id),
7194                    correlation_id: order_event
7195                        .correlation_id
7196                        .checked_add(1)
7197                        .expect("the fixture correlation should remain representable"),
7198                },
7199            )
7200            .expect("the future directive should be accepted into the scheduler");
7201
7202        let mut tampered = simulation.snapshot();
7203        refresh_snapshot_commitments_and_checkpoint(&mut tampered);
7204        let Err(error) = Simulation::from_snapshot_with_plugins(tampered, &[&FailingPlugin]) else {
7205            panic!("a scheduled directive must retain its cause correlation");
7206        };
7207        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7208        assert!(error.message.contains("event correlation"));
7209    }
7210
7211    #[test]
7212    fn internal_runtime_partitions_preserve_flat_persistence_contracts() {
7213        let (scenario, ids) = demo_scenario();
7214        let mut simulation =
7215            Simulation::new(35, scenario.clone()).expect("the demo scenario should load");
7216        simulation
7217            .submit(move_order(&ids))
7218            .expect("the move should populate evidence and scheduled work");
7219        simulation
7220            .advance(SimDuration::days(1))
7221            .expect("the scheduled move should complete");
7222
7223        let snapshot = simulation.snapshot();
7224        let snapshot_value =
7225            serde_json::to_value(&snapshot).expect("the snapshot should become JSON");
7226        let snapshot_object = snapshot_value
7227            .as_object()
7228            .expect("snapshot JSON should remain a flat object");
7229        for public_field in [
7230            "checkpoint_hash",
7231            "state_revision",
7232            "next_event_id",
7233            "admission_cursor_format_version",
7234            "events",
7235            "commands",
7236            "scheduled",
7237        ] {
7238            assert!(
7239                snapshot_object.contains_key(public_field),
7240                "snapshot should retain flat field {public_field}"
7241            );
7242        }
7243        for internal_owner in ["current", "metadata", "counters", "evidence", "scheduler"] {
7244            assert!(
7245                !snapshot_object.contains_key(internal_owner),
7246                "private owner {internal_owner} must not enter the snapshot wire shape"
7247            );
7248        }
7249
7250        let json = serde_json::to_string(&snapshot).expect("the snapshot should serialize");
7251        let restored =
7252            Simulation::from_snapshot_json(&json).expect("the flat snapshot should restore");
7253        assert_eq!(restored.snapshot(), snapshot);
7254
7255        let journal = restored.replay_journal();
7256        let journal_value =
7257            serde_json::to_value(&journal).expect("the replay journal should become JSON");
7258        let journal_object = journal_value
7259            .as_object()
7260            .expect("replay journal JSON should remain a flat object");
7261        for internal_owner in ["current", "metadata", "counters", "evidence", "scheduler"] {
7262            assert!(
7263                !journal_object.contains_key(internal_owner),
7264                "private owner {internal_owner} must not enter the journal wire shape"
7265            );
7266        }
7267        let replayed = Simulation::replay_from_journal(scenario, &[], &journal)
7268            .expect("the flat replay journal should remain exact");
7269        assert_eq!(replayed.snapshot(), snapshot);
7270    }
7271
7272    #[test]
7273    fn domain_commitments_migrate_replay_and_reject_each_tampered_root() {
7274        let (scenario, ids) = demo_scenario();
7275        let mut simulation =
7276            Simulation::new(97, scenario.clone()).expect("the commitment fixture should load");
7277        simulation
7278            .submit(move_order(&ids))
7279            .expect("the commitment fixture should accept its command");
7280        simulation
7281            .advance(SimDuration::days(1))
7282            .expect("the commitment fixture should execute scheduled work");
7283        let snapshot = simulation.snapshot();
7284        assert_eq!(
7285            snapshot.commitment_format_version,
7286            COMMITMENT_FORMAT_VERSION
7287        );
7288        assert!(commitment_roots_are_canonical(
7289            snapshot
7290                .commitment_roots
7291                .as_ref()
7292                .expect("current snapshots should persist domain roots")
7293        ));
7294
7295        let expected_roots = snapshot_commitment_roots(&snapshot)
7296            .expect("the canonical snapshot should produce roots");
7297        let mut reordered = snapshot.clone();
7298        reordered.world.people.reverse();
7299        reordered.world.governments.reverse();
7300        reordered.world.territories.reverse();
7301        reordered.world.routes.reverse();
7302        reordered.world.armies.reverse();
7303        reordered.events.reverse();
7304        reordered.commands.reverse();
7305        reordered.command_attempts.reverse();
7306        reordered.ingress.reverse();
7307        reordered.plugin_components.reverse();
7308        reordered.domain_records.reverse();
7309        reordered.plugin_descriptors.reverse();
7310        reordered.random_streams.reverse();
7311        reordered.random_draws.reverse();
7312        reordered.scheduled.reverse();
7313        assert_eq!(
7314            snapshot_commitment_roots(&reordered)
7315                .expect("collection insertion order should not affect roots"),
7316            expected_roots
7317        );
7318
7319        for root_name in [
7320            "world",
7321            "knowledge",
7322            "plugin_components",
7323            "domain_records",
7324            "scheduler",
7325            "commands",
7326            "events",
7327            "ingress",
7328            "random",
7329            "boundary_chain",
7330            "identity",
7331            "control",
7332        ] {
7333            let mut forged = snapshot.clone();
7334            let mut roots_value = serde_json::to_value(
7335                forged
7336                    .commitment_roots
7337                    .as_ref()
7338                    .expect("the fixture should persist roots"),
7339            )
7340            .expect("commitment roots should become JSON");
7341            roots_value
7342                .as_object_mut()
7343                .expect("commitment roots should be an object")
7344                .insert(root_name.to_owned(), Value::String("0".repeat(64)));
7345            forged.commitment_roots = Some(
7346                serde_json::from_value(roots_value)
7347                    .expect("the forged commitment roots should deserialize"),
7348            );
7349            forged.checkpoint_hash = snapshot_checkpoint_hash(&forged)
7350                .expect("the forged roots should produce a coherent outer checkpoint");
7351            let error = Simulation::from_snapshot(forged)
7352                .err()
7353                .expect("every forged domain root must be rejected");
7354            assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7355            assert!(error.message.contains("commitment roots"));
7356        }
7357
7358        let mut legacy_snapshot = snapshot.clone();
7359        downgrade_snapshot_commitments(&mut legacy_snapshot);
7360        legacy_snapshot.checkpoint_hash = snapshot_checkpoint_hash(&legacy_snapshot)
7361            .expect("the legacy fixture should reproduce checkpoint v3");
7362        let legacy_checkpoint = legacy_snapshot.checkpoint_hash.clone();
7363        let migrated = Simulation::from_snapshot(legacy_snapshot.clone())
7364            .expect("a verified legacy checkpoint should derive current roots");
7365        assert_eq!(
7366            migrated.snapshot().commitment_format_version,
7367            COMMITMENT_FORMAT_VERSION
7368        );
7369        assert_ne!(migrated.checkpoint_hash(), legacy_checkpoint);
7370
7371        let mut tampered_legacy = legacy_snapshot;
7372        tampered_legacy.world.armies[0].morale += 1;
7373        let error = Simulation::from_snapshot(tampered_legacy)
7374            .err()
7375            .expect("migration must verify the old checkpoint before deriving roots");
7376        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7377        assert!(error.message.contains("pre-commitment state"));
7378
7379        let mut legacy_journal = simulation.replay_journal();
7380        legacy_journal.commitment_format_version = 0;
7381        legacy_journal.checkpoint_hash = legacy_checkpoint;
7382        let replayed = Simulation::replay_from_journal(scenario, &[], &legacy_journal)
7383            .expect("legacy commitment journals should replay under checkpoint v3");
7384        assert_eq!(replayed.snapshot().commitment_format_version, 0);
7385        assert!(replayed.snapshot().commitment_roots.is_none());
7386        assert_eq!(replayed.checkpoint_hash(), legacy_journal.checkpoint_hash);
7387    }
7388
7389    #[test]
7390    fn boundary_state_commitments_are_incremental_versioned_and_legacy_replayable() {
7391        let (scenario, _) = demo_scenario();
7392        let configuration = RunConfiguration::read_only_observer();
7393        let manifest = RunManifest::declared(
7394            ArtifactManifest::for_scenario("canwu.test", "boundary-state-fixture", "1", &scenario)
7395                .expect("the boundary-state scenario should hash"),
7396            ArtifactManifest::for_run_configuration(
7397                "canwu.test",
7398                "boundary-state-run",
7399                "1",
7400                &configuration,
7401            )
7402            .expect("the boundary-state run configuration should hash"),
7403        );
7404
7405        let mut current = Simulation::new_with_run_configuration(
7406            211,
7407            scenario.clone(),
7408            manifest.clone(),
7409            configuration.clone(),
7410        )
7411        .expect("the current boundary-state fixture should load");
7412        current
7413            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
7414            .expect("a current boundary should settle");
7415        let current_hash = current.boundaries()[0]
7416            .state_hash
7417            .as_deref()
7418            .expect("current boundaries should commit their state");
7419        let current_digest = current_hash
7420            .strip_prefix(BOUNDARY_STATE_HASH_V1_PREFIX)
7421            .expect("current boundaries should use the tagged commitment contract");
7422        assert!(is_canonical_hash(current_digest));
7423        let current_snapshot = current.snapshot();
7424        assert_eq!(
7425            current_snapshot.boundaries[0].state_hash.as_deref(),
7426            Some(
7427                snapshot_boundary_head_state_hash(&current_snapshot)
7428                    .expect("the current boundary head should reproduce from persisted roots")
7429                    .as_str()
7430            )
7431        );
7432        let current_restored = Simulation::from_snapshot(current_snapshot.clone())
7433            .expect("the current boundary commitment should load");
7434        assert_eq!(current_restored.snapshot(), current_snapshot);
7435        let current_replayed =
7436            Simulation::replay_from_journal(scenario.clone(), &[], &current.replay_journal())
7437                .expect("the current boundary commitment should replay exactly");
7438        assert_eq!(current_replayed.snapshot(), current_snapshot);
7439        let mut mislabeled_journal = current.replay_journal();
7440        mislabeled_journal.commitment_format_version = 0;
7441        let error = Simulation::replay_from_journal(scenario.clone(), &[], &mislabeled_journal)
7442            .err()
7443            .expect("a current boundary commitment cannot use a legacy journal contract");
7444        assert_eq!(error.code, ErrorCode::ReplayEnvironmentMismatch);
7445
7446        let mut forged_state = current_snapshot.clone();
7447        forged_state.world.armies[0].morale += 1;
7448        refresh_snapshot_commitments_and_checkpoint(&mut forged_state);
7449        let error = Simulation::from_snapshot(forged_state)
7450            .err()
7451            .expect("coherently rehashed current state must still match its boundary head");
7452        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7453        assert!(error.message.contains("boundary-head state commitment"));
7454
7455        let mut unsupported = current_snapshot;
7456        unsupported.boundaries[0].state_hash = Some(format!("v2:{}", "0".repeat(64)));
7457        rehash_tampered_snapshot(&mut unsupported);
7458        let error = Simulation::from_snapshot(unsupported)
7459            .err()
7460            .expect("unknown boundary state commitment tags must be rejected");
7461        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7462        assert!(error.message.contains("boundary state commitment"));
7463
7464        let mut legacy =
7465            Simulation::new_with_run_configuration(223, scenario.clone(), manifest, configuration)
7466                .expect("the legacy boundary-state fixture should load");
7467        legacy
7468            .settle_boundary_with_state_hash_format(
7469                BoundaryRequest::at(SimTime::EPOCH),
7470                BoundaryStateHashFormat::LegacyV0,
7471            )
7472            .expect("a legacy boundary should remain reproducible");
7473        let legacy_hash = legacy.boundaries()[0]
7474            .state_hash
7475            .as_deref()
7476            .expect("legacy declared boundaries should commit their state");
7477        assert!(is_canonical_hash(legacy_hash));
7478        let legacy_snapshot = legacy.snapshot();
7479        let mut mixed = Simulation::from_snapshot(legacy_snapshot.clone())
7480            .expect("an existing legacy boundary commitment should still load");
7481        let legacy_replayed =
7482            Simulation::replay_from_journal(scenario.clone(), &[], &legacy.replay_journal())
7483                .expect("an existing legacy boundary commitment should replay exactly");
7484        assert_eq!(legacy_replayed.snapshot(), legacy_snapshot);
7485
7486        mixed
7487            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH + SimDuration::days(1)))
7488            .expect("continuation should append the current commitment contract");
7489        assert!(is_canonical_hash(
7490            mixed.boundaries()[0]
7491                .state_hash
7492                .as_deref()
7493                .expect("the legacy boundary should retain its state commitment")
7494        ));
7495        assert!(
7496            mixed.boundaries()[1]
7497                .state_hash
7498                .as_deref()
7499                .expect("the continued boundary should commit its state")
7500                .starts_with(BOUNDARY_STATE_HASH_V1_PREFIX)
7501        );
7502        let mixed_snapshot = mixed.snapshot();
7503        let mixed_restored = Simulation::from_snapshot(mixed_snapshot.clone())
7504            .expect("a mixed legacy/current boundary chain should load");
7505        assert_eq!(mixed_restored.snapshot(), mixed_snapshot);
7506        let mixed_replayed =
7507            Simulation::replay_from_journal(scenario, &[], &mixed.replay_journal())
7508                .expect("a mixed legacy/current boundary chain should replay exactly");
7509        assert_eq!(mixed_replayed.snapshot(), mixed_snapshot);
7510    }
7511
7512    #[test]
7513    fn cached_mutable_commitments_match_independent_snapshot_roots_after_each_mutation() {
7514        fn assert_exact(simulation: &Simulation) {
7515            let snapshot = simulation.snapshot();
7516            let expected = snapshot_commitment_roots(&snapshot)
7517                .expect("serialized state should independently reproduce every commitment root");
7518            assert_eq!(snapshot.commitment_roots.as_ref(), Some(&expected));
7519            let cache = simulation
7520                .state
7521                .metadata
7522                .commitment_cache
7523                .as_ref()
7524                .expect("current runtimes should maintain a private commitment cache");
7525            assert!(
7526                [
7527                    &cache.world,
7528                    &cache.knowledge,
7529                    &cache.plugin_components,
7530                    &cache.domain_records,
7531                    &cache.scheduler,
7532                    &cache.random_streams,
7533                    &cache.identity,
7534                ]
7535                .into_iter()
7536                .all(Option::is_some),
7537                "every invalidated domain must be refreshed before a transaction commits"
7538            );
7539        }
7540
7541        let (scenario, ids) = demo_scenario();
7542        let mut simulation =
7543            Simulation::new(101, scenario.clone()).expect("cache fixture should load");
7544        assert_exact(&simulation);
7545        simulation
7546            .register_plugin(&AuthorityPlugin)
7547            .expect("component plugin should register");
7548        assert_exact(&simulation);
7549        simulation
7550            .register_plugin(&PrimaryRandomPlugin)
7551            .expect("random plugin should register");
7552        assert_exact(&simulation);
7553
7554        let before_rejection = simulation
7555            .snapshot()
7556            .commitment_roots
7557            .expect("current snapshots should have roots");
7558        let rejected = simulation
7559            .process_command(CommandRequest::new(
7560                CommandRequestId::new(1),
7561                simulation.revision() + 1,
7562                CommandEnvelope::new(
7563                    Issuer::Debug,
7564                    Command::DebugSetArmyMorale {
7565                        army: ids.army,
7566                        morale: 75,
7567                    },
7568                ),
7569            ))
7570            .expect("stale input should become deterministic rejection evidence");
7571        assert!(matches!(rejected, CommandOutcome::Rejected { .. }));
7572        assert_exact(&simulation);
7573        let after_rejection = simulation
7574            .snapshot()
7575            .commitment_roots
7576            .expect("current snapshots should have roots");
7577        assert_eq!(before_rejection.world, after_rejection.world);
7578        assert_eq!(before_rejection.knowledge, after_rejection.knowledge);
7579        assert_eq!(
7580            before_rejection.plugin_components,
7581            after_rejection.plugin_components
7582        );
7583        assert_eq!(
7584            before_rejection.domain_records,
7585            after_rejection.domain_records
7586        );
7587        assert_eq!(before_rejection.scheduler, after_rejection.scheduler);
7588        assert_eq!(before_rejection.random, after_rejection.random);
7589        assert_eq!(before_rejection.identity, after_rejection.identity);
7590        assert_ne!(before_rejection.commands, after_rejection.commands);
7591        assert_ne!(before_rejection.control, after_rejection.control);
7592
7593        simulation
7594            .process_command(CommandRequest::new(
7595                CommandRequestId::new(2),
7596                simulation.revision(),
7597                CommandEnvelope::new(
7598                    Issuer::Actor(ids.commander),
7599                    Command::Plugin {
7600                        plugin: "authority-test".to_owned(),
7601                        command: "set_stance".to_owned(),
7602                        payload: Value::Null,
7603                    },
7604                ),
7605            ))
7606            .expect("component command should commit");
7607        assert_exact(&simulation);
7608        simulation
7609            .process_command(CommandRequest::new(
7610                CommandRequestId::new(3),
7611                simulation.revision(),
7612                move_order(&ids),
7613            ))
7614            .expect("movement command should commit");
7615        assert_exact(&simulation);
7616        simulation
7617            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
7618            .expect("random boundary should commit");
7619        assert_exact(&simulation);
7620        simulation
7621            .advance(SimDuration::days(1))
7622            .expect("scheduled arrival should commit");
7623        assert_exact(&simulation);
7624
7625        let mut records = Simulation::new(103, scenario).expect("record cache fixture should load");
7626        records
7627            .register_plugin(&RecordLifecyclePlugin)
7628            .expect("record plugin should register");
7629        assert_exact(&records);
7630        records
7631            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
7632            .expect("record mutation boundary should commit");
7633        assert_exact(&records);
7634    }
7635
7636    #[test]
7637    fn rejection_transaction_restores_private_commitment_state_after_hash_failure() {
7638        let (scenario, ids) = demo_scenario();
7639        let mut simulation =
7640            Simulation::new(107, scenario).expect("rejection rollback fixture should load");
7641        let before = simulation.snapshot();
7642        simulation
7643            .state
7644            .metadata
7645            .commitment_cache
7646            .as_mut()
7647            .expect("current runtimes should maintain a commitment cache")
7648            .attempts
7649            .len = 2;
7650
7651        let error = simulation
7652            .process_command(CommandRequest::new(
7653                CommandRequestId::new(1),
7654                0,
7655                CommandEnvelope::new(
7656                    Issuer::Debug,
7657                    Command::DebugSetArmyMorale {
7658                        army: ids.army,
7659                        morale: 101,
7660                    },
7661                ),
7662            ))
7663            .expect_err("a fatal commitment-cache mismatch must abort the rejection transaction");
7664        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7665        assert_eq!(simulation.snapshot(), before);
7666        let restored_cache = simulation
7667            .state
7668            .metadata
7669            .commitment_cache
7670            .as_ref()
7671            .expect("rollback should restore the private cache");
7672        assert_eq!(restored_cache.attempts.len, 2);
7673        assert!(simulation.command_attempts().is_empty());
7674        assert_eq!(simulation.state.counters.next_command_attempt_id, 1);
7675        assert_eq!(simulation.revision(), 0);
7676
7677        simulation.state.metadata.commitment_cache = None;
7678        simulation
7679            .refresh_checkpoint_hash()
7680            .expect("discarding the injected corrupt cache should rebuild it from evidence");
7681        let outcome = simulation
7682            .process_command(CommandRequest::new(
7683                CommandRequestId::new(1),
7684                0,
7685                CommandEnvelope::new(
7686                    Issuer::Debug,
7687                    Command::DebugSetArmyMorale {
7688                        army: ids.army,
7689                        morale: 101,
7690                    },
7691                ),
7692            ))
7693            .expect("the repaired runtime should persist the same expected rejection");
7694        assert!(matches!(outcome, CommandOutcome::Rejected { .. }));
7695        let snapshot = simulation.snapshot();
7696        assert_eq!(
7697            snapshot.commitment_roots,
7698            Some(
7699                snapshot_commitment_roots(&snapshot)
7700                    .expect("the repaired rejection should independently reproduce its roots")
7701            )
7702        );
7703    }
7704
7705    #[test]
7706    fn ingress_transaction_restores_queue_and_private_commitments_after_hash_failure() {
7707        let (scenario, _) = demo_scenario();
7708        let mut simulation =
7709            Simulation::new(108, scenario).expect("ingress rollback fixture should load");
7710        let before = simulation.snapshot();
7711        simulation
7712            .state
7713            .metadata
7714            .commitment_cache
7715            .as_mut()
7716            .expect("current runtimes should maintain a commitment cache")
7717            .ingress
7718            .len = 2;
7719        let cache_before = cache_fingerprint(&simulation);
7720
7721        let error = simulation
7722            .schedule_calendar_boundary(SimTime::EPOCH, vec![SystemCadence::Daily])
7723            .expect_err("a fatal commitment-cache mismatch must abort ingress insertion");
7724        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7725        assert_eq!(simulation.snapshot(), before);
7726        assert_eq!(cache_fingerprint(&simulation), cache_before);
7727        let restored_cache = simulation
7728            .state
7729            .metadata
7730            .commitment_cache
7731            .as_ref()
7732            .expect("rollback should restore the private cache");
7733        assert_eq!(restored_cache.ingress.len, 2);
7734        assert!(simulation.ingress_log().is_empty());
7735        assert!(simulation.state.scheduler.pending_ingress.is_empty());
7736        assert_eq!(simulation.state.counters.next_ingress_id, 1);
7737
7738        simulation.state.metadata.commitment_cache = None;
7739        simulation
7740            .refresh_checkpoint_hash()
7741            .expect("discarding the injected corrupt cache should rebuild it from evidence");
7742        let receipt = simulation
7743            .schedule_calendar_boundary(SimTime::EPOCH, vec![SystemCadence::Daily])
7744            .expect("the repaired runtime should queue the same calendar boundary");
7745        assert_eq!(receipt.ingress_id, IngressId::new(1));
7746        let snapshot = simulation.snapshot();
7747        assert_eq!(
7748            snapshot.commitment_roots,
7749            Some(
7750                snapshot_commitment_roots(&snapshot)
7751                    .expect("the repaired ingress should independently reproduce its roots")
7752            )
7753        );
7754    }
7755
7756    #[test]
7757    fn command_transaction_restores_writable_domains_after_hash_failure() {
7758        let (scenario, ids) = demo_scenario();
7759        let mut simulation =
7760            Simulation::new(109, scenario).expect("command rollback fixture should load");
7761        let before = simulation.snapshot();
7762        simulation
7763            .state
7764            .metadata
7765            .commitment_cache
7766            .as_mut()
7767            .expect("current runtimes should maintain a commitment cache")
7768            .commands
7769            .len = 2;
7770        let request = || {
7771            CommandRequest::new(
7772                CommandRequestId::new(1),
7773                0,
7774                CommandEnvelope::new(
7775                    Issuer::Debug,
7776                    Command::DebugSetArmyMorale {
7777                        army: ids.army,
7778                        morale: 73,
7779                    },
7780                ),
7781            )
7782        };
7783
7784        let error = simulation
7785            .process_command(request())
7786            .expect_err("a fatal commitment-cache mismatch must abort command application");
7787        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7788        assert_eq!(simulation.snapshot(), before);
7789        let restored_cache = simulation
7790            .state
7791            .metadata
7792            .commitment_cache
7793            .as_ref()
7794            .expect("rollback should restore the private cache");
7795        assert_eq!(restored_cache.commands.len, 2);
7796
7797        simulation.state.metadata.commitment_cache = None;
7798        simulation
7799            .refresh_checkpoint_hash()
7800            .expect("discarding the injected corrupt cache should rebuild it from evidence");
7801        let outcome = simulation
7802            .process_command(request())
7803            .expect("the repaired runtime should accept the same command");
7804        assert!(matches!(outcome, CommandOutcome::Accepted { .. }));
7805        let snapshot = simulation.snapshot();
7806        assert_eq!(
7807            snapshot.commitment_roots,
7808            Some(
7809                snapshot_commitment_roots(&snapshot)
7810                    .expect("the repaired command should independently reproduce its roots")
7811            )
7812        );
7813    }
7814
7815    #[test]
7816    fn checkpoint_journals_are_incremental_contiguous_and_exact() {
7817        let (scenario, _) = demo_scenario();
7818        let plugins: &[&dyn SimulationPlugin] = &[&JournalCommandPlugin, &BoundaryRollbackPlugin];
7819        let mut simulation =
7820            Simulation::new(35, scenario.clone()).expect("checkpoint fixture should load");
7821        for plugin in plugins {
7822            simulation
7823                .register_plugin(*plugin)
7824                .expect("checkpoint fixture plugin should register");
7825        }
7826        simulation
7827            .enqueue_command(
7828                SimTime::EPOCH,
7829                0,
7830                CommandRequest::new(
7831                    CommandRequestId::new(1),
7832                    0,
7833                    CommandEnvelope::new(
7834                        Issuer::Debug,
7835                        Command::Plugin {
7836                            plugin: "journal-command".to_owned(),
7837                            command: "noop".to_owned(),
7838                            payload: Value::Null,
7839                        },
7840                    ),
7841                ),
7842            )
7843            .expect("checkpoint fixture command should queue");
7844        simulation
7845            .step_canonical()
7846            .expect("the first canonical boundary should settle")
7847            .expect("the queued command should produce a boundary");
7848        let first_cursor = simulation
7849            .evidence_cursor()
7850            .expect("the first journal cursor should be representable");
7851        let first_segment = simulation
7852            .journal_segment_since(EvidenceCursor::default())
7853            .expect("the first evidence segment should export");
7854        assert_eq!(first_segment.start, EvidenceCursor::default());
7855        assert_eq!(first_segment.end, first_cursor);
7856
7857        simulation
7858            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
7859            .expect("the random and ingress-producing boundary should settle");
7860        simulation
7861            .advance_canonical(SimDuration::hours(1))
7862            .expect("generated ingress should enter a later boundary");
7863        let checkpoint = simulation
7864            .checkpoint()
7865            .expect("current state should checkpoint without evidence cloning");
7866        assert_eq!(checkpoint.format_version, CHECKPOINT_JOURNAL_FORMAT_VERSION);
7867        assert!(checkpoint.state.events.is_empty());
7868        assert!(checkpoint.state.commands.is_empty());
7869        assert!(checkpoint.state.command_attempts.is_empty());
7870        assert!(checkpoint.state.ingress.is_empty());
7871        assert!(checkpoint.state.boundaries.is_empty());
7872        assert!(checkpoint.state.random_draws.is_empty());
7873        assert_eq!(
7874            checkpoint.journal_end,
7875            simulation
7876                .evidence_cursor()
7877                .expect("the final journal cursor should be representable")
7878        );
7879        let second_segment = simulation
7880            .journal_segment_since(first_cursor)
7881            .expect("only evidence after the first checkpoint should export");
7882        assert_eq!(second_segment.start, first_cursor);
7883        assert_eq!(second_segment.end, checkpoint.journal_end);
7884        assert!(!second_segment.events.is_empty());
7885        assert!(!second_segment.ingress.is_empty());
7886        assert!(!second_segment.boundaries.is_empty());
7887        assert!(!second_segment.random_draws.is_empty());
7888
7889        let bundle = CheckpointJournal {
7890            checkpoint: checkpoint.clone(),
7891            segments: vec![first_segment.clone(), second_segment.clone()],
7892        };
7893        let restored = Simulation::from_checkpoint_journal_with_plugins(bundle, plugins)
7894            .expect("contiguous evidence segments should restore exact current state");
7895        assert_eq!(restored.snapshot(), simulation.snapshot());
7896        let replayed =
7897            Simulation::replay_from_journal(scenario.clone(), plugins, &restored.replay_journal())
7898                .expect("checkpoint-journal restoration should retain exact replay evidence");
7899        assert_eq!(replayed.snapshot(), simulation.snapshot());
7900
7901        let json = simulation
7902            .checkpoint_journal_json()
7903            .expect("a portable checkpoint-journal bundle should serialize");
7904        let json_restored = Simulation::from_checkpoint_journal_json_with_plugins(&json, plugins)
7905            .expect("the portable checkpoint-journal bundle should restore");
7906        assert_eq!(json_restored.snapshot(), simulation.snapshot());
7907        assert!(
7908            serde_json::to_vec(&checkpoint)
7909                .expect("checkpoint should serialize")
7910                .len()
7911                < serde_json::to_vec(&simulation.snapshot())
7912                    .expect("flat snapshot should serialize")
7913                    .len(),
7914            "the current-state checkpoint must not duplicate accumulated evidence",
7915        );
7916
7917        let error = Simulation::from_checkpoint_and_journal(
7918            checkpoint.clone(),
7919            vec![second_segment.clone()],
7920        )
7921        .err()
7922        .expect("a journal gap must be rejected");
7923        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7924
7925        let error = Simulation::from_checkpoint_and_journal(
7926            checkpoint.clone(),
7927            vec![first_segment.clone(), first_segment.clone()],
7928        )
7929        .err()
7930        .expect("a duplicated journal segment must be rejected");
7931        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7932
7933        let mut inconsistent_end = second_segment.clone();
7934        inconsistent_end.end.event_count += 1;
7935        let error = Simulation::from_checkpoint_and_journal(
7936            checkpoint.clone(),
7937            vec![first_segment.clone(), inconsistent_end],
7938        )
7939        .err()
7940        .expect("a forged segment end must be rejected");
7941        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7942
7943        let mut tampered_segment = second_segment;
7944        tampered_segment.events[0].summary.push_str(" (tampered)");
7945        let error = Simulation::from_checkpoint_and_journal(
7946            checkpoint.clone(),
7947            vec![first_segment.clone(), tampered_segment],
7948        )
7949        .err()
7950        .expect("checkpoint roots must reject tampered archived evidence");
7951        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7952
7953        let mut duplicated_evidence = checkpoint.clone();
7954        duplicated_evidence
7955            .state
7956            .commands
7957            .push(first_segment.commands[0].clone());
7958        let error = Simulation::from_checkpoint_and_journal(
7959            duplicated_evidence,
7960            vec![first_segment.clone()],
7961        )
7962        .err()
7963        .expect("checkpoint state must not duplicate archived evidence");
7964        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7965
7966        let mut unsupported = checkpoint.clone();
7967        unsupported.format_version += 1;
7968        let error =
7969            Simulation::from_checkpoint_and_journal(unsupported, vec![first_segment.clone()])
7970                .err()
7971                .expect("unknown checkpoint-journal formats must be rejected");
7972        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7973
7974        let mut future = checkpoint.journal_end;
7975        future.event_count += 1;
7976        let error = simulation
7977            .journal_segment_since(future)
7978            .expect_err("a future journal cursor must be rejected");
7979        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
7980
7981        let empty = Simulation::new(37, scenario).expect("empty checkpoint fixture should load");
7982        let empty_bundle = empty
7983            .checkpoint_journal()
7984            .expect("an empty run should still checkpoint");
7985        assert!(empty_bundle.segments.is_empty());
7986        let empty_restored = Simulation::from_checkpoint_journal(empty_bundle)
7987            .expect("an empty journal prefix should restore without a synthetic segment");
7988        assert_eq!(empty_restored.snapshot(), empty.snapshot());
7989    }
7990
7991    #[test]
7992    fn compacted_live_journals_preserve_continuation_idempotency_and_exact_replay() {
7993        let (scenario, _) = demo_scenario();
7994        let plugins: &[&dyn SimulationPlugin] = &[&JournalCommandPlugin];
7995        let command = |request_id, revision| {
7996            CommandRequest::new(
7997                CommandRequestId::new(request_id),
7998                revision,
7999                CommandEnvelope::new(
8000                    Issuer::Debug,
8001                    Command::Plugin {
8002                        plugin: "journal-command".to_owned(),
8003                        command: "noop".to_owned(),
8004                        payload: Value::Null,
8005                    },
8006                ),
8007            )
8008        };
8009
8010        let mut simulation =
8011            Simulation::new(41, scenario.clone()).expect("compact fixture should load");
8012        simulation
8013            .register_plugin(&JournalCommandPlugin)
8014            .expect("compact fixture plugin should register");
8015        let first_request = command(1, 0);
8016        let first_ingress = simulation
8017            .enqueue_command(SimTime::EPOCH, 0, first_request.clone())
8018            .expect("the first compact fixture command should queue");
8019        simulation
8020            .step_canonical()
8021            .expect("the first compact fixture boundary should settle")
8022            .expect("queued work should produce a boundary");
8023        let first_hash = simulation.checkpoint_hash().to_owned();
8024        let first_cursor = simulation
8025            .evidence_cursor()
8026            .expect("the first compact cursor should be representable");
8027
8028        let mut compact = simulation
8029            .into_compacted()
8030            .expect("the complete runtime should enter compact mode");
8031        let first_segment = compact
8032            .seal_evidence()
8033            .expect("the first live tail should seal")
8034            .expect("the first live tail should contain evidence");
8035        assert_eq!(first_segment.start, EvidenceCursor::default());
8036        assert_eq!(first_segment.end, first_cursor);
8037        assert_eq!(compact.checkpoint_hash(), first_hash);
8038        assert_eq!(
8039            compact
8040                .enqueue_command(SimTime::EPOCH, 0, first_request.clone())
8041                .expect("an archived ingress retry should remain idempotent"),
8042            first_ingress
8043        );
8044
8045        let second_request = command(2, compact.revision());
8046        compact
8047            .enqueue_command(SimTime::EPOCH, 0, second_request)
8048            .expect("a new request should queue after sealing");
8049        compact
8050            .step_canonical()
8051            .expect("continuation after sealing should settle")
8052            .expect("the new request should produce a boundary");
8053        let second_segment = compact
8054            .seal_evidence()
8055            .expect("the continuation tail should seal")
8056            .expect("the continuation tail should contain evidence");
8057        assert_eq!(second_segment.start, first_cursor);
8058        assert_eq!(second_segment.end, compact.evidence_cursor().unwrap());
8059        assert!(
8060            compact
8061                .checkpoint()
8062                .expect("compacted current state should checkpoint")
8063                .state
8064                .events
8065                .is_empty()
8066        );
8067
8068        compact
8069            .schedule_calendar_boundary(SimTime::EPOCH, vec![SystemCadence::Daily])
8070            .expect("calendar work should remain available after compaction");
8071        compact
8072            .step_canonical()
8073            .expect("calendar continuation should settle")
8074            .expect("scheduled calendar work should produce a boundary");
8075        let calendar_segment = compact
8076            .seal_evidence()
8077            .expect("calendar continuation evidence should seal")
8078            .expect("calendar continuation should produce a segment");
8079        assert_eq!(calendar_segment.start, second_segment.end);
8080
8081        let segments = vec![
8082            first_segment.clone(),
8083            second_segment.clone(),
8084            calendar_segment.clone(),
8085        ];
8086        let snapshot = compact
8087            .snapshot_with_segments(segments.clone())
8088            .expect("the external archive should reconstruct a full snapshot");
8089        let restored = Simulation::from_snapshot_with_plugins(snapshot.clone(), plugins)
8090            .expect("the reconstructed snapshot should continue with exact plugins");
8091        assert_eq!(restored.snapshot(), snapshot);
8092        let replayed = Simulation::replay_from_journal(
8093            scenario.clone(),
8094            plugins,
8095            &compact
8096                .replay_journal_with_segments(segments.clone())
8097                .expect("the external archive should produce an exact replay journal"),
8098        )
8099        .expect("the compact archive should replay exactly");
8100        assert_eq!(replayed.snapshot(), snapshot);
8101
8102        let mut tampered = segments;
8103        tampered[0].commands[0].envelope.expected_time = Some(SimTime::from_minutes(1));
8104        let error = compact
8105            .snapshot_with_segments(tampered)
8106            .expect_err("tampered sealed evidence must fail checkpoint validation");
8107        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8108
8109        let mut emitting =
8110            Simulation::new(42, scenario.clone()).expect("emitting compact fixture should load");
8111        emitting
8112            .register_plugin(&ArchiveEmissionPlugin)
8113            .expect("emitting compact fixture plugin should register");
8114        emitting
8115            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
8116            .expect("the emitting boundary should settle");
8117        let mut emitting = emitting
8118            .into_compacted()
8119            .expect("the emitting runtime should enter compact mode");
8120        let error = emitting
8121            .seal_evidence()
8122            .expect_err("new boundary emissions remain pending admission");
8123        assert_eq!(error.code, ErrorCode::ArchiveNotReady);
8124        emitting
8125            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
8126            .expect("a later boundary should admit the emitted event");
8127        let first_emitting_segment = emitting
8128            .seal_evidence()
8129            .expect("admitted emitting evidence should seal")
8130            .expect("admitted emitting evidence should produce a segment");
8131        emitting
8132            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
8133            .expect("an emitting runtime should continue after sealing");
8134        let error = emitting
8135            .seal_evidence()
8136            .expect_err("the new emission should retain the admission frontier");
8137        assert_eq!(error.code, ErrorCode::ArchiveNotReady);
8138        emitting
8139            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
8140            .expect("the next boundary should admit the second emission");
8141        let second_emitting_segment = emitting
8142            .seal_evidence()
8143            .expect("the second admitted tail should seal")
8144            .expect("the second admitted tail should produce a segment");
8145        assert_eq!(second_emitting_segment.start, first_emitting_segment.end);
8146        emitting
8147            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
8148            .expect("post-seal continuation should preserve the event cursor");
8149
8150        let mut direct = Simulation::new(43, scenario).expect("direct compact fixture should load");
8151        direct
8152            .register_plugin(&JournalCommandPlugin)
8153            .expect("direct compact fixture plugin should register");
8154        let direct_request = command(11, 0);
8155        let direct_outcome = direct
8156            .process_command(direct_request.clone())
8157            .expect("the direct request should commit");
8158        let revision = direct.revision();
8159        let mut direct = direct
8160            .into_compacted()
8161            .expect("the direct runtime should enter compact mode");
8162        let error = direct
8163            .seal_evidence()
8164            .expect_err("unsettled command evidence should remain retained");
8165        assert_eq!(error.code, ErrorCode::ArchiveNotReady);
8166        assert_eq!(direct.revision(), revision);
8167        direct
8168            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
8169            .expect("the retained direct command should settle");
8170        direct
8171            .seal_evidence()
8172            .expect("settled direct evidence should seal")
8173            .expect("settled direct evidence should be returned");
8174        assert_eq!(
8175            direct
8176                .process_command(direct_request)
8177                .expect("an archived direct request retry should stay exact"),
8178            direct_outcome
8179        );
8180        assert_eq!(direct.revision(), revision + 1);
8181    }
8182
8183    #[test]
8184    fn simulation_view_resolves_retained_commands_and_events_by_id() {
8185        let (scenario, ids) = demo_scenario();
8186        let mut simulation = Simulation::new(44, scenario).expect("lookup fixture should load");
8187        let debug_command = |morale| {
8188            CommandEnvelope::new(
8189                Issuer::Debug,
8190                Command::DebugSetArmyMorale {
8191                    army: ids.army,
8192                    morale,
8193                },
8194            )
8195        };
8196        let first = simulation
8197            .submit(debug_command(61))
8198            .expect("the first lookup command should commit");
8199        let second = simulation
8200            .submit(debug_command(62))
8201            .expect("the second lookup command should commit");
8202        let reads = [StateKey::core_commands(), StateKey::core_events()];
8203        let view = simulation.plugin_view("lookup", &reads);
8204
8205        assert_eq!(
8206            view.command(first.command_id).unwrap().unwrap().id,
8207            first.command_id
8208        );
8209        assert_eq!(
8210            view.command(second.command_id).unwrap().unwrap().id,
8211            second.command_id
8212        );
8213        let first_event = *first
8214            .emitted_events
8215            .first()
8216            .expect("the first command should emit an event");
8217        let second_event = *second
8218            .emitted_events
8219            .first()
8220            .expect("the second command should emit an event");
8221        assert_eq!(view.event(first_event).unwrap().unwrap().id, first_event);
8222        assert_eq!(view.event(second_event).unwrap().unwrap().id, second_event);
8223        assert!(view.command(CommandId::new(0)).unwrap().is_none());
8224        assert!(view.event(EventId::new(0)).unwrap().is_none());
8225        assert!(view.command(CommandId::new(3)).unwrap().is_none());
8226        assert!(view.event(EventId::new(3)).unwrap().is_none());
8227    }
8228
8229    #[test]
8230    fn simulation_view_excludes_archived_ids_after_compaction() {
8231        let (scenario, ids) = demo_scenario();
8232        let mut simulation =
8233            Simulation::new(45, scenario).expect("archive lookup fixture should load");
8234        let debug_command = |morale| {
8235            CommandEnvelope::new(
8236                Issuer::Debug,
8237                Command::DebugSetArmyMorale {
8238                    army: ids.army,
8239                    morale,
8240                },
8241            )
8242        };
8243        let archived = simulation
8244            .submit(debug_command(63))
8245            .expect("the archived lookup command should commit");
8246        let retained = simulation
8247            .submit(debug_command(64))
8248            .expect("the retained lookup command should commit");
8249        let retained_command = simulation
8250            .state
8251            .evidence
8252            .commands
8253            .pop()
8254            .expect("the retained command should be in the live tail");
8255        let retained_event = simulation
8256            .state
8257            .evidence
8258            .events
8259            .pop()
8260            .expect("the retained event should be in the live tail");
8261        simulation.state.evidence.archived.command_count = 1;
8262        simulation.state.evidence.archived.event_count = 1;
8263        simulation.state.evidence.commands.clear();
8264        simulation.state.evidence.events.clear();
8265        simulation.state.evidence.commands.push(retained_command);
8266        simulation.state.evidence.events.push(retained_event);
8267        let reads = [StateKey::core_commands(), StateKey::core_events()];
8268        let view = simulation.plugin_view("lookup", &reads);
8269
8270        assert!(view.command(archived.command_id).unwrap().is_none());
8271        assert!(view.event(archived.emitted_events[0]).unwrap().is_none());
8272        assert_eq!(
8273            view.command(retained.command_id).unwrap().unwrap().id,
8274            retained.command_id
8275        );
8276        assert_eq!(
8277            view.event(retained.emitted_events[0]).unwrap().unwrap().id,
8278            retained.emitted_events[0]
8279        );
8280        assert!(view.command(CommandId::new(3)).unwrap().is_none());
8281        assert!(view.event(EventId::new(3)).unwrap().is_none());
8282    }
8283
8284    #[test]
8285    fn runtime_and_snapshot_validation_contexts_share_cause_and_directive_rules() {
8286        let (scenario, _) = demo_scenario();
8287        let simulation = Simulation::new(46, scenario).expect("validation fixture should load");
8288        let snapshot = simulation.snapshot();
8289        let runtime_context = validation::RuntimeValidationContext::new(&simulation.state);
8290        let snapshot_context = validation::SnapshotValidationContext::new(&snapshot);
8291        let missing_cause = CauseRef::Event(EventId::new(1));
8292
8293        assert!(validation::validate_cause_reference(&runtime_context, &missing_cause).is_err());
8294        assert!(validation::validate_cause_reference(&snapshot_context, &missing_cause).is_err());
8295
8296        let directives = [SystemDirective::Emit {
8297            event_type: " marker ".to_owned(),
8298            summary: "invalid event type".to_owned(),
8299            affected: Vec::new(),
8300        }];
8301        let runtime_error = validation::validate_directives_with_context(
8302            &runtime_context,
8303            "fixture",
8304            &[],
8305            &BTreeMap::new(),
8306            &BTreeMap::new(),
8307            &directives,
8308        )
8309        .expect_err("runtime validation must reject a non-canonical directive");
8310        let snapshot_error = validation::validate_directives_with_context(
8311            &snapshot_context,
8312            "fixture",
8313            &[],
8314            &BTreeMap::new(),
8315            &BTreeMap::new(),
8316            &directives,
8317        )
8318        .expect_err("snapshot validation must reject a non-canonical directive");
8319        assert_eq!(runtime_error.code, ErrorCode::InvalidPayload);
8320        assert_eq!(snapshot_error.code, runtime_error.code);
8321        assert_eq!(snapshot_error.message, runtime_error.message);
8322    }
8323
8324    #[test]
8325    fn snapshot_round_trip_preserves_pending_work() {
8326        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
8327        simulation
8328            .submit(move_order(&ids))
8329            .expect("order should validate");
8330        let json = simulation
8331            .snapshot_json()
8332            .expect("snapshot should serialize");
8333        let mut unsupported = simulation.snapshot();
8334        assert_eq!(unsupported.engine_version, ENGINE_VERSION);
8335        assert_eq!(unsupported.snapshot_format_version, SNAPSHOT_FORMAT_VERSION);
8336        unsupported.snapshot_format_version += 1;
8337        let Err(error) = Simulation::from_snapshot(unsupported) else {
8338            panic!("unknown snapshot formats must be rejected");
8339        };
8340        assert_eq!(error.code, ErrorCode::UnsupportedSnapshotVersion);
8341
8342        let mut unmigrated_engine = simulation.snapshot();
8343        unmigrated_engine.engine_version = "0.4.0-other".to_owned();
8344        refresh_snapshot_commitments_and_checkpoint(&mut unmigrated_engine);
8345        let Err(error) = Simulation::from_snapshot(unmigrated_engine) else {
8346            panic!("current-format snapshots from another engine must require migration");
8347        };
8348        assert_eq!(error.code, ErrorCode::UnsupportedSnapshotVersion);
8349
8350        let mut legacy_value = serde_json::to_value(simulation.snapshot())
8351            .expect("snapshot should convert to JSON value");
8352        let legacy_object = legacy_value
8353            .as_object_mut()
8354            .expect("snapshot JSON should be an object");
8355        legacy_object.insert("snapshot_format_version".to_owned(), Value::from(2));
8356        legacy_object.remove("run_manifest");
8357        legacy_object.remove("run_manifest_hash");
8358        legacy_object.remove("run_configuration");
8359        legacy_object.remove("checkpoint_hash");
8360        legacy_object.remove("commitment_format_version");
8361        legacy_object.remove("commitment_roots");
8362        legacy_object.remove("revision_format_version");
8363        legacy_object.remove("state_revision");
8364        legacy_object.remove("replay_revision_format_version");
8365        legacy_object.remove("admission_cursor_format_version");
8366        legacy_object.remove("admitted_attempt_count");
8367        legacy_object.remove("admitted_command_count");
8368        legacy_object.remove("admitted_event_count");
8369        legacy_object.remove("boundaries");
8370        legacy_object.remove("next_boundary_id");
8371        legacy_object.remove("root_seed");
8372        legacy_object.remove("random_streams");
8373        legacy_object.remove("random_draws");
8374        legacy_object.remove("next_random_draw_id");
8375        legacy_object.insert(
8376            "rng".to_owned(),
8377            serde_json::to_value(DeterministicRng::from_seed(35))
8378                .expect("legacy RNG fixture should serialize"),
8379        );
8380        let legacy_json =
8381            serde_json::to_string(&legacy_value).expect("legacy snapshot fixture should serialize");
8382        let migrated = Simulation::from_snapshot_json(&legacy_json)
8383            .expect("format 2 snapshot should migrate explicitly");
8384        assert_eq!(
8385            migrated.snapshot().snapshot_format_version,
8386            SNAPSHOT_FORMAT_VERSION
8387        );
8388        assert_eq!(migrated.snapshot().engine_version, ENGINE_VERSION);
8389        assert!(migrated.boundaries().is_empty());
8390        let legacy_journal = migrated.replay_journal();
8391        let (initial_scenario, _) = demo_scenario();
8392        let Err(error) = Simulation::replay_from_journal(initial_scenario, &[], &legacy_journal)
8393        else {
8394            panic!("identity-unbound legacy checkpoints must not claim exact replay");
8395        };
8396        assert_eq!(error.code, ErrorCode::LegacyReplayUnavailable);
8397
8398        let mut restored = Simulation::from_snapshot_json(&json).expect("snapshot should restore");
8399        restored
8400            .advance(SimDuration::days(1))
8401            .expect("pending arrival should execute");
8402        assert_eq!(
8403            restored
8404                .world()
8405                .army(ids.army)
8406                .expect("army exists")
8407                .location,
8408            ids.eastern_territory
8409        );
8410        let mut changed_delivery = restored.snapshot();
8411        let mut changed_dispatch = None;
8412        for event in &mut changed_delivery.events {
8413            if let EventKind::ReportDispatched { arrives_at, .. } = &mut event.kind {
8414                *arrives_at += SimDuration::minutes(1);
8415                changed_dispatch = Some((event.id, *arrives_at));
8416                break;
8417            }
8418        }
8419        let (dispatch_event, changed_arrival) =
8420            changed_dispatch.expect("arrival should dispatch an observer report");
8421        let scheduled = changed_delivery
8422            .scheduled
8423            .iter_mut()
8424            .find(|record| {
8425                matches!(
8426                    record.action,
8427                    ScheduledAction::KnowledgeReport {
8428                        dispatch_event: candidate,
8429                        ..
8430                    } if candidate == dispatch_event
8431                )
8432            })
8433            .expect("the dispatched report should remain pending");
8434        scheduled.key.at = changed_arrival;
8435        refresh_snapshot_commitments_and_checkpoint(&mut changed_delivery);
8436        let Err(error) = Simulation::from_snapshot(changed_delivery) else {
8437            panic!("report timing must remain tied to its recorded random draw");
8438        };
8439        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8440        assert!(error.message.contains("random draw"));
8441
8442        let mut missing_draw = restored.snapshot();
8443        missing_draw.random_draws.clear();
8444        let core_stream = missing_draw
8445            .random_streams
8446            .iter_mut()
8447            .find(|state| state.key == random::core_report_delay_stream())
8448            .expect("the core report-delay stream should be persisted");
8449        core_stream.position = 0;
8450        core_stream.generator_state = core_stream.seed;
8451        missing_draw.next_random_draw_id = 1;
8452        refresh_snapshot_commitments_and_checkpoint(&mut missing_draw);
8453        let Err(error) = Simulation::from_snapshot(missing_draw) else {
8454            panic!("every report dispatch must retain its generating random draw");
8455        };
8456        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8457        assert!(error.message.contains("core random draw"));
8458
8459        let mut malformed_legacy =
8460            serde_json::to_value(restored.snapshot()).expect("snapshot should convert to JSON");
8461        let malformed_object = malformed_legacy
8462            .as_object_mut()
8463            .expect("snapshot JSON should be an object");
8464        malformed_object.insert("snapshot_format_version".to_owned(), Value::from(3));
8465        malformed_object.remove("run_manifest");
8466        malformed_object.remove("run_manifest_hash");
8467        malformed_object.remove("run_configuration");
8468        malformed_object.remove("checkpoint_hash");
8469        malformed_object.remove("commitment_format_version");
8470        malformed_object.remove("commitment_roots");
8471        malformed_object.remove("revision_format_version");
8472        malformed_object.remove("state_revision");
8473        malformed_object.remove("replay_revision_format_version");
8474        malformed_object.remove("admission_cursor_format_version");
8475        malformed_object.remove("admitted_attempt_count");
8476        malformed_object.remove("admitted_command_count");
8477        malformed_object.remove("admitted_event_count");
8478        malformed_object.remove("root_seed");
8479        malformed_object.remove("random_streams");
8480        malformed_object.remove("random_draws");
8481        malformed_object.remove("next_random_draw_id");
8482        malformed_object.insert(
8483            "rng".to_owned(),
8484            serde_json::to_value(DeterministicRng::from_seed(35))
8485                .expect("legacy RNG fixture should serialize"),
8486        );
8487        let malformed_dispatch = malformed_object
8488            .get_mut("events")
8489            .and_then(Value::as_array_mut)
8490            .and_then(|events| {
8491                events.iter_mut().find(|event| {
8492                    event
8493                        .get("kind")
8494                        .and_then(|kind| kind.get("type"))
8495                        .and_then(Value::as_str)
8496                        == Some("report_dispatched")
8497                })
8498            })
8499            .expect("the legacy fixture should contain a report dispatch");
8500        malformed_dispatch["timestamp"] = Value::from(i64::MAX);
8501        malformed_dispatch["kind"]["arrives_at"] = Value::from(i64::MIN);
8502        let malformed_json = serde_json::to_string(&malformed_legacy)
8503            .expect("malformed legacy fixture should still serialize");
8504        let Err(error) = Simulation::from_snapshot_json(&malformed_json) else {
8505            panic!("legacy report-time overflow must return a structured error");
8506        };
8507        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8508        assert!(error.message.contains("legacy report timing"));
8509
8510        let report_pending = restored
8511            .snapshot_json()
8512            .expect("pending reports should serialize");
8513        let mut report_restored = Simulation::from_snapshot_json(&report_pending)
8514            .expect("pending report evidence should restore");
8515        report_restored
8516            .advance(SimDuration::days(3))
8517            .expect("pending reports should be delivered");
8518        let delivered = report_restored
8519            .snapshot_json()
8520            .expect("delivered reports should serialize");
8521        Simulation::from_snapshot_json(&delivered)
8522            .expect("completed report evidence should restore without pending work");
8523    }
8524
8525    #[test]
8526    fn pre_policy_format_four_journals_hydrate_compatibility_provenance() {
8527        let (scenario, ids) = demo_scenario();
8528        let mut simulation =
8529            Simulation::new(73, scenario.clone()).expect("compatibility run should load");
8530        simulation
8531            .submit(move_order(&ids))
8532            .expect("legacy command fixture should be accepted");
8533        let mut value =
8534            serde_json::to_value(simulation.replay_journal()).expect("journal should become JSON");
8535        let object = value
8536            .as_object_mut()
8537            .expect("replay journal JSON should be an object");
8538        object.remove("run_configuration");
8539        object.remove("command_attempts");
8540        let hydrated: ReplayJournal =
8541            serde_json::from_value(value).expect("pre-policy journal should deserialize");
8542        assert_eq!(
8543            hydrated.run_configuration,
8544            RunConfigurationSnapshot::CompatibilityV1
8545        );
8546        assert!(hydrated.command_attempts.is_empty());
8547        let replayed = Simulation::replay_from_journal(scenario.clone(), &[], &hydrated)
8548            .expect("pre-policy compatibility journal should replay exactly");
8549        assert_eq!(simulation.snapshot(), replayed.snapshot());
8550
8551        let mut aliased = Simulation::new(74, scenario)
8552            .expect("compatibility run should load")
8553            .snapshot();
8554        aliased.run_configuration = Some(RunConfigurationSnapshot::ManifestOnlyV1);
8555        assert_eq!(
8556            snapshot_checkpoint_hash(&aliased)
8557                .expect("the provenance alias should remain checkpoint-neutral"),
8558            aliased.checkpoint_hash
8559        );
8560        let Err(error) = Simulation::from_snapshot(aliased) else {
8561            panic!("default run identity must have exactly one policy provenance");
8562        };
8563        assert_eq!(error.code, ErrorCode::InvalidRunManifest);
8564    }
8565
8566    #[test]
8567    fn pre_policy_format_four_custom_run_identity_remains_loadable() {
8568        let (scenario, _) = demo_scenario();
8569        let mut legacy = Simulation::new(73, scenario.clone())
8570            .expect("compatibility run should load")
8571            .snapshot();
8572        let scenario_manifest =
8573            ArtifactManifest::for_scenario("legacy", "scenario", "1", &scenario)
8574                .expect("scenario should hash");
8575        let run_configuration =
8576            ArtifactManifest::from_bytes("legacy", "custom-run-policy", "7", b"opaque-policy")
8577                .expect("legacy policy identity should hash");
8578        let run_manifest = RunManifest::declared(scenario_manifest, run_configuration);
8579        legacy.run_manifest_hash = manifest::hash(&run_manifest).expect("manifest should hash");
8580        legacy.run_manifest = Some(run_manifest);
8581        legacy.run_configuration = Some(RunConfigurationSnapshot::ManifestOnlyV1);
8582        refresh_snapshot_commitments_and_checkpoint(&mut legacy);
8583        let expected = legacy.clone();
8584
8585        let mut value = serde_json::to_value(legacy).expect("snapshot should become JSON");
8586        value
8587            .as_object_mut()
8588            .expect("snapshot JSON should be an object")
8589            .remove("run_configuration");
8590        let json = serde_json::to_string(&value).expect("legacy snapshot should serialize");
8591        let restored = Simulation::from_snapshot_json(&json)
8592            .expect("custom pre-policy format-4 identity should hydrate explicitly");
8593        assert_eq!(
8594            restored.run_configuration(),
8595            &RunConfigurationSnapshot::ManifestOnlyV1
8596        );
8597        assert_eq!(restored.snapshot(), expected);
8598        let journal = restored.replay_journal();
8599        let mut journal_value =
8600            serde_json::to_value(&journal).expect("custom journal should become JSON");
8601        let journal_object = journal_value
8602            .as_object_mut()
8603            .expect("custom journal JSON should be an object");
8604        journal_object.remove("run_configuration");
8605        journal_object.remove("command_attempts");
8606        let hydrated_journal: ReplayJournal = serde_json::from_value(journal_value)
8607            .expect("custom pre-policy journal should deserialize");
8608        assert_eq!(
8609            hydrated_journal.run_configuration,
8610            RunConfigurationSnapshot::ManifestOnlyV1
8611        );
8612        let replayed = Simulation::replay_from_journal(scenario, &[], &hydrated_journal)
8613            .expect("manifest-only format-4 evidence should remain exactly replayable");
8614        assert_eq!(restored.snapshot(), replayed.snapshot());
8615    }
8616
8617    #[test]
8618    fn persistence_boundaries_reject_unloadable_or_noncanonical_state() {
8619        let (mut in_flight, in_flight_ids) = demo_scenario();
8620        in_flight.world.armies[0].transit = Some(TransitState {
8621            from: in_flight_ids.central_territory,
8622            to: in_flight_ids.eastern_territory,
8623            departed_at: in_flight.start_time,
8624            arrives_at: in_flight.start_time + SimDuration::days(1),
8625        });
8626        let Err(error) = Simulation::new(35, in_flight) else {
8627            panic!("initial transit without queue evidence must be rejected");
8628        };
8629        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8630
8631        let (mut non_finite, _) = demo_scenario();
8632        non_finite.world.territories[0].position.x = f32::NAN;
8633        let Err(error) = Simulation::new(35, non_finite) else {
8634            panic!("non-finite map coordinates must be rejected");
8635        };
8636        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8637
8638        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
8639        simulation
8640            .submit(move_order(&ids))
8641            .expect("order should validate");
8642        let valid = simulation.snapshot();
8643
8644        let mut past_schedule = valid.clone();
8645        past_schedule.scheduled[0].key.at =
8646            SimTime::from_minutes(past_schedule.now.as_minutes() - 1);
8647        let Err(error) = Simulation::from_snapshot(past_schedule) else {
8648            panic!("past scheduled work must be rejected");
8649        };
8650        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8651
8652        let mut duplicate_arrival = valid.clone();
8653        let mut second_arrival = duplicate_arrival.scheduled[0].clone();
8654        second_arrival.key.sequence = duplicate_arrival.next_schedule_sequence;
8655        duplicate_arrival.next_schedule_sequence += 1;
8656        duplicate_arrival.scheduled.push(second_arrival);
8657        let Err(error) = Simulation::from_snapshot(duplicate_arrival) else {
8658            panic!("duplicate logical arrivals must be rejected");
8659        };
8660        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8661
8662        let mut mismatched_arrival = valid.clone();
8663        mismatched_arrival.scheduled[0].key.at += SimDuration::minutes(1);
8664        let Err(error) = Simulation::from_snapshot(mismatched_arrival) else {
8665            panic!("arrival queue time must match transit and order evidence");
8666        };
8667        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8668
8669        let mut stuck_transit = valid.clone();
8670        stuck_transit.scheduled.clear();
8671        let Err(error) = Simulation::from_snapshot(stuck_transit) else {
8672            panic!("an in-transit army must retain exactly one arrival action");
8673        };
8674        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8675
8676        let mut reopened_registration = valid.clone();
8677        reopened_registration.plugin_registration_closed = false;
8678        let Err(error) = Simulation::from_snapshot(reopened_registration) else {
8679            panic!("executed snapshots must not reopen plugin registration");
8680        };
8681        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8682
8683        let mut stale_counter = valid.clone();
8684        stale_counter.next_event_id = stale_counter
8685            .events
8686            .last()
8687            .expect("movement emitted an event")
8688            .id
8689            .get();
8690        let Err(error) = Simulation::from_snapshot(stale_counter) else {
8691            panic!("stale counters must be rejected");
8692        };
8693        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8694
8695        let mut broken_reference = valid;
8696        broken_reference.world.armies[0].commander = PersonId::new(999);
8697        let Err(error) = Simulation::from_snapshot(broken_reference) else {
8698            panic!("broken entity references must be rejected");
8699        };
8700        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
8701
8702        let mut exhausted_counter = simulation.snapshot();
8703        exhausted_counter.next_command_id = u64::MAX;
8704        refresh_snapshot_commitments_and_checkpoint(&mut exhausted_counter);
8705        let mut restored =
8706            Simulation::from_snapshot(exhausted_counter).expect("the exhausted sentinel is valid");
8707        let before = restored.snapshot();
8708        let error = restored
8709            .submit(CommandEnvelope::new(
8710                Issuer::Debug,
8711                Command::DebugSetArmyMorale {
8712                    army: ids.army,
8713                    morale: 50,
8714                },
8715            ))
8716            .expect_err("counter exhaustion must be a structured failure");
8717        assert_eq!(error.code, ErrorCode::IdentifierExhausted);
8718        assert_eq!(before, restored.snapshot());
8719    }
8720
8721    #[test]
8722    fn plugin_command_receives_issuer_and_namespaces_state() {
8723        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
8724        simulation
8725            .register_plugin(&AuthorityPlugin)
8726            .expect("plugin should register");
8727
8728        let before = simulation
8729            .snapshot_json()
8730            .expect("snapshot should serialize");
8731        let rejected = simulation.submit(CommandEnvelope::new(
8732            Issuer::Actor(ids.observer),
8733            Command::Plugin {
8734                plugin: "authority-test".to_owned(),
8735                command: "set_stance".to_owned(),
8736                payload: Value::Null,
8737            },
8738        ));
8739        assert_eq!(
8740            rejected.expect_err("wrong actor must be rejected").code,
8741            ErrorCode::InvalidAuthority
8742        );
8743        assert_eq!(
8744            before,
8745            simulation
8746                .snapshot_json()
8747                .expect("snapshot should serialize")
8748        );
8749
8750        let invalid_payload = simulation.submit(CommandEnvelope::new(
8751            Issuer::Actor(ids.commander),
8752            Command::Plugin {
8753                plugin: "authority-test".to_owned(),
8754                command: "set_stance".to_owned(),
8755                payload: serde_json::json!({}),
8756            },
8757        ));
8758        assert_eq!(
8759            invalid_payload
8760                .expect_err("payloads must match their declared schema")
8761                .code,
8762            ErrorCode::InvalidPayload
8763        );
8764        assert_eq!(
8765            before,
8766            simulation
8767                .snapshot_json()
8768                .expect("payload rejection must not mutate the simulation")
8769        );
8770
8771        simulation
8772            .submit(CommandEnvelope::new(
8773                Issuer::Actor(ids.commander),
8774                Command::Plugin {
8775                    plugin: "authority-test".to_owned(),
8776                    command: "set_stance".to_owned(),
8777                    payload: Value::Null,
8778                },
8779            ))
8780            .expect("authorized actor should be accepted");
8781        let snapshot = simulation.snapshot();
8782        assert_eq!(snapshot.plugin_components.len(), 1);
8783        assert_eq!(snapshot.plugin_components[0].plugin, "authority-test");
8784        assert_eq!(
8785            snapshot.plugin_components[0].state,
8786            StateKey::new("military", "stance")
8787        );
8788        assert_eq!(snapshot.plugin_components[0].component, "stance");
8789        assert_eq!(
8790            simulation
8791                .register_plugin(&MarkerPlugin {
8792                    name: "late-plugin",
8793                    writes: Vec::new(),
8794                })
8795                .expect_err("new plugins cannot appear after execution begins")
8796                .code,
8797            ErrorCode::PluginRegistrationClosed
8798        );
8799    }
8800
8801    #[test]
8802    fn synchronous_reactor_depth_is_bounded_and_rolls_back() {
8803        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
8804        simulation
8805            .register_plugin(&RecursivePlugin)
8806            .expect("recursive compatibility plugin should register");
8807        let before = simulation
8808            .snapshot_json()
8809            .expect("snapshot should serialize before the rejected cascade");
8810
8811        let error = simulation
8812            .submit(move_order(&ids))
8813            .expect_err("recursive immediate reactions must be bounded");
8814        assert_eq!(error.code, ErrorCode::SynchronousReactionLimit);
8815        assert!(error.message.contains("maximum nested depth"));
8816        assert_eq!(
8817            before,
8818            simulation
8819                .snapshot_json()
8820                .expect("bounded cascade must roll back the entire transaction")
8821        );
8822    }
8823
8824    #[test]
8825    fn plugin_registration_is_atomic_and_rejects_duplicate_state_owners() {
8826        let (mut simulation, _) = Simulation::demo(35).expect("demo should load");
8827        simulation
8828            .register_plugin(&MarkerPlugin {
8829                name: "first-owner",
8830                writes: vec![StateKey::new("shared-domain", "balance")],
8831            })
8832            .expect("first owner should register");
8833        let before = simulation
8834            .snapshot_json()
8835            .expect("snapshot should serialize");
8836        let error = simulation
8837            .register_plugin(&MarkerPlugin {
8838                name: "second-owner",
8839                writes: vec![StateKey::new("shared-domain", "balance")],
8840            })
8841            .expect_err("a second owner must be rejected");
8842        assert_eq!(error.code, ErrorCode::DuplicateStateOwner);
8843        assert_eq!(
8844            before,
8845            simulation
8846                .snapshot_json()
8847                .expect("failed registration must not change state or manifests")
8848        );
8849        simulation
8850            .register_plugin(&GhostPlugin)
8851            .expect("a caught registrar error may not poison the candidate registry");
8852        simulation
8853            .register_plugin(&MarkerPlugin {
8854                name: "fresh-owner",
8855                writes: vec![StateKey::new("fresh-domain", "value")],
8856            })
8857            .expect("the failed multi-key claim must leave no ghost owner");
8858        simulation
8859            .register_plugin(&BoundaryGhostPlugin)
8860            .expect("a caught boundary registrar error may not poison the candidate registry");
8861        simulation
8862            .register_plugin(&MarkerPlugin {
8863                name: "boundary-ghost-owner",
8864                writes: vec![StateKey::new("boundary-ghost", "value")],
8865            })
8866            .expect("a later boundary-writer failure must leave no ghost owner");
8867    }
8868
8869    #[test]
8870    fn phased_boundary_allocates_deterministically_and_respects_visibility() {
8871        let (scenario, _) = demo_scenario();
8872        let mut first = Simulation::new(35, scenario.clone()).expect("demo should load");
8873        first
8874            .register_plugin(&JournalCommandPlugin)
8875            .expect("journal command plugin should register");
8876        first
8877            .register_plugin(&GrainSupplyPlugin)
8878            .expect("supply plugin should register");
8879        first
8880            .register_plugin(&HighClaimPlugin)
8881            .expect("high claim should register");
8882        first
8883            .register_plugin(&LowClaimPlugin)
8884            .expect("low claim should register");
8885        first
8886            .register_plugin(&VisibilityValidatorPlugin)
8887            .expect("validator should register");
8888
8889        let mut second = Simulation::new(35, scenario.clone()).expect("demo should load");
8890        second
8891            .register_plugin(&VisibilityValidatorPlugin)
8892            .expect("validator should register");
8893        second
8894            .register_plugin(&LowClaimPlugin)
8895            .expect("low claim should register");
8896        second
8897            .register_plugin(&HighClaimPlugin)
8898            .expect("high claim should register");
8899        second
8900            .register_plugin(&GrainSupplyPlugin)
8901            .expect("supply plugin should register");
8902        second
8903            .register_plugin(&JournalCommandPlugin)
8904            .expect("journal command plugin should register");
8905
8906        for simulation in [&mut first, &mut second] {
8907            for _ in 0..2 {
8908                simulation
8909                    .submit(CommandEnvelope::new(
8910                        Issuer::Debug,
8911                        Command::Plugin {
8912                            plugin: "journal-command".to_owned(),
8913                            command: "noop".to_owned(),
8914                            payload: Value::Null,
8915                        },
8916                    ))
8917                    .expect("journal fixture command should be accepted");
8918            }
8919        }
8920
8921        let request = BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily);
8922        let first_receipt = first
8923            .settle_boundary(request.clone())
8924            .expect("daily boundary should settle");
8925        let second_receipt = second
8926            .settle_boundary(request.clone())
8927            .expect("registration order must not change settlement");
8928        assert_eq!(first_receipt, second_receipt);
8929        assert_eq!(first.snapshot(), second.snapshot());
8930        let first_followup = first
8931            .settle_boundary(request.clone())
8932            .expect("a same-time follow-up boundary should settle");
8933        let second_followup = second
8934            .settle_boundary(request)
8935            .expect("the follow-up boundary must remain registration-order independent");
8936        assert_eq!(first_followup, second_followup);
8937        assert_eq!(first.snapshot(), second.snapshot());
8938
8939        let allocations: BTreeMap<_, _> = first_receipt
8940            .allocations
8941            .iter()
8942            .map(|allocation| {
8943                (
8944                    allocation.reservation.plugin.as_str(),
8945                    (allocation.granted, allocation.disposition),
8946                )
8947            })
8948            .collect();
8949        assert_eq!(
8950            allocations.get("high-claim"),
8951            Some(&(7, ReservationDisposition::Fulfilled))
8952        );
8953        assert_eq!(
8954            allocations.get("low-claim"),
8955            Some(&(3, ReservationDisposition::Partial))
8956        );
8957        let components: BTreeMap<_, _> = first
8958            .snapshot()
8959            .plugin_components
8960            .into_iter()
8961            .map(|record| (record.component, record.value))
8962            .collect();
8963        assert_eq!(components.get("high").and_then(Value::as_u64), Some(7));
8964        assert_eq!(components.get("low").and_then(Value::as_u64), Some(3));
8965
8966        let json = first
8967            .snapshot_json()
8968            .expect("settled boundary should serialize");
8969        let restored = Simulation::from_snapshot_json_with_plugins(
8970            &json,
8971            &[
8972                &GrainSupplyPlugin,
8973                &HighClaimPlugin,
8974                &LowClaimPlugin,
8975                &JournalCommandPlugin,
8976                &VisibilityValidatorPlugin,
8977            ],
8978        )
8979        .expect("settled boundary should rehydrate");
8980        assert_eq!(first.snapshot(), restored.snapshot());
8981
8982        let plugins: &[&dyn SimulationPlugin] = &[
8983            &GrainSupplyPlugin,
8984            &HighClaimPlugin,
8985            &LowClaimPlugin,
8986            &JournalCommandPlugin,
8987            &VisibilityValidatorPlugin,
8988        ];
8989        let replayed = Simulation::replay_with_boundaries(
8990            35,
8991            scenario,
8992            plugins,
8993            first.command_log(),
8994            first.boundaries(),
8995            first.time(),
8996        )
8997        .expect("boundary journal should replay exactly");
8998        assert_eq!(first.snapshot(), replayed.snapshot());
8999
9000        let mut corrupted_allocation = first.snapshot();
9001        corrupted_allocation.boundaries[0].allocations[0].granted += 1;
9002        let error = Simulation::from_snapshot_with_plugins(corrupted_allocation, plugins)
9003            .err()
9004            .expect("tampered allocation evidence must not load");
9005        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9006
9007        let mut corrupted_provenance = first.snapshot();
9008        corrupted_provenance.boundaries[0].emissions[0].system = "request".to_owned();
9009        let error = Simulation::from_snapshot_with_plugins(corrupted_provenance, plugins)
9010            .err()
9011            .expect("tampered boundary source provenance must not load");
9012        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9013
9014        let mut corrupted_command_cut = first.snapshot();
9015        corrupted_command_cut.boundaries[0].admitted_commands = vec![CommandId::new(2)];
9016        let error = Simulation::from_snapshot_with_plugins(corrupted_command_cut, plugins)
9017            .err()
9018            .expect("boundary admission must be a global command-journal prefix");
9019        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9020
9021        let mut corrupted_event_cut = first.snapshot();
9022        let later_event = corrupted_event_cut.boundaries[1].emissions[0].event;
9023        corrupted_event_cut.boundaries[0].admitted_events = vec![later_event];
9024        let error = Simulation::from_snapshot_with_plugins(corrupted_event_cut, plugins)
9025            .err()
9026            .expect("an earlier boundary cannot admit a later boundary event");
9027        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9028
9029        let mut corrupted_boundary_counter = first.snapshot();
9030        corrupted_boundary_counter.next_boundary_id += 1;
9031        let error = Simulation::from_snapshot_with_plugins(corrupted_boundary_counter, plugins)
9032            .err()
9033            .expect("the next boundary counter must not skip an identifier");
9034        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9035
9036        let (mut causal_cut, ids) = Simulation::demo(35).expect("demo should load");
9037        causal_cut
9038            .submit(move_order(&ids))
9039            .expect("movement should emit command-caused evidence");
9040        causal_cut
9041            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH))
9042            .expect("an evidence-only boundary should settle");
9043        assert!(causal_cut.boundaries()[0].emissions.is_empty());
9044
9045        let mut omitted_same_time_event = causal_cut.snapshot();
9046        omitted_same_time_event.boundaries[0]
9047            .admitted_events
9048            .clear();
9049        let error = Simulation::from_snapshot(omitted_same_time_event)
9050            .err()
9051            .expect("a no-emission boundary cannot omit already caused same-time evidence");
9052        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9053
9054        let mut due_at_boundary = causal_cut.snapshot();
9055        due_at_boundary.scheduled[0].key.at = due_at_boundary.now;
9056        due_at_boundary.boundaries[0].state_hash = Some(
9057            snapshot_state_hash(&due_at_boundary)
9058                .expect("the structurally corrupted state should hash"),
9059        );
9060        due_at_boundary.boundaries[0].hash = compute_boundary_hash(&due_at_boundary.boundaries[0])
9061            .expect("the structurally corrupted boundary should hash");
9062        refresh_snapshot_commitments_and_checkpoint(&mut due_at_boundary);
9063        let error = Simulation::from_snapshot(due_at_boundary)
9064            .err()
9065            .expect("completed boundaries cannot retain due ingress");
9066        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9067        assert!(error.message.contains("future-dated"));
9068    }
9069
9070    #[test]
9071    fn domain_record_lifecycle_is_atomic_replayable_and_tamper_evident() {
9072        let (scenario, _) = demo_scenario();
9073        let mut record_free = Simulation::new(87, scenario.clone())
9074            .expect("record-free compatibility fixture should load");
9075        let record_free_snapshot = record_free.snapshot();
9076        assert!(
9077            record_free_snapshot.initial_scenario.is_none(),
9078            "record-free format-4 state must retain its prior additive shape"
9079        );
9080        let mut redundant_initial_scenario = record_free_snapshot.clone();
9081        redundant_initial_scenario.initial_scenario = Some(scenario.clone());
9082        refresh_snapshot_commitments_and_checkpoint(&mut redundant_initial_scenario);
9083        let error = Simulation::from_snapshot(redundant_initial_scenario)
9084            .err()
9085            .expect("record-free snapshots must not carry ignored genesis state");
9086        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9087        let mut record_free_restored = Simulation::from_snapshot(record_free_snapshot)
9088            .expect("a pristine record-free snapshot should restore");
9089        record_free
9090            .register_plugin(&RecordLifecyclePlugin)
9091            .expect("the original pristine runtime should accept record schemas");
9092        record_free_restored
9093            .register_plugin(&RecordLifecyclePlugin)
9094            .expect("a restored pristine runtime must retain record-schema capability");
9095        assert_eq!(record_free.snapshot(), record_free_restored.snapshot());
9096        let mut initial_scenario = scenario.clone();
9097        initial_scenario.domain_records = vec![
9098            initial_record(
9099                "fixture-record-lifecycle",
9100                DomainRecordClass::Entity,
9101                office_draft("office-a", "Primary Office"),
9102            ),
9103            initial_record(
9104                "fixture-record-lifecycle",
9105                DomainRecordClass::Entity,
9106                office_draft("office-b", "Successor Office"),
9107            ),
9108            initial_record(
9109                "fixture-record-lifecycle",
9110                DomainRecordClass::Record,
9111                obligation_draft("office-a", "open"),
9112            ),
9113        ];
9114        let error = Simulation::new(88, initial_scenario.clone())
9115            .err()
9116            .expect("initial domain records must not create a half-configured runtime");
9117        assert_eq!(error.code, ErrorCode::PluginNotActive);
9118        let initial = Simulation::new_with_plugins(88, initial_scenario, &[&RecordLifecyclePlugin])
9119            .expect("plugin-aware construction should validate initial domain records");
9120        let initial_json = initial
9121            .snapshot_json()
9122            .expect("configured initial record state should serialize");
9123        let initial_restored =
9124            Simulation::from_snapshot_json_with_plugins(&initial_json, &[&RecordLifecyclePlugin])
9125                .expect("configured initial record state should reload immediately");
9126        assert_eq!(initial.snapshot(), initial_restored.snapshot());
9127
9128        let mut simulation =
9129            Simulation::new(89, scenario.clone()).expect("record fixture should load");
9130        simulation
9131            .register_plugin(&RecordLifecyclePlugin)
9132            .expect("record lifecycle plugin should register");
9133        let request = BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily);
9134        let created = simulation
9135            .settle_boundary(request.clone())
9136            .expect("record creation boundary should settle");
9137        let retired = simulation
9138            .settle_boundary(request.clone())
9139            .expect("record retirement boundary should settle");
9140        let succession = simulation
9141            .settle_boundary(request.clone())
9142            .expect("a later successor should retire without invalidating its predecessor");
9143        let deleted = simulation
9144            .settle_boundary(request)
9145            .expect("atomic reference transfer and deletion should settle");
9146        assert_eq!(created.record_change_count, 3);
9147        assert_eq!(retired.record_change_count, 2);
9148        assert_eq!(succession.record_change_count, 1);
9149        assert_eq!(deleted.record_change_count, 2);
9150        assert_eq!(
9151            created.change_count
9152                + retired.change_count
9153                + succession.change_count
9154                + deleted.change_count,
9155            1
9156        );
9157
9158        let original = simulation
9159            .domain_record(&office_reference("office-a"))
9160            .expect("deleted office tombstone should remain addressable");
9161        assert!(original.is_deleted());
9162        assert_eq!(original.version, 3);
9163        let obligation = simulation
9164            .domain_record(&obligation_reference())
9165            .expect("transferred obligation should remain present");
9166        assert_eq!(obligation.version, 2);
9167        assert!(obligation.references.iter().any(|reference| {
9168            reference.target == DomainReferenceTarget::Domain(office_reference("office-c"))
9169        }));
9170        assert!(matches!(
9171            &simulation
9172                .domain_record(&office_reference("office-b"))
9173                .expect("the intermediate successor should remain addressable")
9174                .lifecycle,
9175            DomainRecordLifecycle::Retired {
9176                successor: Some(successor),
9177                ..
9178            } if successor == &office_reference("office-c")
9179        ));
9180        assert!(simulation.boundaries().iter().all(|boundary| {
9181            boundary.record_changes.len()
9182                == boundary
9183                    .emissions
9184                    .iter()
9185                    .filter(|emission| {
9186                        matches!(emission.kind, BoundaryEmissionKind::RecordChange { .. })
9187                    })
9188                    .count()
9189        }));
9190
9191        let before_stale_update = simulation
9192            .snapshot_json()
9193            .expect("pre-conflict record state should serialize");
9194        let conflict = simulation
9195            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
9196            .expect_err("stale record versions must reject before commit");
9197        assert_eq!(conflict.code, ErrorCode::DomainRecordVersionConflict);
9198        assert_eq!(
9199            before_stale_update,
9200            simulation
9201                .snapshot_json()
9202                .expect("version conflicts must roll back the complete boundary")
9203        );
9204        let quiet = simulation
9205            .settle_boundary(
9206                BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Monthly),
9207            )
9208            .expect("an unrelated cadence should publish an empty later boundary");
9209        assert_eq!(quiet.change_count + quiet.record_change_count, 0);
9210
9211        let json = simulation
9212            .snapshot_json()
9213            .expect("domain-record snapshot should serialize");
9214        let restored =
9215            Simulation::from_snapshot_json_with_plugins(&json, &[&RecordLifecyclePlugin])
9216                .expect("domain-record evidence should restore with exact plugin code");
9217        assert_eq!(simulation.snapshot(), restored.snapshot());
9218
9219        let plugins: &[&dyn SimulationPlugin] = &[&RecordLifecyclePlugin];
9220        let replayed = Simulation::replay_with_boundaries(
9221            89,
9222            scenario,
9223            plugins,
9224            simulation.command_log(),
9225            simulation.boundaries(),
9226            simulation.time(),
9227        )
9228        .expect("domain-record boundary evidence should replay exactly");
9229        assert_eq!(simulation.snapshot(), replayed.snapshot());
9230
9231        let mut cross_system_creation = simulation.snapshot();
9232        let observer_event = cross_system_creation.boundaries[0]
9233            .emissions
9234            .iter()
9235            .find_map(|emission| {
9236                (emission.system == "observer"
9237                    && matches!(emission.kind, BoundaryEmissionKind::Explicit))
9238                .then_some(emission.event)
9239            })
9240            .expect("the independent observer should emit boundary evidence");
9241        cross_system_creation
9242            .events
9243            .iter_mut()
9244            .find(|event| event.id == observer_event)
9245            .expect("the observer event should exist")
9246            .affected_entities = vec![EntityRef::Domain(office_reference("office-b"))];
9247        let final_state_hash = snapshot_state_hash(&cross_system_creation)
9248            .expect("the cross-system creation forgery should have coherent final state");
9249        cross_system_creation
9250            .boundaries
9251            .last_mut()
9252            .expect("the fixture should have a boundary head")
9253            .state_hash = Some(final_state_hash);
9254        rehash_tampered_snapshot(&mut cross_system_creation);
9255        let error = Simulation::from_snapshot_with_plugins(cross_system_creation, plugins)
9256            .err()
9257            .expect("one proposal cannot consume another system's same-stage creation");
9258        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9259
9260        let mut precreation_reference = simulation.snapshot();
9261        let marker_change = precreation_reference.boundaries[0]
9262            .changes
9263            .first_mut()
9264            .expect("the first boundary should persist its marker change");
9265        marker_change.entity = EntityRef::Domain(office_reference("office-c"));
9266        let marker_event = precreation_reference.boundaries[0]
9267            .emissions
9268            .iter()
9269            .find_map(|emission| {
9270                matches!(
9271                    emission.kind,
9272                    BoundaryEmissionKind::Change { change_index: 0 }
9273                )
9274                .then_some(emission.event)
9275            })
9276            .expect("the marker change should have causal event evidence");
9277        precreation_reference
9278            .events
9279            .iter_mut()
9280            .find(|event| event.id == marker_event)
9281            .expect("the marker change event should exist")
9282            .affected_entities = vec![EntityRef::Domain(office_reference("office-c"))];
9283        precreation_reference
9284            .plugin_components
9285            .iter_mut()
9286            .find(|record| record.component == "status")
9287            .expect("the persisted marker component should exist")
9288            .entity = EntityRef::Domain(office_reference("office-c"));
9289        precreation_reference
9290            .plugin_components
9291            .sort_by_key(|record| {
9292                component_key(
9293                    &record.plugin,
9294                    &record.state,
9295                    &record.entity,
9296                    &record.component,
9297                )
9298            });
9299        let final_state_hash = snapshot_state_hash(&precreation_reference)
9300            .expect("the pre-creation forgery should have coherent final state");
9301        precreation_reference
9302            .boundaries
9303            .last_mut()
9304            .expect("the fixture should have a boundary head")
9305            .state_hash = Some(final_state_hash);
9306        rehash_tampered_snapshot(&mut precreation_reference);
9307        let error = Simulation::from_snapshot_with_plugins(precreation_reference, plugins)
9308            .err()
9309            .expect("earlier evidence cannot reference an entity created by a later boundary");
9310        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9311
9312        let mut post_deletion_reference = simulation.snapshot();
9313        let (last_boundary_id, last_boundary_at, last_boundary_correlation) = {
9314            let last_boundary = post_deletion_reference
9315                .boundaries
9316                .last_mut()
9317                .expect("the fixture should have an empty later boundary");
9318            last_boundary.cadences = vec![SystemCadence::Daily];
9319            (
9320                last_boundary.id,
9321                last_boundary.at,
9322                last_boundary.correlation_id,
9323            )
9324        };
9325        let event_id = EventId::new(post_deletion_reference.next_event_id);
9326        post_deletion_reference.next_event_id = post_deletion_reference
9327            .next_event_id
9328            .checked_add(1)
9329            .expect("the tamper fixture should have event ID capacity");
9330        post_deletion_reference.events.push(SimEvent {
9331            id: event_id,
9332            timestamp: last_boundary_at,
9333            kind: EventKind::Plugin {
9334                plugin: "fixture-record-lifecycle".to_owned(),
9335                event_type: "record_probe".to_owned(),
9336            },
9337            affected_entities: vec![EntityRef::Domain(office_reference("office-a"))],
9338            summary: "Forge evidence after the office was deleted".to_owned(),
9339            cause: Some(CauseRef::Boundary(last_boundary_id)),
9340            correlation_id: last_boundary_correlation,
9341        });
9342        post_deletion_reference
9343            .boundaries
9344            .last_mut()
9345            .expect("the fixture should retain its boundary head")
9346            .emissions
9347            .push(BoundaryEmission {
9348                plugin: "fixture-record-lifecycle".to_owned(),
9349                system: "lifecycle".to_owned(),
9350                event: event_id,
9351                kind: BoundaryEmissionKind::Explicit,
9352            });
9353        let final_state_hash = snapshot_state_hash(&post_deletion_reference)
9354            .expect("the post-deletion forgery should have coherent final state");
9355        post_deletion_reference
9356            .boundaries
9357            .last_mut()
9358            .expect("the fixture should retain its boundary head")
9359            .state_hash = Some(final_state_hash);
9360        rehash_tampered_snapshot(&mut post_deletion_reference);
9361        let error = Simulation::from_snapshot_with_plugins(post_deletion_reference, plugins)
9362            .err()
9363            .expect("later evidence cannot reference a deleted domain entity");
9364        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9365
9366        let mut corrupted = simulation.snapshot();
9367        corrupted.boundaries[1].record_changes[0].system = "forged-system".to_owned();
9368        rehash_tampered_snapshot(&mut corrupted);
9369        let error = Simulation::from_snapshot_with_plugins(corrupted, plugins)
9370            .err()
9371            .expect("forged domain-record provenance must not load");
9372        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9373
9374        let mut shifted_to_genesis = simulation.snapshot();
9375        let forged_initial_records = shifted_to_genesis.domain_records.clone();
9376        shifted_to_genesis
9377            .initial_scenario
9378            .as_mut()
9379            .expect("new snapshots retain their manifest-bound initial scenario")
9380            .domain_records
9381            .clone_from(&forged_initial_records);
9382        shifted_to_genesis.boundaries.clear();
9383        shifted_to_genesis.events.clear();
9384        shifted_to_genesis.plugin_registration_closed = false;
9385        shifted_to_genesis.next_event_id = 1;
9386        shifted_to_genesis.next_boundary_id = 1;
9387        shifted_to_genesis.next_correlation_id = 1;
9388        refresh_snapshot_commitments_and_checkpoint(&mut shifted_to_genesis);
9389        let error = Simulation::from_snapshot_with_plugins(shifted_to_genesis, plugins)
9390            .err()
9391            .expect("record creations cannot be relabeled as manifest-bound genesis state");
9392        assert_eq!(error.code, ErrorCode::InvalidRunManifest);
9393
9394        let mut stripped_feature = simulation.snapshot();
9395        stripped_feature.initial_scenario = None;
9396        stripped_feature.domain_records.clear();
9397        stripped_feature.boundaries.clear();
9398        stripped_feature.events.clear();
9399        stripped_feature.plugin_registration_closed = false;
9400        stripped_feature.next_event_id = 1;
9401        stripped_feature.next_boundary_id = 1;
9402        stripped_feature.next_correlation_id = 1;
9403        refresh_snapshot_commitments_and_checkpoint(&mut stripped_feature);
9404        let error = Simulation::from_snapshot_with_plugins(stripped_feature, plugins)
9405            .err()
9406            .expect("record schemas cannot downgrade to an unbound old-v4 snapshot shape");
9407        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9408    }
9409
9410    #[test]
9411    fn domain_record_delete_rejects_live_references_and_rolls_back() {
9412        let (scenario, _) = demo_scenario();
9413        let mut simulation = Simulation::new(90, scenario).expect("record fixture should load");
9414        simulation
9415            .register_plugin(&RecordDeleteOnlyPlugin)
9416            .expect("invalid-delete fixture should register");
9417        let request = BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily);
9418        simulation
9419            .settle_boundary(request.clone())
9420            .expect("record creation should settle");
9421        simulation
9422            .settle_boundary(request.clone())
9423            .expect("record retirement should settle");
9424        let before = simulation
9425            .snapshot_json()
9426            .expect("pre-failure state should serialize");
9427        let error = simulation
9428            .settle_boundary(request)
9429            .expect_err("a referenced record cannot be deleted");
9430        assert_eq!(error.code, ErrorCode::DomainRecordReferenced);
9431        assert_eq!(
9432            before,
9433            simulation
9434                .snapshot_json()
9435                .expect("failed deletion must restore every persisted field")
9436        );
9437    }
9438
9439    #[test]
9440    fn domain_record_successor_cycles_are_rejected_in_genesis_and_atomic_bundles() {
9441        let (scenario, _) = demo_scenario();
9442        let mut cyclic_genesis = scenario.clone();
9443        let mut first = initial_record(
9444            "fixture-record-cycle",
9445            DomainRecordClass::Entity,
9446            office_draft("office-a", "First Office"),
9447        );
9448        first.lifecycle = DomainRecordLifecycle::Retired {
9449            at: SimTime::EPOCH,
9450            successor: Some(office_reference("office-b")),
9451        };
9452        let mut second = initial_record(
9453            "fixture-record-cycle",
9454            DomainRecordClass::Entity,
9455            office_draft("office-b", "Second Office"),
9456        );
9457        second.lifecycle = DomainRecordLifecycle::Retired {
9458            at: SimTime::EPOCH,
9459            successor: Some(office_reference("office-a")),
9460        };
9461        cyclic_genesis.domain_records = vec![first, second];
9462        let error = Simulation::new_with_plugins(91, cyclic_genesis, &[&RecordCyclePlugin])
9463            .err()
9464            .expect("cyclic successor state must not enter a new run");
9465        assert_eq!(error.code, ErrorCode::InvalidDomainRecord);
9466
9467        let mut simulation = Simulation::new(92, scenario).expect("cycle fixture should load");
9468        simulation
9469            .register_plugin(&RecordCyclePlugin)
9470            .expect("cycle fixture plugin should register");
9471        let request = BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily);
9472        simulation
9473            .settle_boundary(request.clone())
9474            .expect("cycle fixture records should be created");
9475        let before = simulation
9476            .snapshot_json()
9477            .expect("pre-cycle state should serialize");
9478        let error = simulation
9479            .settle_boundary(request)
9480            .expect_err("mutual successor retirement must reject atomically");
9481        assert_eq!(error.code, ErrorCode::InvalidDomainRecord);
9482        assert_eq!(
9483            before,
9484            simulation
9485                .snapshot_json()
9486                .expect("failed successor cycles must roll back the whole boundary")
9487        );
9488    }
9489
9490    #[test]
9491    fn domain_record_snapshot_cannot_delete_the_bound_seat_institution() {
9492        let (scenario, _) = demo_scenario();
9493        let mut initial_scenario = scenario;
9494        initial_scenario.domain_records = vec![initial_record(
9495            "fixture-record-seat-deletion",
9496            DomainRecordClass::Entity,
9497            office_draft("office-a", "Bound Office"),
9498        )];
9499        let mut simulation = Simulation::new_with_plugins(
9500            93,
9501            initial_scenario.clone(),
9502            &[&RecordSeatDeletionPlugin],
9503        )
9504        .expect("unbound seat-deletion fixture should load");
9505        let request = BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily);
9506        simulation
9507            .settle_boundary(request.clone())
9508            .expect("the fixture office should retire");
9509        simulation
9510            .settle_boundary(request)
9511            .expect("an unbound retired office may be deleted");
9512
9513        let configuration = RunConfiguration {
9514            format_version: RUN_CONFIGURATION_FORMAT_VERSION,
9515            purpose: RunPurpose::Play,
9516            controller: ControllerPolicy::HumanRoleBound,
9517            seat: SeatPolicy::InstitutionBound,
9518            observation: ObservationPolicy::ActorBound,
9519            interaction: InteractionPolicy::EraInternalCommands,
9520            trace: TracePolicy::Causal,
9521            seat_binding: Some(SeatBinding {
9522                seat_id: "seat.bound-office".to_owned(),
9523                controller_id: "controller.human".to_owned(),
9524                actor: None,
9525                institution: Some(EntityRef::Domain(office_reference("office-a"))),
9526                permission_profile_id: "permission.institution".to_owned(),
9527            }),
9528            declared_interventions: Vec::new(),
9529            diagnostic_commands_enabled: false,
9530            require_idempotency_keys: true,
9531        };
9532        let mut forged = simulation.snapshot();
9533        let run_manifest = manifest_for_configuration(&initial_scenario, &configuration);
9534        forged.run_manifest_hash =
9535            manifest::hash(&run_manifest).expect("forged manifest should hash canonically");
9536        forged.run_manifest = Some(run_manifest);
9537        forged.run_configuration = Some(RunConfigurationSnapshot::Declared(configuration));
9538        let final_state_hash = snapshot_state_hash(&forged)
9539            .expect("the forged institution-bound final state should hash");
9540        forged
9541            .boundaries
9542            .last_mut()
9543            .expect("the forged fixture should have a boundary head")
9544            .state_hash = Some(final_state_hash);
9545        rehash_tampered_snapshot(&mut forged);
9546        let error = Simulation::from_snapshot_with_plugins(forged, &[&RecordSeatDeletionPlugin])
9547            .err()
9548            .expect("a snapshot cannot delete the institution bound to its active seat");
9549        assert_eq!(error.code, ErrorCode::InvalidRunConfiguration);
9550    }
9551
9552    #[test]
9553    fn failed_phased_boundary_restores_every_writable_domain_and_retries_exactly() {
9554        let (scenario, ids) = demo_scenario();
9555        let record_plugin = RecordLifecyclePlugin;
9556        let rollback_plugin = BoundaryRollbackPlugin;
9557        let random_plugin = PrimaryRandomPlugin;
9558        let mut simulation = Simulation::new(35, scenario).expect("rollback fixture should load");
9559        simulation
9560            .register_plugin(&record_plugin)
9561            .expect("record fixture should register");
9562        simulation
9563            .register_plugin(&rollback_plugin)
9564            .expect("rollback fixture should register");
9565        simulation
9566            .register_plugin(&random_plugin)
9567            .expect("random fixture should register");
9568        simulation
9569            .enqueue_command(
9570                SimTime::EPOCH,
9571                0,
9572                CommandRequest::new(
9573                    CommandRequestId::new(1),
9574                    simulation.revision(),
9575                    move_order(&ids),
9576                ),
9577            )
9578            .expect("the initial movement should queue");
9579        simulation
9580            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
9581            .expect("the initial boundary should create records and schedule the arrival");
9582        let arrival_at = SimTime::EPOCH
9583            .checked_add(SimDuration::hours(18))
9584            .expect("arrival time should be representable");
9585        let return_order = CommandEnvelope::new(
9586            Issuer::Actor(ids.commander),
9587            Command::MoveArmy {
9588                army: ids.army,
9589                destination: ids.western_territory,
9590            },
9591        )
9592        .at_time(arrival_at);
9593        simulation
9594            .enqueue_command(
9595                arrival_at,
9596                0,
9597                CommandRequest::new(
9598                    CommandRequestId::new(2),
9599                    simulation.revision(),
9600                    return_order,
9601                ),
9602            )
9603            .expect("the equal-time return order should queue");
9604
9605        let baseline = simulation.snapshot();
9606        let mut control = Simulation::from_snapshot_with_plugins(
9607            baseline.clone(),
9608            &[&record_plugin, &rollback_plugin, &random_plugin],
9609        )
9610        .expect("the pending rollback fixture should reload exactly");
9611        let next_random_draw_id = simulation.state.counters.next_random_draw_id;
9612        simulation.state.counters.next_random_draw_id = u64::MAX - 1;
9613        let cache_before = cache_fingerprint(&simulation);
9614        let before = simulation
9615            .snapshot_json()
9616            .expect("snapshot should serialize");
9617        let error = simulation
9618            .settle_boundary(BoundaryRequest::at(arrival_at).with_cadence(SystemCadence::Daily))
9619            .expect_err("random-draw identifier exhaustion must abort the whole boundary");
9620        assert_eq!(error.code, ErrorCode::IdentifierExhausted);
9621        assert_eq!(
9622            before,
9623            simulation
9624                .snapshot_json()
9625                .expect("failed settlement must restore every serialized field")
9626        );
9627        assert_eq!(cache_fingerprint(&simulation), cache_before);
9628
9629        simulation.state.counters.next_random_draw_id = next_random_draw_id;
9630        assert_eq!(simulation.snapshot(), baseline);
9631        let retry = simulation
9632            .settle_boundary(BoundaryRequest::at(arrival_at).with_cadence(SystemCadence::Daily))
9633            .expect("the repaired boundary should settle");
9634        let control_receipt = control
9635            .settle_boundary(BoundaryRequest::at(arrival_at).with_cadence(SystemCadence::Daily))
9636            .expect("the control boundary should settle");
9637        assert_eq!(retry, control_receipt);
9638        assert_eq!(simulation.snapshot(), control.snapshot());
9639        assert!(retry.change_count > 0);
9640        assert!(retry.record_change_count > 0);
9641        assert!(!retry.generated_ingress.is_empty());
9642        assert!(!retry.random_draws.is_empty());
9643    }
9644
9645    #[test]
9646    fn scoped_random_streams_are_isolated_recorded_hashed_and_replayable() {
9647        let (scenario, _) = demo_scenario();
9648        let mut primary_only = Simulation::new(73, scenario.clone()).expect("demo should load");
9649        primary_only
9650            .register_plugin(&PrimaryRandomPlugin)
9651            .expect("primary random plugin should register");
9652
9653        let mut with_noise = Simulation::new(73, scenario.clone()).expect("demo should load");
9654        with_noise
9655            .register_plugin(&NoiseRandomPlugin)
9656            .expect("noise random plugin should register");
9657        with_noise
9658            .register_plugin(&PrimaryRandomPlugin)
9659            .expect("primary random plugin should register");
9660
9661        let request = BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily);
9662        let primary_receipt = primary_only
9663            .settle_boundary(request.clone())
9664            .expect("primary boundary should settle");
9665        with_noise
9666            .settle_boundary(request)
9667            .expect("noise boundary should settle");
9668
9669        let primary_draw = primary_only
9670            .random_draws()
9671            .first()
9672            .expect("the primary system should record its draw");
9673        let isolated_draw = with_noise
9674            .random_draws()
9675            .iter()
9676            .find(|draw| draw.stream == primary_random_stream())
9677            .expect("the primary stream should remain present with unrelated noise");
9678        assert_eq!(primary_draw.value, isolated_draw.value);
9679        assert_eq!(primary_draw.position, isolated_draw.position);
9680        assert_eq!(primary_draw.id, primary_receipt.random_draws[0]);
9681        assert_eq!(
9682            primary_draw.cause,
9683            CauseRef::Boundary(primary_receipt.boundary_id)
9684        );
9685        assert!(matches!(
9686            &primary_draw.producer,
9687            RandomDrawProducer::BoundarySystem {
9688                boundary,
9689                plugin,
9690                system,
9691            } if *boundary == primary_receipt.boundary_id
9692                && plugin == "random-primary"
9693                && system == "roll"
9694        ));
9695
9696        let first_hash = primary_receipt.boundary_hash;
9697        let second_receipt = primary_only
9698            .settle_boundary(
9699                BoundaryRequest::at(SimTime::EPOCH + SimDuration::days(1))
9700                    .with_cadence(SystemCadence::Daily),
9701            )
9702            .expect("second primary boundary should settle");
9703        let second_boundary = primary_only
9704            .boundaries()
9705            .last()
9706            .expect("second boundary should be recorded");
9707        assert_eq!(second_boundary.previous_hash, first_hash);
9708        assert_eq!(second_boundary.hash, second_receipt.boundary_hash);
9709        assert!(second_boundary.state_hash.is_some());
9710        assert_eq!(
9711            primary_only.boundary_head_hash(),
9712            Some(second_receipt.boundary_hash.as_str())
9713        );
9714
9715        let restored = Simulation::from_snapshot_with_plugins(
9716            primary_only.snapshot(),
9717            &[&PrimaryRandomPlugin],
9718        )
9719        .expect("scoped random evidence should survive snapshot restoration");
9720        assert_eq!(primary_only.snapshot(), restored.snapshot());
9721
9722        let mut changed_state = primary_only.snapshot();
9723        changed_state.world.armies[0].morale += 1;
9724        let Err(error) =
9725            Simulation::from_snapshot_with_plugins(changed_state, &[&PrimaryRandomPlugin])
9726        else {
9727            panic!("persisted state cannot change while retaining its checkpoint commitment");
9728        };
9729        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9730        assert!(error.message.contains("commitment roots"));
9731
9732        let mut missing_state_commitment = primary_only.snapshot();
9733        missing_state_commitment.boundaries[0].state_hash = None;
9734        missing_state_commitment.boundaries[0].hash =
9735            compute_boundary_hash(&missing_state_commitment.boundaries[0])
9736                .expect("the malformed legacy-style boundary should hash");
9737        let first_hash = missing_state_commitment.boundaries[0].hash.clone();
9738        missing_state_commitment.boundaries[1].previous_hash = first_hash;
9739        missing_state_commitment.boundaries[1].hash =
9740            compute_boundary_hash(&missing_state_commitment.boundaries[1])
9741                .expect("the dependent boundary should rehash");
9742        refresh_snapshot_commitments_and_checkpoint(&mut missing_state_commitment);
9743        let Err(error) = Simulation::from_snapshot_with_plugins(
9744            missing_state_commitment,
9745            &[&PrimaryRandomPlugin],
9746        ) else {
9747            panic!("declared format-4 runs require every boundary state commitment");
9748        };
9749        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9750
9751        let replayed = Simulation::replay_with_boundaries(
9752            73,
9753            scenario,
9754            &[&PrimaryRandomPlugin],
9755            primary_only.command_log(),
9756            primary_only.boundaries(),
9757            primary_only.time(),
9758        )
9759        .expect("scoped draws and boundary hashes should replay exactly");
9760        assert_eq!(primary_only.snapshot(), replayed.snapshot());
9761
9762        let mut corrupted_draw = primary_only.snapshot();
9763        corrupted_draw.random_draws[0].value = (corrupted_draw.random_draws[0].value + 1)
9764            % corrupted_draw.random_draws[0].upper_exclusive;
9765        let Err(error) =
9766            Simulation::from_snapshot_with_plugins(corrupted_draw, &[&PrimaryRandomPlugin])
9767        else {
9768            panic!("tampered random evidence must not load");
9769        };
9770        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9771
9772        let mut corrupted_hash = primary_only.snapshot();
9773        corrupted_hash.boundaries[0].hash.replace_range(..1, "f");
9774        let Err(error) =
9775            Simulation::from_snapshot_with_plugins(corrupted_hash, &[&PrimaryRandomPlugin])
9776        else {
9777            panic!("tampered boundary hashes must not load");
9778        };
9779        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
9780    }
9781
9782    #[test]
9783    fn run_and_plugin_manifests_bind_continuation_and_replay() {
9784        let (scenario, _) = demo_scenario();
9785        let scenario_manifest =
9786            ArtifactManifest::for_scenario("fixture", "reference-scenario", "1", &scenario)
9787                .expect("scenario identity should hash");
9788        let run_configuration = RunConfiguration::read_only_observer();
9789        let run_configuration_manifest = ArtifactManifest::for_run_configuration(
9790            "fixture",
9791            "run-policy",
9792            "1",
9793            &run_configuration,
9794        )
9795        .expect("run configuration should hash");
9796        let mut run_manifest = RunManifest::declared(scenario_manifest, run_configuration_manifest);
9797        let RunManifest::Declared {
9798            rules,
9799            content,
9800            localization_contracts,
9801            sources,
9802            ..
9803        } = &mut run_manifest
9804        else {
9805            unreachable!("the fixture creates a declared manifest");
9806        };
9807        rules.extend([
9808            ArtifactManifest::from_bytes("fixture", "zeta-rules", "1", b"zeta")
9809                .expect("rule identity should hash"),
9810            ArtifactManifest::from_bytes("fixture", "alpha-rules", "1", b"alpha")
9811                .expect("rule identity should hash"),
9812        ]);
9813        content.push(
9814            ArtifactManifest::from_bytes("fixture", "historical-content", "1", b"content")
9815                .expect("content identity should hash"),
9816        );
9817        localization_contracts.push(
9818            ArtifactManifest::from_bytes("fixture", "localization-contract", "1", b"keys-v1")
9819                .expect("localization identity should hash"),
9820        );
9821        sources.push(
9822            ArtifactManifest::from_bytes("fixture", "source-ledger", "1", b"sources")
9823                .expect("source identity should hash"),
9824        );
9825
9826        let mut simulation = Simulation::new_with_run_configuration(
9827            91,
9828            scenario.clone(),
9829            run_manifest,
9830            run_configuration.clone(),
9831        )
9832        .expect("declared run identity should be admitted");
9833        let RunManifest::Declared { rules, .. } = simulation.run_manifest() else {
9834            unreachable!("new runs retain a declared manifest");
9835        };
9836        assert_eq!(rules[0].name, "alpha-rules");
9837        assert_eq!(rules[1].name, "zeta-rules");
9838        assert!(is_canonical_hash(simulation.run_manifest_hash()));
9839        simulation
9840            .register_plugin(&PrimaryRandomPlugin)
9841            .expect("versioned plugin should register");
9842        simulation
9843            .settle_boundary(BoundaryRequest::at(SimTime::EPOCH).with_cadence(SystemCadence::Daily))
9844            .expect("manifest-bound boundary should settle");
9845
9846        let exact_manifest = simulation.run_manifest().clone();
9847        let snapshot = simulation.snapshot();
9848        let restored =
9849            Simulation::from_snapshot_with_plugins(snapshot.clone(), &[&PrimaryRandomPlugin])
9850                .expect("the exact executable manifest should restore");
9851        assert_eq!(simulation.snapshot(), restored.snapshot());
9852
9853        let Err(error) = Simulation::from_snapshot_with_plugins(
9854            snapshot.clone(),
9855            &[&ChangedPrimaryRandomPlugin],
9856        ) else {
9857            panic!("changed executable semantics must not rehydrate an exact descriptor");
9858        };
9859        assert_eq!(error.code, ErrorCode::PluginManifestMismatch);
9860
9861        let mut changed_scenario = scenario.clone();
9862        changed_scenario.world.armies[0].strength += 1;
9863        let Err(error) = Simulation::new_with_run_configuration(
9864            91,
9865            changed_scenario,
9866            exact_manifest.clone(),
9867            run_configuration.clone(),
9868        ) else {
9869            panic!("a scenario must match its declared semantic identity");
9870        };
9871        assert_eq!(error.code, ErrorCode::InvalidRunManifest);
9872
9873        let mut corrupted_manifest_hash = snapshot.clone();
9874        let replacement = if corrupted_manifest_hash.run_manifest_hash.starts_with('f') {
9875            "e"
9876        } else {
9877            "f"
9878        };
9879        corrupted_manifest_hash
9880            .run_manifest_hash
9881            .replace_range(..1, replacement);
9882        let Err(error) = Simulation::from_snapshot(corrupted_manifest_hash) else {
9883            panic!("a tampered run manifest hash must not load");
9884        };
9885        assert_eq!(error.code, ErrorCode::InvalidRunManifest);
9886
9887        let replayed = Simulation::replay_with_run_configuration(
9888            91,
9889            scenario.clone(),
9890            exact_manifest.clone(),
9891            run_configuration.clone(),
9892            &[&PrimaryRandomPlugin],
9893            simulation.command_log(),
9894            simulation.command_attempts(),
9895            simulation.boundaries(),
9896            simulation.time(),
9897        )
9898        .expect("the exact run and plugin environment should replay");
9899        assert_eq!(simulation.snapshot(), replayed.snapshot());
9900
9901        let mut changed_environment = exact_manifest;
9902        let RunManifest::Declared { content, .. } = &mut changed_environment else {
9903            unreachable!("the fixture retains a declared manifest");
9904        };
9905        content[0].semantic_hash =
9906            "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_owned();
9907        let Err(error) = Simulation::replay_with_run_configuration(
9908            91,
9909            scenario,
9910            changed_environment,
9911            run_configuration,
9912            &[&PrimaryRandomPlugin],
9913            simulation.command_log(),
9914            simulation.command_attempts(),
9915            simulation.boundaries(),
9916            simulation.time(),
9917        ) else {
9918            panic!("replay under changed content identity must fail");
9919        };
9920        assert_eq!(error.code, ErrorCode::ReplayMismatch);
9921    }
9922
9923    #[test]
9924    fn plugin_reads_and_writes_are_limited_to_declared_owned_state() {
9925        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
9926        simulation
9927            .register_plugin(&SecretPlugin)
9928            .expect("secret owner should register");
9929        simulation
9930            .register_plugin(&UndeclaredAccessPlugin)
9931            .expect("access fixture should register");
9932        simulation
9933            .submit(CommandEnvelope::new(
9934                Issuer::Actor(ids.commander),
9935                Command::Plugin {
9936                    plugin: "secret-owner".to_owned(),
9937                    command: "seed".to_owned(),
9938                    payload: Value::Null,
9939                },
9940            ))
9941            .expect("the owner should write its declared state");
9942        let before = simulation
9943            .snapshot_json()
9944            .expect("snapshot should serialize");
9945
9946        for (command, expected) in [
9947            ("missing", ErrorCode::EntityNotFound),
9948            ("read", ErrorCode::UndeclaredStateRead),
9949            ("write", ErrorCode::UndeclaredStateWrite),
9950        ] {
9951            let error = simulation
9952                .submit(CommandEnvelope::new(
9953                    Issuer::Actor(ids.commander),
9954                    Command::Plugin {
9955                        plugin: "undeclared-access".to_owned(),
9956                        command: command.to_owned(),
9957                        payload: Value::Null,
9958                    },
9959                ))
9960                .expect_err("undeclared state access must fail");
9961            assert_eq!(error.code, expected);
9962            assert_eq!(
9963                before,
9964                simulation
9965                    .snapshot_json()
9966                    .expect("rejected access must leave no serialized change")
9967            );
9968        }
9969    }
9970
9971    #[test]
9972    fn typed_component_keys_isolate_adversarial_plugin_and_state_names() {
9973        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
9974        simulation
9975            .register_plugin(&CollisionPluginA)
9976            .expect("first collision fixture should register");
9977        simulation
9978            .register_plugin(&CollisionPluginB)
9979            .expect("second collision fixture should register");
9980        for (plugin, expected) in [("a", "first"), ("a/person:1/b", "second")] {
9981            simulation
9982                .submit(CommandEnvelope::new(
9983                    Issuer::Actor(ids.commander),
9984                    Command::Plugin {
9985                        plugin: plugin.to_owned(),
9986                        command: "write".to_owned(),
9987                        payload: Value::Null,
9988                    },
9989                ))
9990                .expect("adversarial key should remain isolated");
9991            assert!(
9992                simulation
9993                    .snapshot()
9994                    .plugin_components
9995                    .iter()
9996                    .any(|record| {
9997                        record.plugin == plugin
9998                            && record.value == Value::String(expected.to_owned())
9999                    })
10000            );
10001        }
10002        assert_eq!(simulation.snapshot().plugin_components.len(), 2);
10003    }
10004
10005    #[test]
10006    fn plugin_event_order_does_not_depend_on_registration_order() {
10007        let (scenario, ids) = demo_scenario();
10008        let mut first = Simulation::new(35, scenario.clone()).expect("demo should load");
10009        first
10010            .register_plugin(&MarkerPlugin {
10011                name: "zeta",
10012                writes: Vec::new(),
10013            })
10014            .expect("zeta should register");
10015        first
10016            .register_plugin(&MarkerPlugin {
10017                name: "alpha",
10018                writes: Vec::new(),
10019            })
10020            .expect("alpha should register");
10021
10022        let mut second = Simulation::new(35, scenario).expect("demo should load");
10023        second
10024            .register_plugin(&MarkerPlugin {
10025                name: "alpha",
10026                writes: Vec::new(),
10027            })
10028            .expect("alpha should register");
10029        second
10030            .register_plugin(&MarkerPlugin {
10031                name: "zeta",
10032                writes: Vec::new(),
10033            })
10034            .expect("zeta should register");
10035
10036        first
10037            .submit(move_order(&ids))
10038            .expect("first order should validate");
10039        second
10040            .submit(move_order(&ids))
10041            .expect("second order should validate");
10042        assert_eq!(first.snapshot(), second.snapshot());
10043        let marker_plugins: Vec<_> = first
10044            .events()
10045            .iter()
10046            .filter_map(|event| match &event.kind {
10047                EventKind::Plugin { plugin, .. } => Some(plugin.as_str()),
10048                _ => None,
10049            })
10050            .collect();
10051        assert_eq!(marker_plugins, vec!["alpha", "zeta"]);
10052    }
10053
10054    #[test]
10055    fn failed_command_application_rolls_back_every_serialized_change() {
10056        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
10057        simulation
10058            .register_plugin(&FailingPlugin)
10059            .expect("plugin should register");
10060        let before = simulation
10061            .snapshot_json()
10062            .expect("snapshot should serialize");
10063        let error = simulation
10064            .submit(CommandEnvelope::new(
10065                Issuer::Actor(ids.commander),
10066                Command::Plugin {
10067                    plugin: "failing-test".to_owned(),
10068                    command: "mutate".to_owned(),
10069                    payload: serde_json::json!({ "scheduled": false }),
10070                },
10071            ))
10072            .expect_err("the injected failure should reject the command");
10073        assert_eq!(error.code, ErrorCode::InvalidDuration);
10074        assert_eq!(
10075            before,
10076            simulation
10077                .snapshot_json()
10078                .expect("failed command must leave no mutation, event, or consumed ID")
10079        );
10080
10081        let panic_error = simulation
10082            .submit(CommandEnvelope::new(
10083                Issuer::Actor(ids.commander),
10084                Command::Plugin {
10085                    plugin: "failing-test".to_owned(),
10086                    command: "panic".to_owned(),
10087                    payload: Value::Null,
10088                },
10089            ))
10090            .expect_err("plugin panics must cross the boundary as structured errors");
10091        assert_eq!(panic_error.code, ErrorCode::PluginPanicked);
10092        assert_eq!(
10093            before,
10094            simulation
10095                .snapshot_json()
10096                .expect("a panicking plugin must leave no serialized change")
10097        );
10098
10099        let (mut ceiling_scenario, ceiling_ids) = demo_scenario();
10100        ceiling_scenario.start_time = SimTime::from_minutes(i64::MAX - 60);
10101        let mut ceiling = Simulation::new(35, ceiling_scenario)
10102            .expect("a scenario near the time ceiling should load");
10103        let ceiling_before = ceiling
10104            .snapshot_json()
10105            .expect("the ceiling fixture should serialize");
10106        let movement_error = ceiling
10107            .submit(move_order(&ceiling_ids))
10108            .expect_err("movement whose arrival overflows simulation time must fail");
10109        assert_eq!(movement_error.code, ErrorCode::InvalidDuration);
10110        let advance_error = ceiling
10111            .advance(SimDuration::hours(2))
10112            .expect_err("advancing beyond the time domain must fail");
10113        assert_eq!(advance_error.code, ErrorCode::InvalidDuration);
10114        assert_eq!(
10115            ceiling_before,
10116            ceiling
10117                .snapshot_json()
10118                .expect("time overflow must leave the simulation unchanged")
10119        );
10120
10121        ceiling
10122            .register_plugin(&FailingPlugin)
10123            .expect("registration should remain open after rejected execution");
10124        let scheduled_before = ceiling
10125            .snapshot_json()
10126            .expect("the registered ceiling fixture should serialize");
10127        let schedule_error = ceiling
10128            .submit(CommandEnvelope::new(
10129                Issuer::Actor(ceiling_ids.commander),
10130                Command::Plugin {
10131                    plugin: "failing-test".to_owned(),
10132                    command: "mutate".to_owned(),
10133                    payload: serde_json::json!({ "scheduled": true }),
10134                },
10135            ))
10136            .expect_err("plugin work whose target overflows simulation time must fail");
10137        assert_eq!(schedule_error.code, ErrorCode::InvalidDuration);
10138        assert_eq!(
10139            scheduled_before,
10140            ceiling
10141                .snapshot_json()
10142                .expect("rejected plugin scheduling must not mutate the simulation")
10143        );
10144    }
10145
10146    #[test]
10147    fn failed_scheduled_batch_restores_every_writable_domain() {
10148        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
10149        simulation
10150            .register_plugin(&FailingPlugin)
10151            .expect("plugin should register");
10152        simulation
10153            .submit(move_order(&ids))
10154            .expect("the arrival should schedule");
10155        let arrival_at = SimTime::EPOCH
10156            .checked_add(SimDuration::hours(18))
10157            .expect("arrival time should be representable");
10158        simulation
10159            .schedule_at(
10160                arrival_at,
10161                ScheduledAction::PluginDirective {
10162                    plugin: "failing-test".to_owned(),
10163                    directive: Box::new(SystemDirective::SetComponent {
10164                        state: StateKey::new("failure-fixture", "flag"),
10165                        entity: EntityRef::Army(ids.army),
10166                        component: "flag".to_owned(),
10167                        value: Value::Bool(true),
10168                        summary: "Set a flag after the arrival mutates state".to_owned(),
10169                    }),
10170                    allowed_writes: vec![StateKey::new("failure-fixture", "flag")],
10171                    cause: CauseRef::System("scheduled-rollback-fixture".to_owned()),
10172                    correlation_id: 0,
10173                },
10174            )
10175            .expect("the failing action should share the arrival timestamp");
10176        let cache_before = cache_fingerprint(&simulation);
10177        let before_boundary = simulation
10178            .snapshot_json()
10179            .expect("snapshot should serialize");
10180        let error = simulation
10181            .advance(SimDuration::hours(18))
10182            .expect_err("the scheduled batch should fail after the arrival");
10183        assert_eq!(error.code, ErrorCode::InvalidDuration);
10184        assert_eq!(
10185            before_boundary,
10186            simulation
10187                .snapshot_json()
10188                .expect("failed boundary must restore its clock, queue, state, events, and IDs")
10189        );
10190        assert_eq!(cache_fingerprint(&simulation), cache_before);
10191    }
10192
10193    #[test]
10194    fn failed_clock_only_advance_restores_time_and_commitments() {
10195        let (mut simulation, _) = Simulation::demo(35).expect("demo should load");
10196        simulation
10197            .state
10198            .metadata
10199            .commitment_cache
10200            .as_mut()
10201            .expect("current runtimes should maintain a commitment cache")
10202            .events
10203            .len = 2;
10204        let cache_before = cache_fingerprint(&simulation);
10205        let before = simulation
10206            .snapshot_json()
10207            .expect("snapshot should serialize");
10208        let error = simulation
10209            .advance(SimDuration::hours(1))
10210            .expect_err("the corrupt cache must abort clock-only advancement");
10211        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10212        assert_eq!(
10213            before,
10214            simulation
10215                .snapshot_json()
10216                .expect("failed clock advancement must restore serialized state")
10217        );
10218        assert_eq!(cache_fingerprint(&simulation), cache_before);
10219    }
10220
10221    #[test]
10222    fn snapshot_continuation_requires_exact_plugin_rehydration() {
10223        let (mut simulation, ids) = Simulation::demo(35).expect("demo should load");
10224        let plugin = AuthorityPlugin;
10225        simulation
10226            .register_plugin(&plugin)
10227            .expect("plugin should register");
10228        simulation
10229            .submit(CommandEnvelope::new(
10230                Issuer::Actor(ids.commander),
10231                Command::Plugin {
10232                    plugin: "authority-test".to_owned(),
10233                    command: "set_stance".to_owned(),
10234                    payload: Value::Null,
10235                },
10236            ))
10237            .expect("plugin command should succeed");
10238        let json = simulation
10239            .snapshot_json()
10240            .expect("snapshot should serialize");
10241
10242        let mut restored = Simulation::from_snapshot_json(&json).expect("snapshot should load");
10243        assert_eq!(
10244            restored
10245                .advance(SimDuration::ZERO)
10246                .expect_err("continuation without handlers must be blocked")
10247                .code,
10248            ErrorCode::PluginNotActive
10249        );
10250        let mismatch = MarkerPlugin {
10251            name: "authority-test",
10252            writes: Vec::new(),
10253        };
10254        assert_eq!(
10255            restored
10256                .register_plugin(&mismatch)
10257                .expect_err("a different executable manifest must be rejected")
10258                .code,
10259            ErrorCode::PluginManifestMismatch
10260        );
10261        restored
10262            .register_plugin(&plugin)
10263            .expect("the exact plugin manifest should rehydrate");
10264        restored
10265            .advance(SimDuration::ZERO)
10266            .expect("rehydrated snapshot should continue");
10267        assert_eq!(simulation.snapshot(), restored.snapshot());
10268    }
10269
10270    #[test]
10271    fn command_only_replay_journal_binds_the_recorded_plugin_environment() {
10272        let (scenario, ids) = demo_scenario();
10273        let mut registration_closed_only =
10274            Simulation::new(35, scenario.clone()).expect("demo should load");
10275        registration_closed_only
10276            .advance(SimDuration::ZERO)
10277            .expect("zero advance should close authoritative registration");
10278        let closure_journal = registration_closed_only.replay_journal();
10279        let closure_replay =
10280            Simulation::replay_from_journal(scenario.clone(), &[], &closure_journal)
10281                .expect("exact replay should reproduce registration closure without other work");
10282        assert_eq!(
10283            registration_closed_only.snapshot(),
10284            closure_replay.snapshot()
10285        );
10286
10287        let plugin = AuthorityPlugin;
10288        let mut simulation = Simulation::new(35, scenario.clone()).expect("demo should load");
10289        simulation
10290            .register_plugin(&plugin)
10291            .expect("plugin should register");
10292        simulation
10293            .submit(CommandEnvelope::new(
10294                Issuer::Actor(ids.commander),
10295                Command::Plugin {
10296                    plugin: "authority-test".to_owned(),
10297                    command: "set_stance".to_owned(),
10298                    payload: Value::Null,
10299                },
10300            ))
10301            .expect("plugin command should succeed");
10302
10303        let replay_without_plugins = Simulation::replay(
10304            35,
10305            scenario.clone(),
10306            simulation.command_log(),
10307            simulation.time(),
10308        );
10309        let Err(error) = replay_without_plugins else {
10310            panic!("plugin replay without executable handlers must fail");
10311        };
10312        assert_eq!(error.code, ErrorCode::PluginCommandNotFound);
10313        let journal = simulation.replay_journal();
10314        let exact =
10315            Simulation::replay_from_journal(scenario.clone(), &[&AuthorityPlugin], &journal)
10316                .expect("the exact command-only environment should replay");
10317        assert_eq!(simulation.snapshot(), exact.snapshot());
10318
10319        let Err(error) =
10320            Simulation::replay_from_journal(scenario.clone(), &[&ChangedAuthorityPlugin], &journal)
10321        else {
10322            panic!("changed handler semantics must fail before command-only replay");
10323        };
10324        assert_eq!(error.code, ErrorCode::ReplayEnvironmentMismatch);
10325
10326        let replayed = Simulation::replay_with_plugins(
10327            35,
10328            scenario,
10329            &[&plugin],
10330            simulation.command_log(),
10331            simulation.time(),
10332        )
10333        .expect("plugin-aware replay should succeed");
10334        assert_eq!(simulation.snapshot(), replayed.snapshot());
10335    }
10336
10337    #[test]
10338    fn canonical_ingress_orders_commands_packets_and_calendar_work() {
10339        let (scenario, ids) = demo_scenario();
10340        let plugin = CanonicalIngressPlugin;
10341        let mut simulation =
10342            Simulation::new(41, scenario.clone()).expect("ingress fixture should load");
10343        simulation
10344            .register_plugin(&plugin)
10345            .expect("canonical ingress plugin should register");
10346        let due_at = SimTime::EPOCH
10347            .checked_add(SimDuration::hours(1))
10348            .expect("fixture due time should be representable");
10349
10350        let information = simulation
10351            .enqueue_plugin_ingress(PluginIngressRequest::new(
10352                "canonical-ingress",
10353                "report",
10354                due_at,
10355                serde_json::json!({ "label": "field report" }),
10356            ))
10357            .expect("information should queue");
10358        let low_priority = simulation
10359            .enqueue_plugin_ingress(
10360                PluginIngressRequest::new(
10361                    "canonical-ingress",
10362                    "dispatch",
10363                    due_at,
10364                    serde_json::json!({ "label": "routine dispatch" }),
10365                )
10366                .with_priority(-10),
10367            )
10368            .expect("low-priority communication should queue");
10369        let acknowledgement = simulation
10370            .enqueue_plugin_ingress(PluginIngressRequest::new(
10371                "canonical-ingress",
10372                "ack",
10373                due_at,
10374                serde_json::json!({ "label": "received" }),
10375            ))
10376            .expect("acknowledgement should queue");
10377        let high_priority = simulation
10378            .enqueue_plugin_ingress(
10379                PluginIngressRequest::new(
10380                    "canonical-ingress",
10381                    "dispatch",
10382                    due_at,
10383                    serde_json::json!({ "label": "urgent dispatch" }),
10384                )
10385                .with_priority(10),
10386            )
10387            .expect("high-priority communication should queue");
10388        let calendar = simulation
10389            .schedule_calendar_boundary(due_at, vec![SystemCadence::Daily])
10390            .expect("daily calendar work should queue");
10391        let command_request = CommandRequest::new(
10392            CommandRequestId::new(77),
10393            0,
10394            move_order(&ids).at_time(due_at),
10395        );
10396        let command = simulation
10397            .enqueue_command(due_at, 0, command_request.clone())
10398            .expect("command should queue");
10399        let future_at = due_at
10400            .checked_add(SimDuration::hours(1))
10401            .expect("future ingress time should be representable");
10402        let future = simulation
10403            .enqueue_plugin_ingress(PluginIngressRequest::new(
10404                "canonical-ingress",
10405                "report",
10406                future_at,
10407                serde_json::json!({ "label": "future report" }),
10408            ))
10409            .expect("future information should queue without becoming visible early");
10410        assert_eq!(
10411            simulation
10412                .enqueue_command(due_at, 0, command_request.clone())
10413                .expect("an exact queued retry should be idempotent"),
10414            command
10415        );
10416        assert_eq!(simulation.ingress_log().len(), 7);
10417        let collision = simulation
10418            .enqueue_command(due_at, 1, command_request)
10419            .expect_err("a queued request-ID collision must fail closed");
10420        assert_eq!(collision.code, ErrorCode::IdempotencyConflict);
10421        let mixed_before = simulation.snapshot();
10422        let mixed = simulation
10423            .submit(move_order(&ids))
10424            .expect_err("legacy commands cannot bypass queued tracked ingress");
10425        assert_eq!(mixed.code, ErrorCode::MixedCommandIngress);
10426        assert_eq!(simulation.snapshot(), mixed_before);
10427
10428        let before_legacy_advance = simulation.snapshot();
10429        let error = simulation
10430            .advance(SimDuration::hours(1))
10431            .expect_err("legacy advancement cannot skip canonical ingress");
10432        assert_eq!(error.code, ErrorCode::InvalidBoundary);
10433        assert_eq!(simulation.snapshot(), before_legacy_advance);
10434
10435        let before_skipped_boundary = simulation.snapshot();
10436        let error = simulation
10437            .settle_boundary(BoundaryRequest::at(future_at))
10438            .expect_err("manual settlement cannot skip an earlier ingress due time");
10439        assert_eq!(error.code, ErrorCode::InvalidBoundary);
10440        assert_eq!(simulation.snapshot(), before_skipped_boundary);
10441
10442        let pending_json = simulation
10443            .snapshot_json()
10444            .expect("pending ingress should serialize");
10445        let mut restored = Simulation::from_snapshot_json_with_plugins(&pending_json, &[&plugin])
10446            .expect("pending ingress should restore with its plugin contract");
10447        assert_eq!(simulation.snapshot(), restored.snapshot());
10448
10449        let receipts = simulation
10450            .advance_canonical(SimDuration::hours(1))
10451            .expect("canonical advancement should settle every due input");
10452        let restored_receipts = restored
10453            .advance_canonical(SimDuration::hours(1))
10454            .expect("restored canonical ingress should settle identically");
10455        assert_eq!(receipts, restored_receipts);
10456        assert_eq!(simulation.snapshot(), restored.snapshot());
10457        assert_eq!(receipts.len(), 1);
10458
10459        let boundary = simulation
10460            .boundaries()
10461            .last()
10462            .expect("canonical advancement should publish a boundary");
10463        let mut altered_boundary = boundary.clone();
10464        altered_boundary.admitted_ingress.pop();
10465        assert_ne!(
10466            compute_boundary_hash(boundary).expect("boundary evidence should hash"),
10467            compute_boundary_hash(&altered_boundary)
10468                .expect("altered boundary evidence should hash"),
10469            "canonical admission evidence must be committed by the boundary chain",
10470        );
10471        assert_eq!(boundary.cadences, vec![SystemCadence::Daily]);
10472        assert_eq!(
10473            boundary.admitted_ingress,
10474            vec![
10475                command.ingress_id,
10476                high_priority.ingress_id,
10477                low_priority.ingress_id,
10478                acknowledgement.ingress_id,
10479                information.ingress_id,
10480                calendar.ingress_id,
10481            ]
10482        );
10483        assert_eq!(boundary.admitted_attempts, vec![CommandAttemptId::new(1)]);
10484        assert_eq!(boundary.admitted_commands, vec![CommandId::new(1)]);
10485        assert!(!boundary.admitted_ingress.contains(&future.ingress_id));
10486        let snapshot = simulation.snapshot();
10487        assert!(snapshot.plugin_components.iter().any(|component| {
10488            component.state == StateKey::new("ingress-fixture", "received")
10489                && component.value
10490                    == serde_json::json!([
10491                        "communication:dispatch:10",
10492                        "communication:dispatch:-10",
10493                        "acknowledgement:ack:0",
10494                        "information:report:0"
10495                    ])
10496        }));
10497        assert!(snapshot.plugin_components.iter().any(|component| {
10498            component.state == StateKey::new("ingress-fixture", "calendar")
10499                && component.value == Value::Bool(true)
10500        }));
10501
10502        let post_boundary_due = future_at
10503            .checked_add(SimDuration::hours(1))
10504            .expect("post-boundary ingress time should be representable");
10505        simulation
10506            .enqueue_plugin_ingress(PluginIngressRequest::new(
10507                "canonical-ingress",
10508                "report",
10509                post_boundary_due,
10510                serde_json::json!({ "label": "post-boundary report" }),
10511            ))
10512            .expect("future ingress may be authored after a completed boundary");
10513        let post_boundary_snapshot = simulation.snapshot();
10514        let post_boundary_restored =
10515            Simulation::from_snapshot_with_plugins(post_boundary_snapshot.clone(), &[&plugin])
10516                .expect("post-boundary pending ingress must not invalidate its own snapshot");
10517        assert_eq!(post_boundary_restored.snapshot(), post_boundary_snapshot);
10518
10519        let late_before = simulation.snapshot();
10520        let late = simulation
10521            .enqueue_plugin_ingress(PluginIngressRequest::new(
10522                "canonical-ingress",
10523                "report",
10524                SimTime::EPOCH,
10525                serde_json::json!({ "label": "late report" }),
10526            ))
10527            .expect_err("late ingress cannot rewrite an already committed boundary");
10528        assert_eq!(late.code, ErrorCode::LateIngress);
10529        assert_eq!(simulation.snapshot(), late_before);
10530
10531        let journal = simulation.replay_journal();
10532        let replayed = Simulation::replay_from_journal(scenario, &[&plugin], &journal)
10533            .expect("canonical ingress should replay in its recorded environment");
10534        assert_eq!(simulation.snapshot(), replayed.snapshot());
10535
10536        let mut reordered = simulation.snapshot();
10537        reordered.boundaries[0].admitted_ingress.swap(0, 1);
10538        rehash_tampered_snapshot(&mut reordered);
10539        let error = Simulation::from_snapshot_with_plugins(reordered, &[&plugin])
10540            .err()
10541            .expect("a rehashed noncanonical ingress order must not load");
10542        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10543
10544        let mut predating_issue_cut = simulation.snapshot();
10545        let last = predating_issue_cut
10546            .ingress
10547            .last_mut()
10548            .expect("the fixture retains post-boundary ingress");
10549        last.issued_at = SimTime::EPOCH;
10550        rehash_tampered_snapshot(&mut predating_issue_cut);
10551        let error = Simulation::from_snapshot_with_plugins(predating_issue_cut, &[&plugin])
10552            .err()
10553            .expect("ingress cannot predate its declared boundary issue cut");
10554        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10555
10556        let mut skipped_due_time = simulation.snapshot();
10557        let information_record = skipped_due_time
10558            .ingress
10559            .iter_mut()
10560            .find(|record| record.id == information.ingress_id)
10561            .expect("the information ingress should remain in the journal");
10562        information_record.due_at = SimTime::EPOCH;
10563        rehash_tampered_snapshot(&mut skipped_due_time);
10564        let error = Simulation::from_snapshot_with_plugins(skipped_due_time, &[&plugin])
10565            .err()
10566            .expect("a boundary cannot be forged past an earlier due ingress time");
10567        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10568    }
10569
10570    #[test]
10571    fn command_ingress_precedes_equal_time_internal_scheduled_work() {
10572        let (scenario, ids) = demo_scenario();
10573        let mut simulation = Simulation::new(47, scenario).expect("ordering fixture should load");
10574        simulation
10575            .enqueue_command(
10576                SimTime::EPOCH,
10577                0,
10578                CommandRequest::new(CommandRequestId::new(1), 0, move_order(&ids)),
10579            )
10580            .expect("the initial movement should queue");
10581        simulation
10582            .step_canonical()
10583            .expect("the movement boundary should settle")
10584            .expect("the queued movement supplies due work");
10585        let arrival_at = SimTime::EPOCH
10586            .checked_add(SimDuration::hours(18))
10587            .expect("arrival time should be representable");
10588        let return_order = CommandEnvelope::new(
10589            Issuer::Actor(ids.commander),
10590            Command::MoveArmy {
10591                army: ids.army,
10592                destination: ids.western_territory,
10593            },
10594        )
10595        .at_time(arrival_at);
10596        simulation
10597            .enqueue_command(
10598                arrival_at,
10599                0,
10600                CommandRequest::new(
10601                    CommandRequestId::new(2),
10602                    simulation.revision(),
10603                    return_order,
10604                ),
10605            )
10606            .expect("the equal-time return order should queue");
10607
10608        simulation
10609            .step_canonical()
10610            .expect("the equal-time boundary should settle")
10611            .expect("the arrival and command are both due");
10612        let attempt = simulation
10613            .command_attempts()
10614            .last()
10615            .expect("the queued command should leave attempt evidence");
10616        assert!(matches!(
10617            &attempt.outcome,
10618            CommandAttemptOutcome::Rejected { error }
10619                if error.code == ErrorCode::InvalidAuthority
10620                    && error.message.contains("already moving")
10621        ));
10622        assert_eq!(
10623            simulation
10624                .world()
10625                .army(ids.army)
10626                .expect("the army should remain present")
10627                .location,
10628            ids.eastern_territory,
10629            "the scheduled arrival executes after the command-class admission decision",
10630        );
10631    }
10632
10633    #[test]
10634    fn exact_replay_cannot_advance_past_unadmitted_due_ingress() {
10635        let (scenario, _) = demo_scenario();
10636        let mut simulation =
10637            Simulation::new(49, scenario.clone()).expect("replay fixture should load");
10638        let due_at = SimTime::EPOCH
10639            .checked_add(SimDuration::hours(1))
10640            .expect("due time should be representable");
10641        simulation
10642            .schedule_calendar_boundary(due_at, vec![SystemCadence::Daily])
10643            .expect("calendar ingress should queue");
10644        let forged_final = due_at
10645            .checked_add(SimDuration::hours(1))
10646            .expect("forged final time should be representable");
10647        let mut forged_snapshot = simulation.snapshot();
10648        forged_snapshot.now = forged_final;
10649        refresh_snapshot_commitments_and_checkpoint(&mut forged_snapshot);
10650        let mut journal = simulation.replay_journal();
10651        journal.final_time = forged_final;
10652        journal.checkpoint_hash = forged_snapshot.checkpoint_hash;
10653
10654        let error = Simulation::replay_from_journal(scenario, &[], &journal)
10655            .err()
10656            .expect("replay must not cross unadmitted due ingress");
10657        assert_eq!(error.code, ErrorCode::InvalidBoundary);
10658    }
10659
10660    #[test]
10661    fn snapshot_ingress_reconstructs_ordered_command_and_calendar_effects() {
10662        let (scenario, ids) = demo_scenario();
10663        let mut commands =
10664            Simulation::new(51, scenario.clone()).expect("command fixture should load");
10665        for (request_id, revision, priority, morale) in [(1, 0, 10, 80), (2, 1, 0, 90)] {
10666            let envelope = CommandEnvelope::new(
10667                Issuer::Debug,
10668                Command::DebugSetArmyMorale {
10669                    army: ids.army,
10670                    morale,
10671                },
10672            )
10673            .at_time(SimTime::EPOCH);
10674            commands
10675                .enqueue_command(
10676                    SimTime::EPOCH,
10677                    priority,
10678                    CommandRequest::new(CommandRequestId::new(request_id), revision, envelope),
10679                )
10680                .expect("ordered command should queue");
10681        }
10682        commands
10683            .step_canonical()
10684            .expect("command boundary should settle")
10685            .expect("commands supply due work");
10686        commands
10687            .schedule_calendar_boundary(
10688                SimTime::EPOCH
10689                    .checked_add(SimDuration::hours(1))
10690                    .expect("future time should be representable"),
10691                vec![SystemCadence::Daily],
10692            )
10693            .expect("future ingress keeps the snapshot beyond its boundary head");
10694        let mut reordered_commands = commands.snapshot();
10695        reordered_commands.ingress[0].priority = 0;
10696        reordered_commands.ingress[1].priority = 10;
10697        reordered_commands.boundaries[0].admitted_ingress.swap(0, 1);
10698        rehash_tampered_snapshot(&mut reordered_commands);
10699        let error = Simulation::from_snapshot(reordered_commands)
10700            .err()
10701            .expect("queue order cannot be detached from command-attempt order");
10702        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10703
10704        let mut relabeled_attempt = commands.snapshot();
10705        relabeled_attempt.command_attempts[0].ingress = CommandIngress::FrozenReplay;
10706        rehash_tampered_snapshot(&mut relabeled_attempt);
10707        let error = Simulation::from_snapshot(relabeled_attempt)
10708            .err()
10709            .expect("queued command attempts must retain live-request provenance");
10710        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10711
10712        let mut calendar = Simulation::new(53, scenario).expect("calendar fixture should load");
10713        calendar
10714            .schedule_calendar_boundary(SimTime::EPOCH, vec![SystemCadence::Daily])
10715            .expect("calendar work should queue");
10716        calendar
10717            .step_canonical()
10718            .expect("calendar boundary should settle")
10719            .expect("calendar work supplies a boundary");
10720        calendar
10721            .schedule_calendar_boundary(
10722                SimTime::EPOCH
10723                    .checked_add(SimDuration::hours(1))
10724                    .expect("future time should be representable"),
10725                vec![SystemCadence::Daily],
10726            )
10727            .expect("future calendar work keeps the snapshot beyond its boundary head");
10728        let mut omitted_calendar = calendar.snapshot();
10729        omitted_calendar.boundaries[0].cadences.clear();
10730        rehash_tampered_snapshot(&mut omitted_calendar);
10731        let error = Simulation::from_snapshot(omitted_calendar)
10732            .err()
10733            .expect("admitted calendar work must appear in boundary cadence evidence");
10734        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10735    }
10736
10737    #[test]
10738    fn generated_ingress_delay_must_be_representable() {
10739        let (mut scenario, _) = demo_scenario();
10740        let earliest = SimTime::from_minutes(i64::MIN);
10741        scenario.start_time = earliest;
10742        for actor in scenario.knowledge.actors.values_mut() {
10743            for army in actor.armies.values_mut() {
10744                army.observed_at = earliest;
10745                army.learned_at = earliest;
10746            }
10747        }
10748        let plugin = GeneratedIngressPlugin;
10749        let mut simulation =
10750            Simulation::new(55, scenario).expect("extreme-time ingress fixture should load");
10751        simulation
10752            .register_plugin(&plugin)
10753            .expect("generated ingress plugin should register");
10754        simulation
10755            .enqueue_plugin_ingress(PluginIngressRequest::new(
10756                "generated-ingress",
10757                "dispatch",
10758                earliest,
10759                serde_json::json!({ "label": "dispatch" }),
10760            ))
10761            .expect("extreme-time dispatch should queue");
10762        simulation
10763            .step_canonical()
10764            .expect("extreme-time boundary should settle")
10765            .expect("dispatch supplies due work");
10766        let mut overflow = simulation.snapshot();
10767        overflow.ingress[1].due_at = SimTime::from_minutes(i64::MAX);
10768        rehash_tampered_snapshot(&mut overflow);
10769        let error = Simulation::from_snapshot_with_plugins(overflow, &[&plugin])
10770            .err()
10771            .expect("generated delay must fit the simulation duration domain");
10772        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10773    }
10774
10775    #[test]
10776    fn boundary_generated_zero_delay_ingress_waits_for_the_next_same_time_boundary() {
10777        let (scenario, _) = demo_scenario();
10778        let plugin = GeneratedIngressPlugin;
10779        let mut simulation =
10780            Simulation::new(43, scenario.clone()).expect("generated ingress fixture should load");
10781        simulation
10782            .register_plugin(&plugin)
10783            .expect("generated ingress plugin should register");
10784        let dispatch = simulation
10785            .enqueue_plugin_ingress(
10786                PluginIngressRequest::new(
10787                    "generated-ingress",
10788                    "dispatch",
10789                    SimTime::EPOCH,
10790                    serde_json::json!({ "label": "dispatch" }),
10791                )
10792                .with_entity(EntityRef::Person(PersonId::new(1))),
10793            )
10794            .expect("dispatch should queue");
10795
10796        let first = simulation
10797            .step_canonical()
10798            .expect("the first canonical boundary should settle")
10799            .expect("the dispatch supplies due work");
10800        assert_eq!(first.settled_at, SimTime::EPOCH);
10801        assert_eq!(first.generated_ingress, vec![IngressId::new(2)]);
10802        assert_eq!(simulation.boundaries().len(), 1);
10803        assert_eq!(
10804            simulation.boundaries()[0].admitted_ingress,
10805            vec![dispatch.ingress_id]
10806        );
10807        assert_eq!(
10808            simulation.boundaries()[0]
10809                .generated_ingress
10810                .iter()
10811                .map(|generation| generation.ingress)
10812                .collect::<Vec<_>>(),
10813            vec![IngressId::new(2)],
10814        );
10815        let generation = &simulation.boundaries()[0].generated_ingress[0];
10816        assert_eq!(generation.plugin, "generated-ingress");
10817        assert_eq!(generation.system, "relay-ingress");
10818        assert_eq!(generation.phase, BoundaryPhase::DomainDeltaProposal);
10819        assert_eq!(generation.visibility, StateVisibility::SameBoundary);
10820        let mut altered_boundary = simulation.boundaries()[0].clone();
10821        altered_boundary.generated_ingress.clear();
10822        assert_ne!(
10823            compute_boundary_hash(&simulation.boundaries()[0])
10824                .expect("generated ingress evidence should hash"),
10825            compute_boundary_hash(&altered_boundary)
10826                .expect("altered generation evidence should hash"),
10827            "generated ingress evidence must be committed by the boundary chain",
10828        );
10829        assert!(
10830            !simulation
10831                .snapshot()
10832                .plugin_components
10833                .iter()
10834                .any(|component| {
10835                    component.state == StateKey::new("generated-ingress-fixture", "received")
10836                })
10837        );
10838
10839        let pending = simulation.snapshot();
10840        let mut restored = Simulation::from_snapshot_with_plugins(pending.clone(), &[&plugin])
10841            .expect("a pending generated acknowledgement should restore");
10842        assert_eq!(restored.snapshot(), pending);
10843
10844        let second = simulation
10845            .step_canonical()
10846            .expect("the generated acknowledgement boundary should settle")
10847            .expect("the acknowledgement remains due at the same simulation time");
10848        let restored_second = restored
10849            .step_canonical()
10850            .expect("the restored acknowledgement boundary should settle")
10851            .expect("the restored acknowledgement remains due");
10852        assert_eq!(second, restored_second);
10853        assert_eq!(second.settled_at, SimTime::EPOCH);
10854        assert!(second.generated_ingress.is_empty());
10855        assert_eq!(simulation.boundaries().len(), 2);
10856        assert_eq!(
10857            simulation.boundaries()[1].admitted_ingress,
10858            vec![IngressId::new(2)]
10859        );
10860        assert!(
10861            simulation
10862                .snapshot()
10863                .plugin_components
10864                .iter()
10865                .any(|component| {
10866                    component.state == StateKey::new("generated-ingress-fixture", "received")
10867                        && component.value == Value::Bool(true)
10868                })
10869        );
10870        assert_eq!(simulation.snapshot(), restored.snapshot());
10871
10872        let journal = simulation.replay_journal();
10873        let replayed = Simulation::replay_from_journal(scenario, &[&plugin], &journal)
10874            .expect("boundary-generated ingress should replay from its producing system");
10875        assert_eq!(simulation.snapshot(), replayed.snapshot());
10876
10877        let mut missing_generation_evidence = simulation.snapshot();
10878        missing_generation_evidence.boundaries[0]
10879            .generated_ingress
10880            .clear();
10881        rehash_tampered_snapshot(&mut missing_generation_evidence);
10882        let error = Simulation::from_snapshot_with_plugins(missing_generation_evidence, &[&plugin])
10883            .err()
10884            .expect("boundary-caused ingress without producer evidence must not load");
10885        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10886
10887        let mut false_producer = simulation.snapshot();
10888        false_producer.boundaries[0].generated_ingress[0].phase =
10889            BoundaryPhase::StrategicAggregation;
10890        rehash_tampered_snapshot(&mut false_producer);
10891        let error = Simulation::from_snapshot_with_plugins(false_producer, &[&plugin])
10892            .err()
10893            .expect("generated ingress must retain exact producer-stage provenance");
10894        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10895    }
10896
10897    #[test]
10898    fn read_only_runs_reject_live_plugin_ingress_without_mutation() {
10899        let (scenario, _) = demo_scenario();
10900        let configuration = RunConfiguration::read_only_observer();
10901        let manifest = manifest_for_configuration(&scenario, &configuration);
10902        let plugin = CanonicalIngressPlugin;
10903        let mut simulation =
10904            Simulation::new_with_run_configuration(47, scenario, manifest, configuration)
10905                .expect("read-only ingress fixture should load");
10906        simulation
10907            .register_plugin(&plugin)
10908            .expect("read-only ingress plugin should register");
10909        let before = simulation.snapshot();
10910        let error = simulation
10911            .enqueue_plugin_ingress(PluginIngressRequest::new(
10912                "canonical-ingress",
10913                "report",
10914                SimTime::EPOCH,
10915                serde_json::json!({ "label": "unauthorized live report" }),
10916            ))
10917            .expect_err("read-only runs cannot accept newly authored plugin ingress");
10918        assert_eq!(error.code, ErrorCode::InteractionReadOnly);
10919        assert_eq!(simulation.snapshot(), before);
10920
10921        simulation
10922            .append_ingress(
10923                SimTime::EPOCH,
10924                IngressClass::Information,
10925                0,
10926                IngressPayload::Plugin {
10927                    plugin: "canonical-ingress".to_owned(),
10928                    packet_type: "report".to_owned(),
10929                    payload: serde_json::json!({ "label": "forged live report" }),
10930                    affected_entities: Vec::new(),
10931                },
10932                None,
10933                false,
10934            )
10935            .expect("the fixture should construct coherent but unauthorized evidence");
10936        let error = Simulation::from_snapshot_with_plugins(simulation.snapshot(), &[&plugin])
10937            .err()
10938            .expect("snapshot validation must reject impossible read-only live ingress");
10939        assert_eq!(error.code, ErrorCode::InvalidSnapshot);
10940    }
10941}