1#![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;
118pub const STATE_REVISION_FORMAT_VERSION: u32 = 1;
120pub const ADMISSION_CURSOR_FORMAT_VERSION: u32 = 1;
122pub const COMMITMENT_FORMAT_VERSION: u32 = 1;
124pub 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)]
134pub 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 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 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 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 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 #[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 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 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
1130pub type SimulationSystemHandler =
1137 fn(&SimulationView<'_>, &SimEvent) -> Result<Vec<SystemDirective>, CanwuError>;
1138
1139pub type PluginCommandHandler =
1140 fn(&SimulationView<'_>, &CommandContext, &Value) -> Result<Vec<SystemDirective>, CanwuError>;
1141
1142pub trait SimulationPlugin {
1145 fn name(&self) -> &str;
1146 fn version(&self) -> &str;
1148 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 pub commitment_format_version: u32,
1686 #[serde(default, skip_serializing_if = "Option::is_none")]
1687 pub commitment_roots: Option<CommitmentRoots>,
1689 #[serde(default)]
1690 pub revision_format_version: u32,
1692 #[serde(default, skip_serializing_if = "is_zero_u64")]
1693 pub state_revision: u64,
1695 #[serde(default, skip_serializing_if = "is_zero_u32")]
1696 pub replay_revision_format_version: u32,
1698 #[serde(default, skip_serializing_if = "is_zero_u32")]
1699 pub admission_cursor_format_version: u32,
1701 #[serde(default, skip_serializing_if = "is_zero_u64")]
1702 pub admitted_attempt_count: u64,
1704 #[serde(default, skip_serializing_if = "is_zero_u64")]
1705 pub admitted_command_count: u64,
1707 #[serde(default, skip_serializing_if = "is_zero_u64")]
1708 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)]
1754pub 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 pub commitment_format_version: u32,
1773 pub revision_format_version: u32,
1775 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 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 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 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 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 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 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 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 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 #[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(¤t_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(), &[], ¤t.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}