1#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)]
4
5pub use canwu_core::{
6 ArmyId, BoundaryId, CommandAttemptId, CommandId, CommandRequestId, CoreEntityKind,
7 DecisionRequestId, DecisionTicketId, DecisionTraceId, DomainEntityKindClass, DomainEntityType,
8 DomainKindClass, DomainRecordKind, DomainRecordRef, DomainRecordType, DomainRecordVersionRef,
9 DomainRecordVersionSource, DomainValueKindClass, DomainValueType, EntityRef, EventId,
10 EvidenceRef, GovernmentId, HolderKnowledgeRecordId, IngressId, KnowledgeHolderPolicy,
11 KnowledgeHolderRef, KnowledgeRecordId, KnowledgeRecordKind, KnowledgeSchemaId, LetterId,
12 PersonId, RandomDrawId, ResourceId, RouteId, SchemaRegistry, SimulationGranularity,
13 TerritoryId, TypeSchema, TypedDomainRecordRef,
14};
15pub use canwu_event::{CauseRef, EventAudience, EventKind, EventKindError, SimEvent};
16pub use canwu_knowledge::{
17 ActorKnowledge, ArmyKnowledge, EstimateRange, KnowledgeCursor, KnowledgeHistoryView,
18 KnowledgeOrigin, KnowledgeQuery, KnowledgeQueryError, KnowledgeQueryResult, KnowledgeReadCut,
19 KnowledgeRecord, KnowledgeRecordDraft, KnowledgeRecordView, KnowledgeSnapshot, KnowledgeSource,
20 KnowledgeSubject, KnowledgeSubjectTarget, MAX_KNOWLEDGE_PAGE_SIZE,
21};
22pub use canwu_routing::{
23 DepartureSlot, DurationSample, PlanningSnapshot, ROUTING_ALGORITHM_VERSION, RouteCost,
24 RouteLeg, RoutePlan, RoutingAlgorithm, RoutingCache, RoutingConnection, RoutingConnectionRef,
25 RoutingEndpoint, RoutingEndpointKind, RoutingError, RoutingNetwork, RoutingNodeRef,
26 RoutingPolicy, RoutingRequest, TransferMode, TraversalModel, plan_route,
27};
28use canwu_sim::Simulation;
29pub use canwu_sim::{
30 ADMISSION_CURSOR_FORMAT_VERSION, ArchiveProvider, ArchiveStore, ArchiveStoreOutcome,
31 ArchivedEvidenceLocator, ArchivedEvidenceReceipt, ArchivedSegmentHeader, Army,
32 ArtifactManifest, BoundaryChange, BoundaryContext, BoundaryDirective, BoundaryEmission,
33 BoundaryEmissionKind, BoundaryIngressGeneration, BoundaryKnowledgeChange, BoundaryPhase,
34 BoundaryProposal, BoundaryReceipt, BoundaryRecord, BoundaryRequest, BoundarySystemContract,
35 BoundarySystemHandler, CHECKPOINT_JOURNAL_FORMAT_VERSION, COMMITMENT_FORMAT_VERSION,
36 CanwuError, CheckpointJournal, Command, CommandAttemptOutcome, CommandAttemptRecord,
37 CommandAuthority, CommandContext, CommandEnvelope, CommandIngress, CommandOutcome,
38 CommandPolicyContext, CommandReceipt, CommandRecord, CommandRejection, CommandRequest,
39 CommitmentRoots, CompactedSimulation, ControllerDecision, ControllerPolicy, DecisionAction,
40 DecisionAttemptErrorCode, DecisionAttemptOutcome, DecisionAttemptRecord, DecisionAuthority,
41 DecisionContext, DecisionController, DecisionControllerBinding, DecisionError,
42 DecisionErrorCode, DecisionEvaluation, DecisionExternalEvidence, DecisionFactorContribution,
43 DecisionIngressRequest, DecisionMutation, DecisionOption, DecisionOptionEvaluation,
44 DecisionOrigin, DecisionOutcome, DecisionPolicy, DecisionPolicyIdentity, DecisionPolicyKind,
45 DecisionRule, DecisionState, DecisionTicket, DecisionTicketDraft, DecisionTicketState,
46 DecisionTrace, DemoIds, DomainRecord, DomainRecordChange, DomainRecordClass, DomainRecordDraft,
47 DomainRecordLifecycle, DomainRecordMutation, DomainRecordMutationPolicy, DomainRecordOperation,
48 DomainRecordPage, DomainRecordSchema, DomainReference, DomainReferenceSchema,
49 DomainReferenceTarget, DomainReferenceTargetKind, ENGINE_VERSION, ErrorCode,
50 EvidenceArchiveIndex, EvidenceCursor, EvidenceIndexEntry, EvidenceItemLocator,
51 EvidenceJournalKind, EvidenceJournalRoots, EvidenceJournalSegment, EvidenceNestedLocator,
52 EvidenceSealToken, ExternalDecisionOption, ExternalDecisionRequest, ExternalDecisionResponse,
53 ExternalPolicy, Government, HumanDecisionResponse, HumanPolicy, IngressClass, IngressPayload,
54 IngressReceipt, IngressRecord, InteractionPolicy, Issuer, KnowledgeLimitsV1,
55 KnowledgeSubjectSchema, KnowledgeSubjectTargetKind, KnowledgeWriteGrant, LetterCargo,
56 LetterStatus, LlmModelIdentity, LlmPolicy, MapPoint, ObservationPolicy, OrderedRulePolicy,
57 OutboxEntry, PAYLOAD_REQUIRED_EVIDENCE_CONTINUATION_FIELD,
58 PAYLOAD_REQUIRED_EVIDENCE_CONTINUATION_FORMAT_VERSION, PayloadProperty,
59 PayloadRequiredEvidenceContinuationV1, PayloadSchema, PayloadValueType, Person,
60 PersonTransitState, PluginActionDescriptor, PluginCommandHandler, PluginComponentRecord,
61 PluginDescriptor, PluginIngressDescriptor, PluginIngressRequest, PluginIngressTarget,
62 PluginKnowledgeSchema, PluginRegistrar, PluginRegistry, PolicyDecision,
63 PreparedDecisionIngress, PreparedEvidenceSeal, QueuedExternalPolicy, QueuedHumanPolicy,
64 QueuedLlmPolicy, RUN_CONFIGURATION_FORMAT_VERSION, RUN_MANIFEST_FORMAT_VERSION,
65 RandomAlgorithm, RandomDrawAddress, RandomDrawOutcome, RandomDrawProducer, RandomDrawRecord,
66 RandomOperationAddressV1, RandomOperationTarget, RandomStreamKey, RandomStreamState,
67 ReplayJournal, ReservationAllocation, ReservationDisposition, ReservationOffer,
68 ReservationOfferRecord, ReservationPoolKey, ReservationRef, ReservationRequest,
69 ReservationRequestRecord, Route, RuleChoice, RulePolicy, RunConfiguration,
70 RunConfigurationSnapshot, RunManifest, RunPurpose, SNAPSHOT_FORMAT_VERSION,
71 STATE_REVISION_FORMAT_VERSION, Scenario, SeatBinding, SeatPolicy, SimulationCheckpoint,
72 SimulationPlugin, SimulationSnapshot, SimulationSystemHandler, SimulationView, StateKey,
73 StateVisibility, SystemCadence, SystemContract, SystemDirective, Territory, TracePolicy,
74 TransitState, UtilityEvaluator, UtilityPolicy, UtilityProfile, WeightedUtilityEvaluator,
75 WeightedUtilityPolicy, WorldSnapshot, canonical_byte_hash, canonical_hash,
76 payload_required_evidence_continuation_property_v1,
77};
78pub use canwu_time::{SimDuration, SimTime};
79pub use canwu_transport::{
80 CapacityBooking, CapacityBookingId, CapacityBookingStatus, DeliveryCompletionRequest,
81 DeliverySaga, Handoff, HandoffId, ItineraryRevision, ItineraryRevisionId,
82 ItineraryRevisionReason, LegExecution, LegExecutionId, LegExecutionStatus, MovementInitiative,
83 MovementOrder, MovementOrderError, MovementOrderId, MovementSubject, MovementSubjectRole,
84 SagaState, TRANSPORT_SEMANTIC_VERSION, TransportError, TransportExecution,
85 TransportExecutionId, TransportExecutionState, delivery_completion_operation_key,
86};
87use serde::{Deserialize, Serialize};
88
89pub struct Canwu {
91 simulation: Simulation,
92}
93
94pub struct CompactedCanwu {
96 simulation: CompactedSimulation,
97}
98
99impl Canwu {
100 #[must_use]
101 pub const fn version() -> &'static str {
102 ENGINE_VERSION
103 }
104
105 pub fn new(seed: u64, scenario: Scenario) -> Result<Self, CanwuError> {
106 Ok(Self {
107 simulation: Simulation::new(seed, scenario)?,
108 })
109 }
110
111 pub fn into_compacted(self) -> Result<CompactedCanwu, CanwuError> {
113 Ok(CompactedCanwu {
114 simulation: self.simulation.into_compacted()?,
115 })
116 }
117
118 pub fn new_with_plugins(
119 seed: u64,
120 scenario: Scenario,
121 plugins: &[&dyn SimulationPlugin],
122 ) -> Result<Self, CanwuError> {
123 Ok(Self {
124 simulation: Simulation::new_with_plugins(seed, scenario, plugins)?,
125 })
126 }
127
128 pub fn new_with_manifest(
129 seed: u64,
130 scenario: Scenario,
131 run_manifest: RunManifest,
132 ) -> Result<Self, CanwuError> {
133 Ok(Self {
134 simulation: Simulation::new_with_manifest(seed, scenario, run_manifest)?,
135 })
136 }
137
138 pub fn new_with_manifest_and_plugins(
139 seed: u64,
140 scenario: Scenario,
141 run_manifest: RunManifest,
142 plugins: &[&dyn SimulationPlugin],
143 ) -> Result<Self, CanwuError> {
144 Ok(Self {
145 simulation: Simulation::new_with_manifest_and_plugins(
146 seed,
147 scenario,
148 run_manifest,
149 plugins,
150 )?,
151 })
152 }
153
154 pub fn new_with_run_configuration(
155 seed: u64,
156 scenario: Scenario,
157 run_manifest: RunManifest,
158 run_configuration: RunConfiguration,
159 ) -> Result<Self, CanwuError> {
160 Ok(Self {
161 simulation: Simulation::new_with_run_configuration(
162 seed,
163 scenario,
164 run_manifest,
165 run_configuration,
166 )?,
167 })
168 }
169
170 pub fn new_with_run_configuration_and_plugins(
171 seed: u64,
172 scenario: Scenario,
173 run_manifest: RunManifest,
174 run_configuration: RunConfiguration,
175 plugins: &[&dyn SimulationPlugin],
176 ) -> Result<Self, CanwuError> {
177 Ok(Self {
178 simulation: Simulation::new_with_run_configuration_and_plugins(
179 seed,
180 scenario,
181 run_manifest,
182 run_configuration,
183 plugins,
184 )?,
185 })
186 }
187
188 pub fn demo(seed: u64) -> Result<Self, CanwuError> {
190 let (simulation, _) = Simulation::demo(seed)?;
191 Ok(Self { simulation })
192 }
193
194 #[must_use]
196 pub fn demo_ids() -> DemoIds {
197 let (_, ids) = canwu_sim::demo_scenario();
198 ids
199 }
200
201 #[must_use]
202 pub const fn time(&self) -> SimTime {
203 self.simulation.time()
204 }
205
206 #[must_use]
207 pub const fn run_manifest(&self) -> &RunManifest {
208 self.simulation.run_manifest()
209 }
210
211 #[must_use]
212 pub const fn run_configuration(&self) -> &RunConfigurationSnapshot {
213 self.simulation.run_configuration()
214 }
215
216 #[must_use]
217 pub fn revision(&self) -> u64 {
224 self.simulation.revision()
225 }
226
227 #[must_use]
228 pub fn run_manifest_hash(&self) -> &str {
229 self.simulation.run_manifest_hash()
230 }
231
232 #[must_use]
233 pub fn checkpoint_hash(&self) -> &str {
234 self.simulation.checkpoint_hash()
235 }
236
237 pub fn authoritative_state_hash(&self) -> Result<String, CanwuError> {
238 self.simulation.authoritative_state_hash()
239 }
240
241 pub fn entities(&self) -> impl Iterator<Item = &EntityRef> {
242 self.simulation.entities()
243 }
244
245 #[must_use]
246 pub fn entity_exists(&self, entity: &EntityRef) -> bool {
247 self.simulation.entity_exists(entity)
248 }
249
250 #[must_use]
252 pub fn world(&self) -> WorldSnapshot {
253 self.simulation.world()
254 }
255
256 #[must_use]
261 pub fn knowledge(&self) -> &KnowledgeSnapshot {
262 self.simulation.knowledge()
263 }
264
265 #[must_use]
266 pub fn events(&self) -> &[SimEvent] {
267 self.simulation.events()
268 }
269
270 #[must_use]
271 pub fn commands(&self) -> &[CommandRecord] {
272 self.simulation.command_log()
273 }
274
275 #[must_use]
276 pub fn boundaries(&self) -> &[BoundaryRecord] {
277 self.simulation.boundaries()
278 }
279
280 #[must_use]
281 pub fn command_attempts(&self) -> &[CommandAttemptRecord] {
282 self.simulation.command_attempts()
283 }
284
285 #[must_use]
286 pub fn ingress_log(&self) -> &[IngressRecord] {
287 self.simulation.ingress_log()
288 }
289
290 #[must_use]
291 pub fn domain_record(&self, reference: &DomainRecordRef) -> Option<&DomainRecord> {
292 self.simulation.domain_record(reference)
293 }
294
295 #[must_use]
297 pub fn domain_record_version_evidence_exists(
298 &self,
299 reference: &DomainRecordVersionRef,
300 ) -> bool {
301 self.simulation
302 .domain_record_version_evidence_exists(reference)
303 }
304
305 #[must_use]
307 pub fn evidence_exists(&self, reference: &EvidenceRef) -> bool {
308 self.simulation.evidence_exists(reference)
309 }
310
311 #[must_use]
316 pub fn evidence_time(&self, reference: &EvidenceRef) -> Option<SimTime> {
317 self.simulation.evidence_time(reference)
318 }
319
320 #[must_use]
325 pub fn domain_record_version(
326 &self,
327 reference: &DomainRecordVersionRef,
328 ) -> Option<DomainRecord> {
329 self.simulation.domain_record_version(reference)
330 }
331
332 #[must_use]
333 pub fn typed_domain_record<T: DomainRecordType>(
334 &self,
335 reference: &TypedDomainRecordRef<T>,
336 ) -> Option<&DomainRecord> {
337 self.simulation.typed_domain_record(reference)
338 }
339
340 pub fn domain_records(&self) -> impl Iterator<Item = &DomainRecord> {
341 self.simulation.domain_records()
342 }
343
344 pub fn domain_record_page(
348 &self,
349 kind: &DomainRecordKind,
350 after: Option<&DomainRecordRef>,
351 limit: usize,
352 expected_revision: Option<u64>,
353 ) -> Result<DomainRecordPage, CanwuError> {
354 self.simulation
355 .domain_record_page(kind, after, limit, expected_revision)
356 }
357
358 #[must_use]
359 pub const fn decision_state(&self) -> &DecisionState {
360 self.simulation.decision_state()
361 }
362
363 #[must_use]
364 pub fn decision_ticket(&self, id: DecisionTicketId) -> Option<&DecisionTicket> {
365 self.simulation.decision_ticket(id)
366 }
367
368 #[must_use]
369 pub fn decision_traces(&self) -> &[DecisionTrace] {
370 self.simulation.decision_traces()
371 }
372
373 #[must_use]
374 pub fn decision_attempts(&self) -> &[DecisionAttemptRecord] {
375 self.simulation.decision_attempts()
376 }
377
378 #[must_use]
379 pub fn random_draws(&self) -> &[RandomDrawRecord] {
380 self.simulation.random_draws()
381 }
382
383 #[must_use]
384 pub fn boundary_head_hash(&self) -> Option<&str> {
385 self.simulation.boundary_head_hash()
386 }
387
388 #[must_use]
389 pub const fn schema(&self) -> &SchemaRegistry {
390 self.simulation.schema()
391 }
392
393 pub fn plugin_descriptors(&self) -> impl Iterator<Item = &PluginDescriptor> {
394 self.simulation.plugin_descriptors()
395 }
396
397 #[must_use]
398 pub fn replay_journal(&self) -> ReplayJournal {
399 self.simulation.replay_journal()
400 }
401
402 pub fn outbox_entries(&self) -> Result<Vec<OutboxEntry>, CanwuError> {
403 self.simulation.outbox_entries()
404 }
405
406 pub fn evidence_cursor(&self) -> Result<EvidenceCursor, CanwuError> {
407 self.simulation.evidence_cursor()
408 }
409
410 pub fn checkpoint(&self) -> Result<SimulationCheckpoint, CanwuError> {
411 self.simulation.checkpoint()
412 }
413
414 pub fn journal_segment_since(
415 &self,
416 start: EvidenceCursor,
417 ) -> Result<EvidenceJournalSegment, CanwuError> {
418 self.simulation.journal_segment_since(start)
419 }
420
421 pub fn checkpoint_journal(&self) -> Result<CheckpointJournal, CanwuError> {
422 self.simulation.checkpoint_journal()
423 }
424
425 pub fn checkpoint_journal_json(&self) -> Result<String, CanwuError> {
426 self.simulation.checkpoint_journal_json()
427 }
428
429 pub fn register_plugin<P: SimulationPlugin + ?Sized>(
430 &mut self,
431 plugin: &P,
432 ) -> Result<(), CanwuError> {
433 self.simulation.register_plugin(plugin)
434 }
435
436 pub fn submit(&mut self, command: CommandEnvelope) -> Result<CommandReceipt, CanwuError> {
437 self.simulation.submit(command)
438 }
439
440 pub fn process_command(
441 &mut self,
442 request: CommandRequest,
443 ) -> Result<CommandOutcome, CanwuError> {
444 self.simulation.process_command(request)
445 }
446
447 pub fn enqueue_command(
448 &mut self,
449 due_at: SimTime,
450 priority: i32,
451 request: CommandRequest,
452 ) -> Result<IngressReceipt, CanwuError> {
453 self.simulation.enqueue_command(due_at, priority, request)
454 }
455
456 pub fn enqueue_plugin_ingress(
457 &mut self,
458 request: PluginIngressRequest,
459 ) -> Result<IngressReceipt, CanwuError> {
460 self.simulation.enqueue_plugin_ingress(request)
461 }
462
463 pub fn prepare_decision(
464 &self,
465 decision_request_id: DecisionRequestId,
466 command_request_id: Option<CommandRequestId>,
467 ticket_id: DecisionTicketId,
468 policy: &dyn DecisionPolicy,
469 ) -> Result<DecisionEvaluation, CanwuError> {
470 self.simulation
471 .prepare_decision(decision_request_id, command_request_id, ticket_id, policy)
472 }
473
474 pub fn prepare_decision_at(
475 &self,
476 due_at: SimTime,
477 decision_request_id: DecisionRequestId,
478 command_request_id: Option<CommandRequestId>,
479 ticket_id: DecisionTicketId,
480 policy: &dyn DecisionPolicy,
481 ) -> Result<DecisionEvaluation, CanwuError> {
482 self.simulation.prepare_decision_at(
483 due_at,
484 decision_request_id,
485 command_request_id,
486 ticket_id,
487 policy,
488 )
489 }
490
491 pub fn enqueue_decision(
492 &mut self,
493 due_at: SimTime,
494 priority: i32,
495 request: DecisionIngressRequest,
496 ) -> Result<IngressReceipt, CanwuError> {
497 self.simulation.enqueue_decision(due_at, priority, request)
498 }
499
500 pub fn drive_decision(
501 &mut self,
502 due_at: SimTime,
503 priority: i32,
504 decision_request_id: DecisionRequestId,
505 command_request_id: Option<CommandRequestId>,
506 ticket_id: DecisionTicketId,
507 policy: &dyn DecisionPolicy,
508 ) -> Result<DecisionEvaluation, CanwuError> {
509 self.simulation.drive_decision(
510 due_at,
511 priority,
512 decision_request_id,
513 command_request_id,
514 ticket_id,
515 policy,
516 )
517 }
518
519 pub fn schedule_calendar_boundary(
520 &mut self,
521 due_at: SimTime,
522 cadences: Vec<SystemCadence>,
523 ) -> Result<IngressReceipt, CanwuError> {
524 self.simulation.schedule_calendar_boundary(due_at, cadences)
525 }
526
527 pub fn advance_canonical(
528 &mut self,
529 duration: SimDuration,
530 ) -> Result<Vec<BoundaryReceipt>, CanwuError> {
531 self.simulation.advance_canonical(duration)
532 }
533
534 pub fn step_canonical(&mut self) -> Result<Option<BoundaryReceipt>, CanwuError> {
535 self.simulation.step_canonical()
536 }
537
538 pub fn advance(&mut self, duration: SimDuration) -> Result<Vec<SimEvent>, CanwuError> {
539 self.simulation.advance(duration)
540 }
541
542 pub fn settle_boundary(
543 &mut self,
544 request: BoundaryRequest,
545 ) -> Result<BoundaryReceipt, CanwuError> {
546 self.simulation.settle_boundary(request)
547 }
548
549 pub fn wait(&mut self, duration: SimDuration) -> Result<Vec<SimEvent>, CanwuError> {
550 self.advance(duration)
551 }
552
553 pub fn step(&mut self) -> Result<Vec<SimEvent>, CanwuError> {
554 self.simulation.step()
555 }
556
557 #[must_use]
558 pub fn snapshot(&self) -> SimulationSnapshot {
559 self.simulation.snapshot()
560 }
561
562 pub fn snapshot_json(&self) -> Result<String, CanwuError> {
563 self.simulation.snapshot_json()
564 }
565
566 pub fn from_snapshot_json(json: &str) -> Result<Self, CanwuError> {
567 Ok(Self {
568 simulation: Simulation::from_snapshot_json(json)?,
569 })
570 }
571
572 pub fn from_snapshot_json_with_plugins(
573 json: &str,
574 plugins: &[&dyn SimulationPlugin],
575 ) -> Result<Self, CanwuError> {
576 Ok(Self {
577 simulation: Simulation::from_snapshot_json_with_plugins(json, plugins)?,
578 })
579 }
580
581 pub fn from_checkpoint_and_journal(
582 checkpoint: SimulationCheckpoint,
583 segments: Vec<EvidenceJournalSegment>,
584 ) -> Result<Self, CanwuError> {
585 Ok(Self {
586 simulation: Simulation::from_checkpoint_and_journal(checkpoint, segments)?,
587 })
588 }
589
590 pub fn from_checkpoint_journal(bundle: CheckpointJournal) -> Result<Self, CanwuError> {
591 Ok(Self {
592 simulation: Simulation::from_checkpoint_journal(bundle)?,
593 })
594 }
595
596 pub fn from_checkpoint_journal_with_plugins(
597 bundle: CheckpointJournal,
598 plugins: &[&dyn SimulationPlugin],
599 ) -> Result<Self, CanwuError> {
600 Ok(Self {
601 simulation: Simulation::from_checkpoint_journal_with_plugins(bundle, plugins)?,
602 })
603 }
604
605 pub fn from_checkpoint_journal_json(json: &str) -> Result<Self, CanwuError> {
606 Ok(Self {
607 simulation: Simulation::from_checkpoint_journal_json(json)?,
608 })
609 }
610
611 pub fn from_checkpoint_journal_json_with_plugins(
612 json: &str,
613 plugins: &[&dyn SimulationPlugin],
614 ) -> Result<Self, CanwuError> {
615 Ok(Self {
616 simulation: Simulation::from_checkpoint_journal_json_with_plugins(json, plugins)?,
617 })
618 }
619
620 pub fn replay_from_journal(
621 plugins: &[&dyn SimulationPlugin],
622 journal: &ReplayJournal,
623 ) -> Result<Self, CanwuError> {
624 Ok(Self {
625 simulation: Simulation::replay_from_journal(plugins, journal)?,
626 })
627 }
628
629 pub fn replay_from_journal_json(
630 plugins: &[&dyn SimulationPlugin],
631 json: &str,
632 ) -> Result<Self, CanwuError> {
633 Ok(Self {
634 simulation: Simulation::replay_from_journal_json(plugins, json)?,
635 })
636 }
637
638 #[must_use]
639 pub fn fork(&self) -> Self {
640 Self {
641 simulation: self.simulation.fork(),
642 }
643 }
644
645 pub fn admin_query_knowledge(
648 &self,
649 holder: KnowledgeHolderRef,
650 query: &KnowledgeQuery,
651 ) -> Result<KnowledgeQueryResult, CanwuError> {
652 self.simulation
653 .knowledge()
654 .query_current(
655 holder,
656 query,
657 self.simulation.boundaries().last().map(|value| value.id),
658 )
659 .map_err(map_knowledge_query_error)
660 }
661
662 pub fn viewer(&self) -> Result<CanwuViewer<'_>, CanwuError> {
665 let principal = self.declared_observation_principal()?;
666 Ok(CanwuViewer {
667 canwu: self,
668 context: KnowledgeViewContext { principal },
669 })
670 }
671
672 pub fn viewer_for_actor(&self, actor: PersonId) -> Result<CanwuViewer<'_>, CanwuError> {
675 if !self.entity_exists(&EntityRef::Person(actor)) {
676 return Err(CanwuError::new(
677 ErrorCode::ActorNotFound,
678 format!("actor {actor} was not found"),
679 ));
680 }
681 let principal = match self.run_configuration().declared() {
682 Some(configuration)
683 if configuration.observation == ObservationPolicy::ActorBound
684 && configuration.seat == SeatPolicy::CharacterBound
685 && configuration
686 .seat_binding
687 .as_ref()
688 .and_then(|binding| binding.actor)
689 == Some(actor) =>
690 {
691 ObservationPrincipal::Person(actor)
692 }
693 Some(_) => {
694 return Err(CanwuError::new(
695 ErrorCode::InvalidAuthority,
696 "the persisted run policy does not authorize a character viewer",
697 ));
698 }
699 None => ObservationPrincipal::Person(actor),
700 };
701 Ok(CanwuViewer {
702 canwu: self,
703 context: KnowledgeViewContext { principal },
704 })
705 }
706
707 pub fn viewer_context(&self, actor: PersonId) -> Result<ViewerContext, CanwuError> {
708 let viewer = self.viewer_for_actor(actor)?;
709 Ok(ViewerContext {
710 principal: viewer.context.principal.clone(),
711 observation: ObservationPolicy::ActorBound,
712 checkpoint_hash: self.checkpoint_hash().to_owned(),
713 })
714 }
715
716 fn declared_observation_principal(&self) -> Result<ObservationPrincipal, CanwuError> {
717 let Some(configuration) = self.run_configuration().declared() else {
718 return Err(CanwuError::new(
719 ErrorCode::InvalidAuthority,
720 "legacy runs require viewer_for_actor with an existing character",
721 ));
722 };
723 match configuration.observation {
724 ObservationPolicy::ActorBound => match configuration.seat {
725 SeatPolicy::CharacterBound => configuration
726 .seat_binding
727 .as_ref()
728 .and_then(|binding| binding.actor)
729 .map(ObservationPrincipal::Person)
730 .ok_or_else(|| {
731 CanwuError::new(
732 ErrorCode::InvalidAuthority,
733 "character-bound observation lacks an actor binding",
734 )
735 }),
736 SeatPolicy::InstitutionBound => configuration
737 .seat_binding
738 .as_ref()
739 .and_then(|binding| binding.institution.clone())
740 .map(ObservationPrincipal::Institution)
741 .ok_or_else(|| {
742 CanwuError::new(
743 ErrorCode::InvalidAuthority,
744 "institution-bound observation lacks an institution binding",
745 )
746 }),
747 SeatPolicy::ObserverSeat | SeatPolicy::AdvisorSeat | SeatPolicy::None => {
748 Err(CanwuError::new(
749 ErrorCode::InvalidAuthority,
750 "actor-bound observation requires a character or institution seat",
751 ))
752 }
753 },
754 ObservationPolicy::PublicObserver => Ok(ObservationPrincipal::Public),
755 ObservationPolicy::ResearchFull => Ok(ObservationPrincipal::Research),
756 ObservationPolicy::DeveloperDiagnostic => Ok(ObservationPrincipal::Developer),
757 }
758 }
759
760 #[must_use]
761 pub fn explain(&self, request: &ExplanationRequest) -> Explanation {
762 match request {
763 ExplanationRequest::Event(event_id) => self.explain_event(*event_id),
764 ExplanationRequest::Failure(error) => Explanation {
765 summary: error.message.clone(),
766 causal_chain: vec![ExplanationStep {
767 label: format!("Validation failed: {:?}", error.code),
768 event: None,
769 }],
770 },
771 }
772 }
773
774 fn explain_event(&self, event_id: EventId) -> Explanation {
775 let mut chain = Vec::new();
776 let events = self.events();
777 let mut current = event_by_id(events, event_id);
778 while let Some(event) = current {
779 chain.push(ExplanationStep {
780 label: event.summary.clone(),
781 event: Some(event.id),
782 });
783 current = match &event.cause {
784 Some(CauseRef::Boundary(boundary)) => {
785 chain.push(ExplanationStep {
786 label: format!("Committed by boundary {boundary}"),
787 event: None,
788 });
789 None
790 }
791 Some(CauseRef::Event(parent)) => event_by_id(events, *parent),
792 Some(CauseRef::Command(command)) => {
793 chain.push(ExplanationStep {
794 label: format!("Accepted command {command}"),
795 event: None,
796 });
797 None
798 }
799 Some(CauseRef::System(system)) => {
800 chain.push(ExplanationStep {
801 label: format!("Produced by system {system}"),
802 event: None,
803 });
804 None
805 }
806 None => None,
807 };
808 }
809 Explanation {
810 summary: chain.first().map_or_else(
811 || "Event was not found".to_owned(),
812 |step| step.label.clone(),
813 ),
814 causal_chain: chain,
815 }
816 }
817}
818
819fn event_by_id(events: &[SimEvent], event_id: EventId) -> Option<&SimEvent> {
820 let index = usize::try_from(event_id.get().checked_sub(1)?).ok()?;
821 events.get(index).filter(|event| event.id == event_id)
822}
823
824impl CompactedCanwu {
825 pub fn from_checkpoint_and_journal(
826 checkpoint: SimulationCheckpoint,
827 segments: Vec<EvidenceJournalSegment>,
828 ) -> Result<Self, CanwuError> {
829 Ok(Self {
830 simulation: CompactedSimulation::from_checkpoint_and_journal(checkpoint, segments)?,
831 })
832 }
833
834 pub fn from_checkpoint_and_journal_with_plugins(
835 checkpoint: SimulationCheckpoint,
836 segments: Vec<EvidenceJournalSegment>,
837 plugins: &[&dyn SimulationPlugin],
838 ) -> Result<Self, CanwuError> {
839 Ok(Self {
840 simulation: CompactedSimulation::from_checkpoint_and_journal_with_plugins(
841 checkpoint, segments, plugins,
842 )?,
843 })
844 }
845
846 pub fn evidence_cursor(&self) -> Result<EvidenceCursor, CanwuError> {
847 self.simulation.evidence_cursor()
848 }
849
850 pub fn checkpoint(&self) -> Result<SimulationCheckpoint, CanwuError> {
851 self.simulation.checkpoint()
852 }
853
854 pub fn outbox_entries(&self) -> Result<Vec<OutboxEntry>, CanwuError> {
855 self.simulation.outbox_entries()
856 }
857
858 pub fn outbox_entries_for_segment(
859 &self,
860 segment: &EvidenceJournalSegment,
861 ) -> Result<Vec<OutboxEntry>, CanwuError> {
862 self.simulation.outbox_entries_for_segment(segment)
863 }
864
865 #[must_use]
866 pub fn archived_evidence_receipt(
867 &self,
868 reference: &EvidenceRef,
869 ) -> Option<&ArchivedEvidenceReceipt> {
870 self.simulation.archived_evidence_receipt(reference)
871 }
872
873 pub fn load_archived_evidence_segment(
874 &self,
875 reference: &EvidenceRef,
876 provider: &dyn ArchiveProvider,
877 ) -> Result<EvidenceJournalSegment, CanwuError> {
878 self.simulation
879 .load_archived_evidence_segment(reference, provider)
880 }
881
882 pub fn seal_evidence(&mut self) -> Result<Option<EvidenceJournalSegment>, CanwuError> {
883 self.simulation.seal_evidence()
884 }
885
886 pub fn prepare_evidence_seal(&self) -> Result<Option<PreparedEvidenceSeal>, CanwuError> {
887 self.simulation.prepare_evidence_seal()
888 }
889
890 pub fn commit_evidence_seal(
891 &mut self,
892 token: &EvidenceSealToken,
893 provider: &dyn ArchiveProvider,
894 ) -> Result<(), CanwuError> {
895 self.simulation.commit_evidence_seal(token, provider)
896 }
897
898 pub fn snapshot_with_segments(
899 &self,
900 segments: Vec<EvidenceJournalSegment>,
901 ) -> Result<SimulationSnapshot, CanwuError> {
902 self.simulation.snapshot_with_segments(segments)
903 }
904
905 pub fn replay_journal_with_segments(
906 &self,
907 segments: Vec<EvidenceJournalSegment>,
908 ) -> Result<ReplayJournal, CanwuError> {
909 self.simulation.replay_journal_with_segments(segments)
910 }
911
912 #[must_use]
913 pub const fn time(&self) -> SimTime {
914 self.simulation.time()
915 }
916
917 #[must_use]
918 pub const fn revision(&self) -> u64 {
919 self.simulation.revision()
920 }
921
922 #[must_use]
923 pub fn checkpoint_hash(&self) -> &str {
924 self.simulation.checkpoint_hash()
925 }
926
927 #[must_use]
928 pub fn boundary_head_hash(&self) -> Option<&str> {
929 self.simulation.boundary_head_hash()
930 }
931
932 pub fn entities(&self) -> impl Iterator<Item = &EntityRef> {
933 self.simulation.entities()
934 }
935
936 #[must_use]
937 pub fn entity_exists(&self, entity: &EntityRef) -> bool {
938 self.simulation.entity_exists(entity)
939 }
940
941 #[must_use]
943 pub fn world(&self) -> WorldSnapshot {
944 self.simulation.world()
945 }
946
947 #[must_use]
948 pub fn knowledge(&self) -> &KnowledgeSnapshot {
949 self.simulation.knowledge()
950 }
951
952 #[must_use]
953 pub fn domain_record(&self, reference: &DomainRecordRef) -> Option<&DomainRecord> {
954 self.simulation.domain_record(reference)
955 }
956
957 #[must_use]
958 pub fn typed_domain_record<T: DomainRecordType>(
959 &self,
960 reference: &TypedDomainRecordRef<T>,
961 ) -> Option<&DomainRecord> {
962 self.simulation.typed_domain_record(reference)
963 }
964
965 #[must_use]
966 pub const fn decision_state(&self) -> &DecisionState {
967 self.simulation.decision_state()
968 }
969
970 #[must_use]
971 pub fn decision_ticket(&self, id: DecisionTicketId) -> Option<&DecisionTicket> {
972 self.simulation.decision_ticket(id)
973 }
974
975 #[must_use]
976 pub fn decision_traces(&self) -> &[DecisionTrace] {
977 self.simulation.decision_traces()
978 }
979
980 #[must_use]
981 pub fn decision_attempts(&self) -> &[DecisionAttemptRecord] {
982 self.simulation.decision_attempts()
983 }
984
985 pub fn submit(&mut self, envelope: CommandEnvelope) -> Result<CommandReceipt, CanwuError> {
986 self.simulation.submit(envelope)
987 }
988
989 pub fn process_command(
990 &mut self,
991 request: CommandRequest,
992 ) -> Result<CommandOutcome, CanwuError> {
993 self.simulation.process_command(request)
994 }
995
996 pub fn enqueue_command(
997 &mut self,
998 due_at: SimTime,
999 priority: i32,
1000 request: CommandRequest,
1001 ) -> Result<IngressReceipt, CanwuError> {
1002 self.simulation.enqueue_command(due_at, priority, request)
1003 }
1004
1005 pub fn enqueue_plugin_ingress(
1006 &mut self,
1007 request: PluginIngressRequest,
1008 ) -> Result<IngressReceipt, CanwuError> {
1009 self.simulation.enqueue_plugin_ingress(request)
1010 }
1011
1012 pub fn prepare_decision(
1013 &self,
1014 decision_request_id: DecisionRequestId,
1015 command_request_id: Option<CommandRequestId>,
1016 ticket_id: DecisionTicketId,
1017 policy: &dyn DecisionPolicy,
1018 ) -> Result<DecisionEvaluation, CanwuError> {
1019 self.simulation
1020 .prepare_decision(decision_request_id, command_request_id, ticket_id, policy)
1021 }
1022
1023 pub fn prepare_decision_at(
1024 &self,
1025 due_at: SimTime,
1026 decision_request_id: DecisionRequestId,
1027 command_request_id: Option<CommandRequestId>,
1028 ticket_id: DecisionTicketId,
1029 policy: &dyn DecisionPolicy,
1030 ) -> Result<DecisionEvaluation, CanwuError> {
1031 self.simulation.prepare_decision_at(
1032 due_at,
1033 decision_request_id,
1034 command_request_id,
1035 ticket_id,
1036 policy,
1037 )
1038 }
1039
1040 pub fn enqueue_decision(
1041 &mut self,
1042 due_at: SimTime,
1043 priority: i32,
1044 request: DecisionIngressRequest,
1045 ) -> Result<IngressReceipt, CanwuError> {
1046 self.simulation.enqueue_decision(due_at, priority, request)
1047 }
1048
1049 pub fn drive_decision(
1050 &mut self,
1051 due_at: SimTime,
1052 priority: i32,
1053 decision_request_id: DecisionRequestId,
1054 command_request_id: Option<CommandRequestId>,
1055 ticket_id: DecisionTicketId,
1056 policy: &dyn DecisionPolicy,
1057 ) -> Result<DecisionEvaluation, CanwuError> {
1058 self.simulation.drive_decision(
1059 due_at,
1060 priority,
1061 decision_request_id,
1062 command_request_id,
1063 ticket_id,
1064 policy,
1065 )
1066 }
1067
1068 pub fn schedule_calendar_boundary(
1069 &mut self,
1070 due_at: SimTime,
1071 cadences: Vec<SystemCadence>,
1072 ) -> Result<IngressReceipt, CanwuError> {
1073 self.simulation.schedule_calendar_boundary(due_at, cadences)
1074 }
1075
1076 pub fn advance(&mut self, duration: SimDuration) -> Result<Vec<SimEvent>, CanwuError> {
1077 self.simulation.advance(duration)
1078 }
1079
1080 pub fn advance_canonical(
1081 &mut self,
1082 duration: SimDuration,
1083 ) -> Result<Vec<BoundaryReceipt>, CanwuError> {
1084 self.simulation.advance_canonical(duration)
1085 }
1086
1087 pub fn step_canonical(&mut self) -> Result<Option<BoundaryReceipt>, CanwuError> {
1088 self.simulation.step_canonical()
1089 }
1090
1091 pub fn settle_boundary(
1092 &mut self,
1093 request: BoundaryRequest,
1094 ) -> Result<BoundaryReceipt, CanwuError> {
1095 self.simulation.settle_boundary(request)
1096 }
1097}
1098
1099#[derive(Clone, Debug, Eq, PartialEq)]
1103pub enum ObservationPrincipal {
1104 Person(PersonId),
1105 Institution(EntityRef),
1106 Public,
1107 Research,
1108 Developer,
1109}
1110
1111impl ObservationPrincipal {
1112 const fn person(&self) -> Option<PersonId> {
1113 match self {
1114 Self::Person(actor) => Some(*actor),
1115 Self::Institution(_) | Self::Public | Self::Research | Self::Developer => None,
1116 }
1117 }
1118}
1119
1120#[derive(Clone, Debug, Eq, PartialEq)]
1121pub struct ViewerContext {
1122 principal: ObservationPrincipal,
1123 observation: ObservationPolicy,
1124 checkpoint_hash: String,
1125}
1126
1127impl ViewerContext {
1128 #[must_use]
1129 pub const fn principal(&self) -> &ObservationPrincipal {
1130 &self.principal
1131 }
1132
1133 #[must_use]
1134 pub const fn actor(&self) -> Option<PersonId> {
1135 self.principal.person()
1136 }
1137
1138 #[must_use]
1139 pub const fn observation(&self) -> ObservationPolicy {
1140 self.observation
1141 }
1142}
1143
1144#[derive(Clone, Debug)]
1145struct KnowledgeViewContext {
1146 principal: ObservationPrincipal,
1147}
1148
1149pub struct CanwuViewer<'a> {
1152 canwu: &'a Canwu,
1153 context: KnowledgeViewContext,
1154}
1155
1156impl CanwuViewer<'_> {
1157 #[must_use]
1158 pub const fn principal(&self) -> &ObservationPrincipal {
1159 &self.context.principal
1160 }
1161
1162 pub fn query_knowledge(
1166 &self,
1167 query: &KnowledgeQuery,
1168 ) -> Result<KnowledgeQueryResult, CanwuError> {
1169 let holder = match &self.context.principal {
1170 ObservationPrincipal::Person(actor) => KnowledgeHolderRef::Person(*actor),
1171 ObservationPrincipal::Institution(entity) => KnowledgeHolderRef::Entity(entity.clone()),
1172 ObservationPrincipal::Public => return Err(invalid_knowledge_authority()),
1173 ObservationPrincipal::Research | ObservationPrincipal::Developer => {
1174 return Err(CanwuError::new(
1175 ErrorCode::InvalidKnowledgeAuthority,
1176 "diagnostic viewers must select a holder explicitly",
1177 ));
1178 }
1179 };
1180 self.canwu.admin_query_knowledge(holder, query)
1181 }
1182
1183 pub fn query_holder_knowledge(
1186 &self,
1187 holder: KnowledgeHolderRef,
1188 query: &KnowledgeQuery,
1189 ) -> Result<KnowledgeQueryResult, CanwuError> {
1190 match self.context.principal {
1191 ObservationPrincipal::Research | ObservationPrincipal::Developer => {}
1192 ObservationPrincipal::Person(_)
1193 | ObservationPrincipal::Institution(_)
1194 | ObservationPrincipal::Public => return Err(invalid_knowledge_authority()),
1195 }
1196 if !knowledge_holder_exists(self.canwu, &holder) {
1197 return Err(CanwuError::new(
1198 ErrorCode::InvalidKnowledgeHolder,
1199 "the requested knowledge holder does not exist",
1200 ));
1201 }
1202 self.canwu.admin_query_knowledge(holder, query)
1203 }
1204
1205 pub fn audit_knowledge_record(
1208 &self,
1209 holder: &KnowledgeHolderRef,
1210 record: HolderKnowledgeRecordId,
1211 ) -> Result<KnowledgeRecord, CanwuError> {
1212 match self.context.principal {
1213 ObservationPrincipal::Research | ObservationPrincipal::Developer => {}
1214 ObservationPrincipal::Person(_)
1215 | ObservationPrincipal::Institution(_)
1216 | ObservationPrincipal::Public => return Err(invalid_knowledge_authority()),
1217 }
1218 if !knowledge_holder_exists(self.canwu, holder) {
1219 return Err(CanwuError::new(
1220 ErrorCode::InvalidKnowledgeHolder,
1221 "the requested knowledge holder does not exist",
1222 ));
1223 }
1224 let index = usize::try_from(record.get().saturating_sub(1)).map_err(|_| {
1225 CanwuError::new(
1226 ErrorCode::KnowledgeRecordNotFound,
1227 "holder-relative knowledge record ID is outside the supported range",
1228 )
1229 })?;
1230 self.canwu
1231 .knowledge()
1232 .for_holder(holder)
1233 .and_then(|records| records.values().nth(index))
1234 .cloned()
1235 .ok_or_else(|| {
1236 CanwuError::new(
1237 ErrorCode::KnowledgeRecordNotFound,
1238 "holder-relative knowledge record was not found",
1239 )
1240 })
1241 }
1242
1243 #[must_use]
1244 pub fn visible_changes_since(&self, since: SimTime) -> Vec<VisibleChange> {
1245 let context = ViewerContext {
1246 principal: self.context.principal.clone(),
1247 observation: observation_for_principal(&self.context.principal),
1248 checkpoint_hash: self.canwu.checkpoint_hash().to_owned(),
1249 };
1250 self.canwu
1251 .events()
1252 .iter()
1253 .filter(|event| event.timestamp > since)
1254 .filter_map(|event| {
1255 let audience = self.canwu.simulation.event_audience(event);
1256 visible_change(&context, event, &audience)
1257 })
1258 .collect()
1259 }
1260}
1261
1262const fn observation_for_principal(principal: &ObservationPrincipal) -> ObservationPolicy {
1263 match principal {
1264 ObservationPrincipal::Person(_) | ObservationPrincipal::Institution(_) => {
1265 ObservationPolicy::ActorBound
1266 }
1267 ObservationPrincipal::Public => ObservationPolicy::PublicObserver,
1268 ObservationPrincipal::Research => ObservationPolicy::ResearchFull,
1269 ObservationPrincipal::Developer => ObservationPolicy::DeveloperDiagnostic,
1270 }
1271}
1272
1273fn invalid_knowledge_authority() -> CanwuError {
1274 CanwuError::new(
1275 ErrorCode::InvalidKnowledgeAuthority,
1276 "this observation principal cannot read a private knowledge ledger",
1277 )
1278}
1279
1280fn knowledge_holder_exists(canwu: &Canwu, holder: &KnowledgeHolderRef) -> bool {
1281 match holder {
1282 KnowledgeHolderRef::Person(actor) => canwu.entity_exists(&EntityRef::Person(*actor)),
1283 KnowledgeHolderRef::Entity(entity) => canwu.entity_exists(entity),
1284 }
1285}
1286
1287fn map_knowledge_query_error(error: KnowledgeQueryError) -> CanwuError {
1288 match error {
1289 KnowledgeQueryError::ReadCutUnavailable => CanwuError::new(
1290 ErrorCode::KnowledgeReadCutUnavailable,
1291 "knowledge cursor read cut is no longer available",
1292 ),
1293 KnowledgeQueryError::InvalidLimit => CanwuError::new(
1294 ErrorCode::KnowledgeLimitExceeded,
1295 "knowledge query page size is outside the supported range",
1296 ),
1297 KnowledgeQueryError::InvalidCursor
1298 | KnowledgeQueryError::InvalidLedger
1299 | KnowledgeQueryError::Encoding => CanwuError::new(
1300 ErrorCode::InvalidKnowledgeRecord,
1301 "knowledge query, cursor, or ledger is invalid",
1302 ),
1303 }
1304}
1305
1306#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1307pub struct VisibleChange {
1308 pub timestamp: SimTime,
1309 pub summary: String,
1310 pub source_event: EventId,
1311}
1312
1313fn visible_change(
1314 viewer: &ViewerContext,
1315 event: &SimEvent,
1316 plugin_audience: &EventAudience,
1317) -> Option<VisibleChange> {
1318 let visible = event_visible_to(viewer, event, plugin_audience);
1319 visible.then(|| VisibleChange {
1320 timestamp: event.timestamp,
1321 summary: event.summary.clone(),
1322 source_event: event.id,
1323 })
1324}
1325
1326fn event_visible_to(viewer: &ViewerContext, event: &SimEvent, audience: &EventAudience) -> bool {
1327 if matches!(
1328 viewer.observation,
1329 ObservationPolicy::ResearchFull | ObservationPolicy::DeveloperDiagnostic
1330 ) {
1331 return true;
1332 }
1333 match audience {
1334 EventAudience::Public => true,
1335 EventAudience::Actor(actor) => viewer.principal.person() == Some(*actor),
1336 EventAudience::Actors(actors) => viewer
1337 .principal
1338 .person()
1339 .is_some_and(|actor| actors.binary_search(&actor).is_ok()),
1340 EventAudience::KnowledgeHolder(holder) => {
1341 principal_matches_holder(&viewer.principal, holder)
1342 }
1343 EventAudience::AffectedActors => viewer
1344 .principal
1345 .person()
1346 .is_some_and(|actor| event.affected_entities.contains(&EntityRef::Person(actor))),
1347 EventAudience::Private => false,
1348 }
1349}
1350
1351fn principal_matches_holder(principal: &ObservationPrincipal, holder: &KnowledgeHolderRef) -> bool {
1352 match (principal, holder) {
1353 (ObservationPrincipal::Person(actor), KnowledgeHolderRef::Person(holder)) => {
1354 actor == holder
1355 }
1356 (ObservationPrincipal::Institution(institution), KnowledgeHolderRef::Entity(holder)) => {
1357 institution == holder
1358 }
1359 (ObservationPrincipal::Research | ObservationPrincipal::Developer, _) => true,
1360 (
1361 ObservationPrincipal::Person(_)
1362 | ObservationPrincipal::Institution(_)
1363 | ObservationPrincipal::Public,
1364 _,
1365 ) => false,
1366 }
1367}
1368
1369#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1370#[serde(tag = "type", content = "value", rename_all = "snake_case")]
1371pub enum ExplanationRequest {
1372 Event(EventId),
1373 Failure(CanwuError),
1374}
1375
1376#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1377pub struct ExplanationStep {
1378 pub label: String,
1379 pub event: Option<EventId>,
1380}
1381
1382#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1383pub struct Explanation {
1384 pub summary: String,
1385 pub causal_chain: Vec<ExplanationStep>,
1386}
1387
1388#[cfg(any())]
1391mod tests {
1392 use super::*;
1393
1394 fn manifest_for_configuration(
1395 scenario: &Scenario,
1396 configuration: &RunConfiguration,
1397 ) -> RunManifest {
1398 let scenario_manifest =
1399 ArtifactManifest::for_scenario("fixture", "viewer-scenario", "1", scenario)
1400 .expect("scenario manifest should hash");
1401 let configuration_manifest = ArtifactManifest::for_run_configuration(
1402 "fixture",
1403 "viewer-configuration",
1404 "1",
1405 configuration,
1406 )
1407 .expect("run configuration manifest should hash");
1408 RunManifest::declared(scenario_manifest, configuration_manifest)
1409 }
1410
1411 struct VisibilityPlugin {
1412 audience: EventAudience,
1413 }
1414
1415 #[allow(clippy::unnecessary_wraps)]
1416 fn visibility_system(
1417 _view: &SimulationView<'_>,
1418 event: &SimEvent,
1419 ) -> Result<Vec<SystemDirective>, CanwuError> {
1420 if !event.kind.is_type("move_ordered") {
1421 return Ok(Vec::new());
1422 }
1423 Ok(vec![SystemDirective::Emit {
1424 event_type: "notice".to_owned(),
1425 summary: "a plugin visibility notice".to_owned(),
1426 affected: vec![EntityRef::Person(PersonId::new(1))],
1427 }])
1428 }
1429
1430 impl SimulationPlugin for VisibilityPlugin {
1431 fn name(&self) -> &'static str {
1432 "visibility-test"
1433 }
1434
1435 fn version(&self) -> &'static str {
1436 "test-v1"
1437 }
1438
1439 fn semantic_hash(&self) -> &'static str {
1440 "0000000000000000000000000000000000000000000000000000000000000001"
1441 }
1442
1443 fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
1444 registrar.register_event_audience("notice", self.audience.clone())?;
1445 registrar.register_system(
1446 SystemContract::event_driven(
1447 "emit-notice",
1448 BoundaryPhase::PerspectiveAndReportMaterialization,
1449 ),
1450 visibility_system,
1451 )
1452 }
1453 }
1454
1455 #[test]
1456 fn plugin_event_visibility_respects_public_actor_and_private_audiences() {
1457 let ids = Canwu::demo_ids();
1458 let event = SimEvent {
1459 id: EventId::new(1),
1460 timestamp: SimTime::EPOCH,
1461 kind: EventKind::plugin("visibility-test", "notice"),
1462 affected_entities: vec![EntityRef::Person(ids.commander)],
1463 summary: "notice".to_owned(),
1464 cause: None,
1465 correlation_id: 1,
1466 };
1467 let actor = ViewerContext {
1468 principal: ObservationPrincipal::Person(ids.commander),
1469 observation: ObservationPolicy::ActorBound,
1470 checkpoint_hash: String::new(),
1471 };
1472 let observer = ViewerContext {
1473 principal: ObservationPrincipal::Person(ids.observer),
1474 observation: ObservationPolicy::ActorBound,
1475 checkpoint_hash: String::new(),
1476 };
1477 let public_observer = ViewerContext {
1478 principal: ObservationPrincipal::Public,
1479 observation: ObservationPolicy::PublicObserver,
1480 checkpoint_hash: String::new(),
1481 };
1482 let research = ViewerContext {
1483 principal: ObservationPrincipal::Research,
1484 observation: ObservationPolicy::ResearchFull,
1485 checkpoint_hash: String::new(),
1486 };
1487
1488 assert!(visible_change(&actor, &event, &EventAudience::Public).is_some());
1489 assert!(visible_change(&public_observer, &event, &EventAudience::Public).is_some());
1490 assert!(visible_change(&actor, &event, &EventAudience::Actor(ids.commander)).is_some());
1491 assert!(visible_change(&observer, &event, &EventAudience::Actor(ids.commander)).is_none());
1492 assert!(visible_change(&observer, &event, &EventAudience::Private).is_none());
1493 assert!(visible_change(&research, &event, &EventAudience::Private).is_some());
1494 }
1495
1496 #[test]
1497 fn observe_changes_since_uses_persisted_plugin_audience() {
1498 let ids = Canwu::demo_ids();
1499 let mut canwu = Canwu::demo(35).expect("demo should load");
1500 canwu
1501 .register_plugin(&VisibilityPlugin {
1502 audience: EventAudience::Public,
1503 })
1504 .expect("visibility plugin should register");
1505 let since = SimTime::from_minutes(-1);
1506 canwu
1507 .act(
1508 ids.commander,
1509 SemanticAction::MoveEntity {
1510 subject: EntityRef::Army(ids.army),
1511 destination: ids.eastern_territory,
1512 cargo: Vec::new(),
1513 },
1514 )
1515 .expect("movement should emit plugin notice");
1516
1517 let observer = canwu
1518 .observe(
1519 ids.observer,
1520 &ObserveRequest {
1521 focus: ObservationFocus::Changes,
1522 since: Some(since),
1523 },
1524 )
1525 .expect("observer should be authorized");
1526 assert!(
1527 observer
1528 .changes_since
1529 .iter()
1530 .any(|change| change.summary == "a plugin visibility notice")
1531 );
1532
1533 let snapshot_json = canwu
1534 .snapshot_json()
1535 .expect("audience declaration should serialize");
1536 let restored = Canwu::from_snapshot_json_with_plugins(
1537 &snapshot_json,
1538 &[&VisibilityPlugin {
1539 audience: EventAudience::Public,
1540 }],
1541 )
1542 .expect("audience declaration should survive snapshot loading");
1543 let restored_observer = restored
1544 .observe(
1545 ids.observer,
1546 &ObserveRequest {
1547 focus: ObservationFocus::Changes,
1548 since: Some(since),
1549 },
1550 )
1551 .expect("restored observer should be authorized");
1552 assert!(
1553 restored_observer
1554 .changes_since
1555 .iter()
1556 .any(|change| change.summary == "a plugin visibility notice")
1557 );
1558 }
1559
1560 #[test]
1561 fn observe_with_viewer_revalidates_input_control_context() {
1562 let canwu = Canwu::demo(35).expect("demo should load");
1563 let escalated = ViewerContext {
1564 principal: ObservationPrincipal::Research,
1565 observation: ObservationPolicy::ResearchFull,
1566 checkpoint_hash: canwu.checkpoint_hash().to_owned(),
1567 };
1568
1569 let error = canwu
1570 .observe_with_viewer(&escalated, &ObserveRequest::default())
1571 .expect_err("a caller cannot self-escalate the observation policy");
1572 assert_eq!(error.code, ErrorCode::InvalidAuthority);
1573 }
1574
1575 #[test]
1576 #[allow(clippy::too_many_lines)]
1577 fn restricted_viewer_derives_principal_and_rejects_public_private_reads() {
1578 let (scenario, ids) = canwu_sim::demo_scenario();
1579 let actor = Canwu::demo(69).expect("actor viewer fixture should initialize");
1580 let actor_viewer = actor
1581 .viewer_for_actor(ids.commander)
1582 .expect("legacy character viewer should derive");
1583 let error = actor_viewer
1584 .audit_knowledge_record(
1585 &KnowledgeHolderRef::Person(ids.commander),
1586 HolderKnowledgeRecordId::new(1),
1587 )
1588 .expect_err("actor viewers cannot read audit-bearing records");
1589 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
1590
1591 let public_configuration = RunConfiguration::read_only_observer();
1592 let public_manifest = manifest_for_configuration(&scenario, &public_configuration);
1593 let public = Canwu::new_with_run_configuration(
1594 71,
1595 scenario.clone(),
1596 public_manifest,
1597 public_configuration,
1598 )
1599 .expect("public viewer fixture should initialize");
1600 let public_viewer = public.viewer().expect("public principal should derive");
1601 assert_eq!(public_viewer.principal(), &ObservationPrincipal::Public);
1602 let error = public_viewer
1603 .query_knowledge(&KnowledgeQuery::default())
1604 .expect_err("public principal cannot read a private ledger");
1605 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
1606 let error = public_viewer
1607 .query_holder_knowledge(
1608 KnowledgeHolderRef::Person(ids.commander),
1609 &KnowledgeQuery::default(),
1610 )
1611 .expect_err("an arbitrary valid actor ID cannot upgrade a public viewer");
1612 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
1613 let error = public_viewer
1614 .audit_knowledge_record(
1615 &KnowledgeHolderRef::Person(ids.commander),
1616 HolderKnowledgeRecordId::new(1),
1617 )
1618 .expect_err("public viewers cannot read audit-bearing records");
1619 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
1620
1621 let institution_configuration = RunConfiguration {
1622 format_version: RUN_CONFIGURATION_FORMAT_VERSION,
1623 purpose: RunPurpose::Play,
1624 controller: ControllerPolicy::HumanRoleBound,
1625 seat: SeatPolicy::InstitutionBound,
1626 observation: ObservationPolicy::ActorBound,
1627 interaction: InteractionPolicy::EraInternalCommands,
1628 trace: TracePolicy::Causal,
1629 seat_binding: Some(SeatBinding {
1630 seat_id: "institution-seat".to_owned(),
1631 controller_id: "institution-controller".to_owned(),
1632 actor: Some(ids.commander),
1633 institution: Some(EntityRef::Government(ids.government)),
1634 permission_profile_id: "institution-profile".to_owned(),
1635 }),
1636 declared_interventions: Vec::new(),
1637 diagnostic_commands_enabled: false,
1638 require_idempotency_keys: true,
1639 };
1640 let institution_manifest =
1641 manifest_for_configuration(&scenario, &institution_configuration);
1642 let institution = Canwu::new_with_run_configuration(
1643 73,
1644 scenario.clone(),
1645 institution_manifest,
1646 institution_configuration,
1647 )
1648 .expect("institution viewer fixture should initialize");
1649 let institution_viewer = institution
1650 .viewer()
1651 .expect("institution principal should derive");
1652 assert_eq!(
1653 institution_viewer.principal(),
1654 &ObservationPrincipal::Institution(EntityRef::Government(ids.government))
1655 );
1656 assert_eq!(
1657 institution_viewer
1658 .query_knowledge(&KnowledgeQuery::default())
1659 .expect("institution may query only its bound ledger")
1660 .holder,
1661 KnowledgeHolderRef::Entity(EntityRef::Government(ids.government))
1662 );
1663 assert!(institution.viewer_for_actor(ids.commander).is_err());
1664 let error = institution_viewer
1665 .audit_knowledge_record(
1666 &KnowledgeHolderRef::Entity(EntityRef::Government(ids.government)),
1667 HolderKnowledgeRecordId::new(1),
1668 )
1669 .expect_err("institution viewers cannot read audit-bearing records");
1670 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
1671
1672 let mut research_configuration = RunConfiguration::read_only_observer();
1673 research_configuration.observation = ObservationPolicy::ResearchFull;
1674 let research_manifest = manifest_for_configuration(&scenario, &research_configuration);
1675 let research = Canwu::new_with_run_configuration(
1676 79,
1677 scenario,
1678 research_manifest,
1679 research_configuration,
1680 )
1681 .expect("research viewer fixture should initialize");
1682 let research_viewer = research.viewer().expect("research principal should derive");
1683 assert_eq!(research_viewer.principal(), &ObservationPrincipal::Research);
1684 assert_eq!(
1685 research_viewer
1686 .query_holder_knowledge(
1687 KnowledgeHolderRef::Person(ids.commander),
1688 &KnowledgeQuery::default(),
1689 )
1690 .expect("research may explicitly select an existing holder")
1691 .holder,
1692 KnowledgeHolderRef::Person(ids.commander)
1693 );
1694 }
1695
1696 #[test]
1697 fn detached_viewer_context_is_bound_to_the_authorized_checkpoint() {
1698 let mut canwu = Canwu::demo(83).expect("demo should load");
1699 let ids = Canwu::demo_ids();
1700 let context = canwu
1701 .viewer_context(ids.commander)
1702 .expect("the commander should receive a detached viewer context");
1703
1704 canwu
1705 .act(
1706 ids.commander,
1707 SemanticAction::MoveEntity {
1708 subject: EntityRef::Army(ids.army),
1709 destination: ids.eastern_territory,
1710 cargo: Vec::new(),
1711 },
1712 )
1713 .expect("the authoritative checkpoint should advance");
1714 let error = canwu
1715 .observe_with_viewer(&context, &ObserveRequest::default())
1716 .expect_err("a context from an older checkpoint must be rejected");
1717 assert_eq!(error.code, ErrorCode::InvalidAuthority);
1718
1719 let refreshed = canwu
1720 .viewer_context(ids.commander)
1721 .expect("the current checkpoint should issue a fresh context");
1722 canwu
1723 .observe_with_viewer(&refreshed, &ObserveRequest::default())
1724 .expect("the refreshed context should remain authorized");
1725 }
1726
1727 #[test]
1728 fn actor_relative_observation_does_not_leak_arrival() {
1729 let mut canwu = Canwu::demo(35).expect("demo should load");
1730 let ids = Canwu::demo_ids();
1731 canwu
1732 .act(
1733 ids.commander,
1734 SemanticAction::MoveEntity {
1735 subject: EntityRef::Army(ids.army),
1736 destination: ids.eastern_territory,
1737 cargo: Vec::new(),
1738 },
1739 )
1740 .expect("commander can move army");
1741 canwu
1742 .advance(SimDuration::days(1))
1743 .expect("arrival should execute");
1744
1745 assert_eq!(
1746 canwu.world().army(ids.army).expect("army exists").location,
1747 ids.eastern_territory
1748 );
1749 let observer = canwu
1750 .observe(ids.observer, &ObserveRequest::default())
1751 .expect("observer exists");
1752 assert_eq!(
1753 observer.known_armies[0].known_location,
1754 Some(ids.central_territory)
1755 );
1756 let person_rows = canwu
1757 .query_as(ids.observer, &Query::all(QueryEntity::Person))
1758 .expect("actor query should succeed");
1759 assert_eq!(person_rows.rows.len(), 1);
1760 assert_eq!(person_rows.rows[0].get("id"), Some(&json!(ids.observer)));
1761 for entity in [
1762 QueryEntity::Government,
1763 QueryEntity::Territory,
1764 QueryEntity::Route,
1765 ] {
1766 assert!(
1767 canwu
1768 .query_as(ids.observer, &Query::all(entity))
1769 .expect("actor query should succeed")
1770 .rows
1771 .is_empty()
1772 );
1773 }
1774 assert!(
1775 canwu
1776 .inspect(
1777 ids.observer,
1778 &EntityRef::Person(ids.commander),
1779 DetailLevel::RawFields,
1780 )
1781 .expect("inspection should succeed")
1782 .fields
1783 .is_empty()
1784 );
1785 assert!(
1786 canwu
1787 .inspect(
1788 ids.observer,
1789 &EntityRef::Territory(ids.eastern_territory),
1790 DetailLevel::RawFields,
1791 )
1792 .expect("inspection should succeed")
1793 .fields
1794 .is_empty()
1795 );
1796
1797 canwu
1798 .advance(SimDuration::days(3))
1799 .expect("report should arrive");
1800 let updated = canwu
1801 .observe(ids.observer, &ObserveRequest::default())
1802 .expect("observer exists");
1803 assert_eq!(
1804 updated.known_armies[0].known_location,
1805 Some(ids.eastern_territory)
1806 );
1807 }
1808
1809 #[test]
1810 fn self_move_is_an_actor_bound_order_movement() {
1811 let mut canwu = Canwu::demo(35).expect("demo should load");
1812 let ids = Canwu::demo_ids();
1813 let actions = canwu
1814 .available_actions(ids.commander)
1815 .expect("commander actions should be available");
1816 assert!(actions.iter().any(|action| {
1817 action.action_type == "self_move"
1818 && action.payload["destination"] == json!(ids.eastern_territory)
1819 }));
1820
1821 canwu
1822 .act(
1823 ids.commander,
1824 SemanticAction::SelfMove {
1825 destination: ids.eastern_territory,
1826 cargo: Vec::new(),
1827 },
1828 )
1829 .expect("a person may order their own movement");
1830 assert!(
1831 canwu
1832 .world()
1833 .person(ids.commander)
1834 .expect("commander exists")
1835 .transit
1836 .is_some()
1837 );
1838 }
1839
1840 #[test]
1841 fn debug_mutation_uses_validated_command_and_provenance() {
1842 let mut canwu = Canwu::demo(35).expect("demo should load");
1843 let ids = Canwu::demo_ids();
1844 let result = canwu.submit(CommandEnvelope::new(
1845 Issuer::Debug,
1846 Command::DebugSetArmyMorale {
1847 army: ids.army,
1848 morale: 37,
1849 },
1850 ));
1851 let receipt = result.expect("debug command should validate");
1852 assert_eq!(
1853 canwu.world().army(ids.army).expect("army exists").morale,
1854 37
1855 );
1856 let explanation = canwu.explain(&ExplanationRequest::Event(receipt.emitted_events[0]));
1857 assert!(explanation.causal_chain.len() >= 2);
1858 }
1859
1860 #[test]
1861 fn public_checkpoint_journal_round_trip_is_exact() {
1862 let mut canwu = Canwu::demo(35).expect("demo should load");
1863 let ids = Canwu::demo_ids();
1864 canwu
1865 .submit(CommandEnvelope::new(
1866 Issuer::Actor(ids.commander),
1867 Command::OrderMovement {
1868 subject: EntityRef::Army(ids.army),
1869 destination: ids.eastern_territory,
1870 cargo: Vec::new(),
1871 },
1872 ))
1873 .expect("movement should be accepted");
1874 canwu
1875 .advance(SimDuration::days(1))
1876 .expect("scheduled work should execute");
1877
1878 let checkpoint = canwu.checkpoint().expect("current state should checkpoint");
1879 assert!(checkpoint.state.events.is_empty());
1880 assert_eq!(
1881 checkpoint.journal_end,
1882 canwu
1883 .evidence_cursor()
1884 .expect("journal cursor should be representable")
1885 );
1886 let json = canwu
1887 .checkpoint_journal_json()
1888 .expect("checkpoint journal should serialize");
1889 let restored = Canwu::from_checkpoint_journal_json(&json)
1890 .expect("checkpoint journal should restore through the public API");
1891 assert_eq!(restored.snapshot(), canwu.snapshot());
1892
1893 canwu
1894 .settle_boundary(BoundaryRequest::at(canwu.time()))
1895 .expect("a public boundary should complete the live evidence tail");
1896 let expected = canwu.snapshot();
1897 let mut compact = canwu
1898 .into_compacted()
1899 .expect("the public API should enter compact mode");
1900 let segment = compact
1901 .seal_evidence()
1902 .expect("the public compact API should seal evidence")
1903 .expect("the public compact API should return a segment");
1904 let compact_checkpoint = compact
1905 .checkpoint()
1906 .expect("the public compact API should checkpoint");
1907 assert_eq!(
1908 compact
1909 .snapshot_with_segments(vec![segment.clone()])
1910 .expect("the public compact API should reconstruct its snapshot"),
1911 expected
1912 );
1913 let restored_compact =
1914 CompactedCanwu::from_checkpoint_and_journal(compact_checkpoint, vec![segment])
1915 .expect("the public compact API should restore from its archive");
1916 assert_eq!(
1917 restored_compact
1918 .snapshot_with_segments(Vec::new())
1919 .expect("the restored compact API should retain validated evidence"),
1920 expected
1921 );
1922 }
1923}