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, RouteId, SchemaRegistry, TerritoryId, TypeSchema, TypedDomainRecordRef,
13};
14pub use canwu_event::{CauseRef, EventAudience, EventKind, SimEvent};
15pub use canwu_knowledge::{
16 ActorKnowledge, ArmyKnowledge, EstimateRange, KnowledgeCursor, KnowledgeHistoryView,
17 KnowledgeOrigin, KnowledgeQuery, KnowledgeQueryError, KnowledgeQueryResult, KnowledgeReadCut,
18 KnowledgeRecord, KnowledgeRecordDraft, KnowledgeRecordView, KnowledgeSnapshot, KnowledgeSource,
19 KnowledgeSubject, KnowledgeSubjectTarget,
20};
21pub use canwu_routing::{
22 DepartureSlot, DurationSample, PlanningSnapshot, ROUTING_ALGORITHM_VERSION, RouteCost,
23 RouteLeg, RoutePlan, RoutingAlgorithm, RoutingCache, RoutingConnection, RoutingConnectionRef,
24 RoutingEndpoint, RoutingEndpointKind, RoutingError, RoutingNetwork, RoutingNodeRef,
25 RoutingPolicy, RoutingRequest, TransferMode, TraversalModel, plan_route,
26 planning_snapshot_from_world,
27};
28pub use canwu_sim::{
29 ADMISSION_CURSOR_FORMAT_VERSION, ArchiveProvider, ArchiveStore, ArchiveStoreOutcome,
30 ArchivedEvidenceLocator, ArchivedEvidenceReceipt, ArchivedSegmentHeader, ArtifactManifest,
31 BoundaryChange, BoundaryContext, BoundaryDirective, BoundaryEmission, BoundaryEmissionKind,
32 BoundaryIngressGeneration, BoundaryKnowledgeChange, BoundaryPhase, BoundaryProposal,
33 BoundaryReceipt, BoundaryRecord, BoundaryRequest, BoundarySystemContract,
34 BoundarySystemHandler, CHECKPOINT_JOURNAL_FORMAT_VERSION, COMMITMENT_FORMAT_VERSION,
35 CanwuError, CheckpointJournal, Command, CommandAttemptOutcome, CommandAttemptRecord,
36 CommandAuthority, CommandContext, CommandEnvelope, CommandIngress, CommandOutcome,
37 CommandPolicyContext, CommandReceipt, CommandRecord, CommandRejection, CommandRequest,
38 CommitmentRoots, CompactedSimulation, ControllerDecision, ControllerPolicy, DecisionAction,
39 DecisionAttemptErrorCode, DecisionAttemptOutcome, DecisionAttemptRecord, DecisionAuthority,
40 DecisionContext, DecisionController, DecisionControllerBinding, DecisionError,
41 DecisionErrorCode, DecisionEvaluation, DecisionExternalEvidence, DecisionFactorContribution,
42 DecisionIngressRequest, DecisionMutation, DecisionOption, DecisionOptionEvaluation,
43 DecisionOrigin, DecisionOutcome, DecisionPolicy, DecisionPolicyIdentity, DecisionPolicyKind,
44 DecisionRule, DecisionState, DecisionTicket, DecisionTicketDraft, DecisionTicketState,
45 DecisionTrace, DemoIds, DomainRecord, DomainRecordChange, DomainRecordClass, DomainRecordDraft,
46 DomainRecordLifecycle, DomainRecordMutation, DomainRecordMutationPolicy, DomainRecordOperation,
47 DomainRecordSchema, DomainReference, DomainReferenceSchema, DomainReferenceTarget,
48 DomainReferenceTargetKind, ENGINE_VERSION, ErrorCode, EvidenceArchiveIndex, EvidenceCursor,
49 EvidenceIndexEntry, EvidenceItemLocator, EvidenceJournalKind, EvidenceJournalRoots,
50 EvidenceJournalSegment, EvidenceNestedLocator, EvidenceSealToken, ExternalDecisionOption,
51 ExternalDecisionRequest, ExternalDecisionResponse, ExternalPolicy, HumanDecisionResponse,
52 HumanPolicy, IngressClass, IngressPayload, IngressReceipt, IngressRecord, InteractionPolicy,
53 Issuer, KnowledgeLimitsV1, KnowledgeSubjectSchema, KnowledgeSubjectTargetKind,
54 KnowledgeWriteGrant, LlmModelIdentity, LlmPolicy, ObservationPolicy, OrderedRulePolicy,
55 PAYLOAD_REQUIRED_EVIDENCE_CONTINUATION_FIELD,
56 PAYLOAD_REQUIRED_EVIDENCE_CONTINUATION_FORMAT_VERSION, PayloadProperty,
57 PayloadRequiredEvidenceContinuationV1, PayloadSchema, PayloadValueType, PluginActionDescriptor,
58 PluginCommandHandler, PluginComponentRecord, PluginDescriptor, PluginIngressDescriptor,
59 PluginIngressRequest, PluginIngressTarget, PluginKnowledgeSchema, PluginRegistrar,
60 PluginRegistry, PolicyDecision, PreparedDecisionIngress, PreparedEvidenceSeal,
61 QueuedExternalPolicy, QueuedHumanPolicy, QueuedLlmPolicy, RUN_CONFIGURATION_FORMAT_VERSION,
62 RUN_MANIFEST_FORMAT_VERSION, RandomAlgorithm, RandomDrawAddress, RandomDrawOutcome,
63 RandomDrawProducer, RandomDrawRecord, RandomOperationAddressV1, RandomOperationTarget,
64 RandomStreamKey, RandomStreamState, ReplayJournal, ReservationAllocation,
65 ReservationDisposition, ReservationOffer, ReservationOfferRecord, ReservationPoolKey,
66 ReservationRef, ReservationRequest, ReservationRequestRecord, RuleChoice, RulePolicy,
67 RunConfiguration, RunConfigurationSnapshot, RunManifest, RunPurpose, SNAPSHOT_FORMAT_VERSION,
68 STATE_REVISION_FORMAT_VERSION, Scenario, SeatBinding, SeatPolicy, SimulationCheckpoint,
69 SimulationPlugin, SimulationSnapshot, SimulationSystemHandler, SimulationView, StateKey,
70 StateVisibility, SystemCadence, SystemContract, SystemDirective, TracePolicy, UtilityEvaluator,
71 UtilityPolicy, UtilityProfile, WeightedUtilityEvaluator, WeightedUtilityPolicy,
72 canonical_byte_hash, canonical_hash, payload_required_evidence_continuation_property_v1,
73};
74pub use canwu_time::{SimDuration, SimTime};
75pub use canwu_transport::{
76 CapacityBooking, CapacityBookingId, CapacityBookingStatus, DeliveryCompletionRequest,
77 DeliverySaga, Handoff, HandoffId, ItineraryRevision, ItineraryRevisionId,
78 ItineraryRevisionReason, LegExecution, LegExecutionId, LegExecutionStatus, MovementInitiative,
79 MovementOrder, MovementOrderError, MovementOrderId, MovementSubject, MovementSubjectRole,
80 SagaState, TRANSPORT_SEMANTIC_VERSION, TransportError, TransportExecution,
81 TransportExecutionId, TransportExecutionState, delivery_completion_operation_key,
82};
83pub use canwu_world::{
84 Army, Government, LetterCargo, LetterStatus, MapPoint, Person, PersonTransitState, Route,
85 Territory, TransitState, WorldDiff, WorldSnapshot,
86};
87
88use canwu_sim::Simulation;
89use serde::{Deserialize, Serialize};
90use serde_json::{Value, json};
91use std::collections::BTreeMap;
92
93pub struct Canwu {
95 simulation: Simulation,
96}
97
98pub struct CompactedCanwu {
100 simulation: CompactedSimulation,
101}
102
103impl Canwu {
104 #[must_use]
105 pub const fn version() -> &'static str {
106 ENGINE_VERSION
107 }
108
109 pub fn new(seed: u64, scenario: Scenario) -> Result<Self, CanwuError> {
110 Ok(Self {
111 simulation: Simulation::new(seed, scenario)?,
112 })
113 }
114
115 pub fn into_compacted(self) -> Result<CompactedCanwu, CanwuError> {
117 Ok(CompactedCanwu {
118 simulation: self.simulation.into_compacted()?,
119 })
120 }
121
122 pub fn new_with_plugins(
123 seed: u64,
124 scenario: Scenario,
125 plugins: &[&dyn SimulationPlugin],
126 ) -> Result<Self, CanwuError> {
127 Ok(Self {
128 simulation: Simulation::new_with_plugins(seed, scenario, plugins)?,
129 })
130 }
131
132 pub fn new_with_manifest(
133 seed: u64,
134 scenario: Scenario,
135 run_manifest: RunManifest,
136 ) -> Result<Self, CanwuError> {
137 Ok(Self {
138 simulation: Simulation::new_with_manifest(seed, scenario, run_manifest)?,
139 })
140 }
141
142 pub fn new_with_manifest_and_plugins(
143 seed: u64,
144 scenario: Scenario,
145 run_manifest: RunManifest,
146 plugins: &[&dyn SimulationPlugin],
147 ) -> Result<Self, CanwuError> {
148 Ok(Self {
149 simulation: Simulation::new_with_manifest_and_plugins(
150 seed,
151 scenario,
152 run_manifest,
153 plugins,
154 )?,
155 })
156 }
157
158 pub fn new_with_run_configuration(
159 seed: u64,
160 scenario: Scenario,
161 run_manifest: RunManifest,
162 run_configuration: RunConfiguration,
163 ) -> Result<Self, CanwuError> {
164 Ok(Self {
165 simulation: Simulation::new_with_run_configuration(
166 seed,
167 scenario,
168 run_manifest,
169 run_configuration,
170 )?,
171 })
172 }
173
174 pub fn new_with_run_configuration_and_plugins(
175 seed: u64,
176 scenario: Scenario,
177 run_manifest: RunManifest,
178 run_configuration: RunConfiguration,
179 plugins: &[&dyn SimulationPlugin],
180 ) -> Result<Self, CanwuError> {
181 Ok(Self {
182 simulation: Simulation::new_with_run_configuration_and_plugins(
183 seed,
184 scenario,
185 run_manifest,
186 run_configuration,
187 plugins,
188 )?,
189 })
190 }
191
192 pub fn demo(seed: u64) -> Result<Self, CanwuError> {
193 let (simulation, _) = Simulation::demo(seed)?;
194 Ok(Self { simulation })
195 }
196
197 #[must_use]
198 pub fn demo_ids() -> DemoIds {
199 let (_, ids) = canwu_sim::demo_scenario();
200 ids
201 }
202
203 #[must_use]
204 pub const fn time(&self) -> SimTime {
205 self.simulation.time()
206 }
207
208 #[must_use]
209 pub const fn run_manifest(&self) -> &RunManifest {
210 self.simulation.run_manifest()
211 }
212
213 #[must_use]
214 pub const fn run_configuration(&self) -> &RunConfigurationSnapshot {
215 self.simulation.run_configuration()
216 }
217
218 #[must_use]
219 pub fn revision(&self) -> u64 {
226 self.simulation.revision()
227 }
228
229 #[must_use]
230 pub fn run_manifest_hash(&self) -> &str {
231 self.simulation.run_manifest_hash()
232 }
233
234 #[must_use]
235 pub fn checkpoint_hash(&self) -> &str {
236 self.simulation.checkpoint_hash()
237 }
238
239 pub fn authoritative_state_hash(&self) -> Result<String, CanwuError> {
240 self.simulation.authoritative_state_hash()
241 }
242
243 #[must_use]
244 pub fn world(&self) -> WorldSnapshot {
245 self.simulation.world()
246 }
247
248 #[must_use]
253 pub fn knowledge(&self) -> &KnowledgeSnapshot {
254 self.simulation.knowledge()
255 }
256
257 #[must_use]
258 pub fn events(&self) -> &[SimEvent] {
259 self.simulation.events()
260 }
261
262 #[must_use]
263 pub fn commands(&self) -> &[CommandRecord] {
264 self.simulation.command_log()
265 }
266
267 #[must_use]
268 pub fn boundaries(&self) -> &[BoundaryRecord] {
269 self.simulation.boundaries()
270 }
271
272 #[must_use]
273 pub fn command_attempts(&self) -> &[CommandAttemptRecord] {
274 self.simulation.command_attempts()
275 }
276
277 #[must_use]
278 pub fn ingress_log(&self) -> &[IngressRecord] {
279 self.simulation.ingress_log()
280 }
281
282 #[must_use]
283 pub fn domain_record(&self, reference: &DomainRecordRef) -> Option<&DomainRecord> {
284 self.simulation.domain_record(reference)
285 }
286
287 #[must_use]
288 pub fn typed_domain_record<T: DomainRecordType>(
289 &self,
290 reference: &TypedDomainRecordRef<T>,
291 ) -> Option<&DomainRecord> {
292 self.simulation.typed_domain_record(reference)
293 }
294
295 pub fn domain_records(&self) -> impl Iterator<Item = &DomainRecord> {
296 self.simulation.domain_records()
297 }
298
299 #[must_use]
300 pub const fn decision_state(&self) -> &DecisionState {
301 self.simulation.decision_state()
302 }
303
304 #[must_use]
305 pub fn decision_ticket(&self, id: DecisionTicketId) -> Option<&DecisionTicket> {
306 self.simulation.decision_ticket(id)
307 }
308
309 #[must_use]
310 pub fn decision_traces(&self) -> &[DecisionTrace] {
311 self.simulation.decision_traces()
312 }
313
314 #[must_use]
315 pub fn decision_attempts(&self) -> &[DecisionAttemptRecord] {
316 self.simulation.decision_attempts()
317 }
318
319 #[must_use]
320 pub fn random_draws(&self) -> &[RandomDrawRecord] {
321 self.simulation.random_draws()
322 }
323
324 #[must_use]
325 pub fn boundary_head_hash(&self) -> Option<&str> {
326 self.simulation.boundary_head_hash()
327 }
328
329 #[must_use]
330 pub const fn schema(&self) -> &SchemaRegistry {
331 self.simulation.schema()
332 }
333
334 pub fn plugin_descriptors(&self) -> impl Iterator<Item = &PluginDescriptor> {
335 self.simulation.plugin_descriptors()
336 }
337
338 #[must_use]
339 pub fn replay_journal(&self) -> ReplayJournal {
340 self.simulation.replay_journal()
341 }
342
343 pub fn evidence_cursor(&self) -> Result<EvidenceCursor, CanwuError> {
344 self.simulation.evidence_cursor()
345 }
346
347 pub fn checkpoint(&self) -> Result<SimulationCheckpoint, CanwuError> {
348 self.simulation.checkpoint()
349 }
350
351 pub fn journal_segment_since(
352 &self,
353 start: EvidenceCursor,
354 ) -> Result<EvidenceJournalSegment, CanwuError> {
355 self.simulation.journal_segment_since(start)
356 }
357
358 pub fn checkpoint_journal(&self) -> Result<CheckpointJournal, CanwuError> {
359 self.simulation.checkpoint_journal()
360 }
361
362 pub fn checkpoint_journal_json(&self) -> Result<String, CanwuError> {
363 self.simulation.checkpoint_journal_json()
364 }
365
366 pub fn register_plugin<P: SimulationPlugin + ?Sized>(
367 &mut self,
368 plugin: &P,
369 ) -> Result<(), CanwuError> {
370 self.simulation.register_plugin(plugin)
371 }
372
373 pub fn submit(&mut self, command: CommandEnvelope) -> Result<CommandReceipt, CanwuError> {
374 self.simulation.submit(command)
375 }
376
377 pub fn process_command(
378 &mut self,
379 request: CommandRequest,
380 ) -> Result<CommandOutcome, CanwuError> {
381 self.simulation.process_command(request)
382 }
383
384 pub fn enqueue_command(
385 &mut self,
386 due_at: SimTime,
387 priority: i32,
388 request: CommandRequest,
389 ) -> Result<IngressReceipt, CanwuError> {
390 self.simulation.enqueue_command(due_at, priority, request)
391 }
392
393 pub fn enqueue_plugin_ingress(
394 &mut self,
395 request: PluginIngressRequest,
396 ) -> Result<IngressReceipt, CanwuError> {
397 self.simulation.enqueue_plugin_ingress(request)
398 }
399
400 pub fn prepare_decision(
401 &self,
402 decision_request_id: DecisionRequestId,
403 command_request_id: Option<CommandRequestId>,
404 ticket_id: DecisionTicketId,
405 policy: &dyn DecisionPolicy,
406 ) -> Result<DecisionEvaluation, CanwuError> {
407 self.simulation
408 .prepare_decision(decision_request_id, command_request_id, ticket_id, policy)
409 }
410
411 pub fn prepare_decision_at(
412 &self,
413 due_at: SimTime,
414 decision_request_id: DecisionRequestId,
415 command_request_id: Option<CommandRequestId>,
416 ticket_id: DecisionTicketId,
417 policy: &dyn DecisionPolicy,
418 ) -> Result<DecisionEvaluation, CanwuError> {
419 self.simulation.prepare_decision_at(
420 due_at,
421 decision_request_id,
422 command_request_id,
423 ticket_id,
424 policy,
425 )
426 }
427
428 pub fn enqueue_decision(
429 &mut self,
430 due_at: SimTime,
431 priority: i32,
432 request: DecisionIngressRequest,
433 ) -> Result<IngressReceipt, CanwuError> {
434 self.simulation.enqueue_decision(due_at, priority, request)
435 }
436
437 pub fn drive_decision(
438 &mut self,
439 due_at: SimTime,
440 priority: i32,
441 decision_request_id: DecisionRequestId,
442 command_request_id: Option<CommandRequestId>,
443 ticket_id: DecisionTicketId,
444 policy: &dyn DecisionPolicy,
445 ) -> Result<DecisionEvaluation, CanwuError> {
446 self.simulation.drive_decision(
447 due_at,
448 priority,
449 decision_request_id,
450 command_request_id,
451 ticket_id,
452 policy,
453 )
454 }
455
456 pub fn schedule_calendar_boundary(
457 &mut self,
458 due_at: SimTime,
459 cadences: Vec<SystemCadence>,
460 ) -> Result<IngressReceipt, CanwuError> {
461 self.simulation.schedule_calendar_boundary(due_at, cadences)
462 }
463
464 pub fn advance_canonical(
465 &mut self,
466 duration: SimDuration,
467 ) -> Result<Vec<BoundaryReceipt>, CanwuError> {
468 self.simulation.advance_canonical(duration)
469 }
470
471 pub fn step_canonical(&mut self) -> Result<Option<BoundaryReceipt>, CanwuError> {
472 self.simulation.step_canonical()
473 }
474
475 pub fn advance(&mut self, duration: SimDuration) -> Result<Vec<SimEvent>, CanwuError> {
476 self.simulation.advance(duration)
477 }
478
479 pub fn settle_boundary(
480 &mut self,
481 request: BoundaryRequest,
482 ) -> Result<BoundaryReceipt, CanwuError> {
483 self.simulation.settle_boundary(request)
484 }
485
486 pub fn wait(&mut self, duration: SimDuration) -> Result<Vec<SimEvent>, CanwuError> {
487 self.advance(duration)
488 }
489
490 pub fn step(&mut self) -> Result<Vec<SimEvent>, CanwuError> {
491 self.simulation.step()
492 }
493
494 #[must_use]
495 pub fn snapshot(&self) -> SimulationSnapshot {
496 self.simulation.snapshot()
497 }
498
499 pub fn snapshot_json(&self) -> Result<String, CanwuError> {
500 self.simulation.snapshot_json()
501 }
502
503 pub fn from_snapshot_json(json: &str) -> Result<Self, CanwuError> {
504 Ok(Self {
505 simulation: Simulation::from_snapshot_json(json)?,
506 })
507 }
508
509 pub fn from_snapshot_json_with_plugins(
510 json: &str,
511 plugins: &[&dyn SimulationPlugin],
512 ) -> Result<Self, CanwuError> {
513 Ok(Self {
514 simulation: Simulation::from_snapshot_json_with_plugins(json, plugins)?,
515 })
516 }
517
518 pub fn from_checkpoint_and_journal(
519 checkpoint: SimulationCheckpoint,
520 segments: Vec<EvidenceJournalSegment>,
521 ) -> Result<Self, CanwuError> {
522 Ok(Self {
523 simulation: Simulation::from_checkpoint_and_journal(checkpoint, segments)?,
524 })
525 }
526
527 pub fn from_checkpoint_journal(bundle: CheckpointJournal) -> Result<Self, CanwuError> {
528 Ok(Self {
529 simulation: Simulation::from_checkpoint_journal(bundle)?,
530 })
531 }
532
533 pub fn from_checkpoint_journal_with_plugins(
534 bundle: CheckpointJournal,
535 plugins: &[&dyn SimulationPlugin],
536 ) -> Result<Self, CanwuError> {
537 Ok(Self {
538 simulation: Simulation::from_checkpoint_journal_with_plugins(bundle, plugins)?,
539 })
540 }
541
542 pub fn from_checkpoint_journal_json(json: &str) -> Result<Self, CanwuError> {
543 Ok(Self {
544 simulation: Simulation::from_checkpoint_journal_json(json)?,
545 })
546 }
547
548 pub fn from_checkpoint_journal_json_with_plugins(
549 json: &str,
550 plugins: &[&dyn SimulationPlugin],
551 ) -> Result<Self, CanwuError> {
552 Ok(Self {
553 simulation: Simulation::from_checkpoint_journal_json_with_plugins(json, plugins)?,
554 })
555 }
556
557 pub fn replay(
558 seed: u64,
559 scenario: Scenario,
560 commands: &[CommandRecord],
561 final_time: SimTime,
562 ) -> Result<Self, CanwuError> {
563 Ok(Self {
564 simulation: Simulation::replay(seed, scenario, commands, final_time)?,
565 })
566 }
567
568 pub fn replay_with_plugins(
569 seed: u64,
570 scenario: Scenario,
571 plugins: &[&dyn SimulationPlugin],
572 commands: &[CommandRecord],
573 final_time: SimTime,
574 ) -> Result<Self, CanwuError> {
575 Ok(Self {
576 simulation: Simulation::replay_with_plugins(
577 seed, scenario, plugins, commands, final_time,
578 )?,
579 })
580 }
581
582 pub fn replay_with_boundaries(
583 seed: u64,
584 scenario: Scenario,
585 plugins: &[&dyn SimulationPlugin],
586 commands: &[CommandRecord],
587 boundaries: &[BoundaryRecord],
588 final_time: SimTime,
589 ) -> Result<Self, CanwuError> {
590 Ok(Self {
591 simulation: Simulation::replay_with_boundaries(
592 seed, scenario, plugins, commands, boundaries, final_time,
593 )?,
594 })
595 }
596
597 pub fn replay_with_run_manifest(
598 seed: u64,
599 scenario: Scenario,
600 run_manifest: RunManifest,
601 plugins: &[&dyn SimulationPlugin],
602 commands: &[CommandRecord],
603 boundaries: &[BoundaryRecord],
604 final_time: SimTime,
605 ) -> Result<Self, CanwuError> {
606 Ok(Self {
607 simulation: Simulation::replay_with_run_manifest(
608 seed,
609 scenario,
610 run_manifest,
611 plugins,
612 commands,
613 boundaries,
614 final_time,
615 )?,
616 })
617 }
618
619 #[allow(clippy::too_many_arguments)]
620 pub fn replay_with_run_configuration(
621 seed: u64,
622 scenario: Scenario,
623 run_manifest: RunManifest,
624 run_configuration: RunConfiguration,
625 plugins: &[&dyn SimulationPlugin],
626 commands: &[CommandRecord],
627 command_attempts: &[CommandAttemptRecord],
628 boundaries: &[BoundaryRecord],
629 final_time: SimTime,
630 ) -> Result<Self, CanwuError> {
631 Ok(Self {
632 simulation: Simulation::replay_with_run_configuration(
633 seed,
634 scenario,
635 run_manifest,
636 run_configuration,
637 plugins,
638 commands,
639 command_attempts,
640 boundaries,
641 final_time,
642 )?,
643 })
644 }
645
646 pub fn replay_from_journal(
647 scenario: Scenario,
648 plugins: &[&dyn SimulationPlugin],
649 journal: &ReplayJournal,
650 ) -> Result<Self, CanwuError> {
651 Ok(Self {
652 simulation: Simulation::replay_from_journal(scenario, plugins, journal)?,
653 })
654 }
655
656 #[must_use]
657 pub fn fork(&self) -> Self {
658 Self {
659 simulation: self.simulation.fork(),
660 }
661 }
662
663 #[must_use]
664 pub fn diff(&self, other: &Self) -> WorldDiff {
665 WorldDiff::between(&self.world(), &other.world())
666 }
667
668 #[must_use]
669 pub fn query(&self, query: &Query) -> QueryResult {
670 run_query(&self.world(), self.events(), query)
671 }
672
673 pub fn query_as(&self, actor: PersonId, query: &Query) -> Result<QueryResult, CanwuError> {
674 if self.world().person(actor).is_none() {
675 return Err(CanwuError::new(
676 ErrorCode::ActorNotFound,
677 format!("actor {actor} was not found"),
678 ));
679 }
680 Ok(run_actor_query(
681 &self.world(),
682 actor,
683 self.knowledge().for_actor(actor),
684 query,
685 ))
686 }
687
688 pub fn admin_query_knowledge(
691 &self,
692 holder: KnowledgeHolderRef,
693 query: &KnowledgeQuery,
694 ) -> Result<KnowledgeQueryResult, CanwuError> {
695 self.simulation
696 .knowledge()
697 .query_current(
698 holder,
699 query,
700 self.simulation.boundaries().last().map(|value| value.id),
701 )
702 .map_err(map_knowledge_query_error)
703 }
704
705 pub fn viewer(&self) -> Result<CanwuViewer<'_>, CanwuError> {
708 let principal = self.declared_observation_principal()?;
709 Ok(CanwuViewer {
710 canwu: self,
711 context: KnowledgeViewContext { principal },
712 })
713 }
714
715 pub fn viewer_for_actor(&self, actor: PersonId) -> Result<CanwuViewer<'_>, CanwuError> {
718 if self.world().person(actor).is_none() {
719 return Err(CanwuError::new(
720 ErrorCode::ActorNotFound,
721 format!("actor {actor} was not found"),
722 ));
723 }
724 let principal = match self.run_configuration().declared() {
725 Some(configuration)
726 if configuration.observation == ObservationPolicy::ActorBound
727 && configuration.seat == SeatPolicy::CharacterBound
728 && configuration
729 .seat_binding
730 .as_ref()
731 .and_then(|binding| binding.actor)
732 == Some(actor) =>
733 {
734 ObservationPrincipal::Person(actor)
735 }
736 Some(_) => {
737 return Err(CanwuError::new(
738 ErrorCode::InvalidAuthority,
739 "the persisted run policy does not authorize a character viewer",
740 ));
741 }
742 None => ObservationPrincipal::Person(actor),
743 };
744 Ok(CanwuViewer {
745 canwu: self,
746 context: KnowledgeViewContext { principal },
747 })
748 }
749
750 fn declared_observation_principal(&self) -> Result<ObservationPrincipal, CanwuError> {
751 let Some(configuration) = self.run_configuration().declared() else {
752 return Err(CanwuError::new(
753 ErrorCode::InvalidAuthority,
754 "legacy runs require viewer_for_actor with an existing character",
755 ));
756 };
757 match configuration.observation {
758 ObservationPolicy::ActorBound => match configuration.seat {
759 SeatPolicy::CharacterBound => configuration
760 .seat_binding
761 .as_ref()
762 .and_then(|binding| binding.actor)
763 .map(ObservationPrincipal::Person)
764 .ok_or_else(|| {
765 CanwuError::new(
766 ErrorCode::InvalidAuthority,
767 "character-bound observation lacks an actor binding",
768 )
769 }),
770 SeatPolicy::InstitutionBound => configuration
771 .seat_binding
772 .as_ref()
773 .and_then(|binding| binding.institution.clone())
774 .map(ObservationPrincipal::Institution)
775 .ok_or_else(|| {
776 CanwuError::new(
777 ErrorCode::InvalidAuthority,
778 "institution-bound observation lacks an institution binding",
779 )
780 }),
781 SeatPolicy::ObserverSeat | SeatPolicy::AdvisorSeat | SeatPolicy::None => {
782 Err(CanwuError::new(
783 ErrorCode::InvalidAuthority,
784 "actor-bound observation requires a character or institution seat",
785 ))
786 }
787 },
788 ObservationPolicy::PublicObserver => Ok(ObservationPrincipal::Public),
789 ObservationPolicy::ResearchFull => Ok(ObservationPrincipal::Research),
790 ObservationPolicy::DeveloperDiagnostic => Ok(ObservationPrincipal::Developer),
791 }
792 }
793
794 pub fn viewer_context(&self, actor: PersonId) -> Result<ViewerContext, CanwuError> {
800 let viewer = self.viewer_for_actor(actor)?;
801 Ok(ViewerContext {
802 principal: viewer.context.principal.clone(),
803 observation: ObservationPolicy::ActorBound,
804 checkpoint_hash: self.checkpoint_hash().to_owned(),
805 })
806 }
807
808 pub fn observe(
809 &self,
810 actor: PersonId,
811 request: &ObserveRequest,
812 ) -> Result<AgentContext, CanwuError> {
813 let viewer = self.viewer_context(actor)?;
814 self.observe_with_viewer(&viewer, request)
815 }
816
817 pub fn observe_with_viewer(
823 &self,
824 viewer: &ViewerContext,
825 request: &ObserveRequest,
826 ) -> Result<AgentContext, CanwuError> {
827 let Some(actor) = viewer.principal.person() else {
828 return Err(CanwuError::new(
829 ErrorCode::InvalidAuthority,
830 "character observation requires a person principal",
831 ));
832 };
833 let authorized = self.viewer_context(actor)?;
834 if authorized != *viewer {
835 return Err(CanwuError::new(
836 ErrorCode::InvalidAuthority,
837 format!("actor {actor} is not authorized for this observation context"),
838 ));
839 }
840 let world = self.world();
841 let person = world.person(actor).ok_or_else(|| {
842 CanwuError::new(
843 ErrorCode::ActorNotFound,
844 format!("actor {actor} was not found"),
845 )
846 })?;
847 let knowledge = self.knowledge().for_actor(actor);
848 let known_armies = match knowledge {
849 Some(records) => records
850 .armies
851 .values()
852 .map(|record| known_army_view(self.time(), record))
853 .collect::<Result<Vec<_>, _>>()?,
854 None => Vec::new(),
855 };
856 let changes_since = request.since.map_or_else(Vec::new, |since| {
857 self.events()
858 .iter()
859 .filter(|event| event.timestamp > since)
860 .filter_map(|event| {
861 let audience = self.simulation.event_audience(event);
862 visible_change(viewer, event, &audience)
863 })
864 .collect()
865 });
866 let pending_actions = world
867 .armies
868 .iter()
869 .filter(|army| army.commander == actor)
870 .filter_map(|army| {
871 army.transit.as_ref().map(|transit| PendingCommitment {
872 summary: format!(
873 "{} is moving from {} to {}",
874 army.name, transit.from, transit.to
875 ),
876 due_at: transit.arrives_at,
877 })
878 })
879 .chain(
880 world
881 .people
882 .iter()
883 .filter(|person| person.id == actor)
884 .filter_map(|person| {
885 person.transit.as_ref().map(|transit| PendingCommitment {
886 summary: format!(
887 "{} is traveling from {} to {}",
888 person.name, transit.from, transit.to
889 ),
890 due_at: transit.arrives_at,
891 })
892 }),
893 )
894 .collect();
895 Ok(AgentContext {
896 identity: AgentIdentity {
897 person: person.id,
898 name: person.name.clone(),
899 roles: person.roles.clone(),
900 },
901 current_time: self.time(),
902 current_location: person.current_location,
903 focus: request.focus.clone(),
904 known_armies,
905 changes_since,
906 pending_actions,
907 available_actions: self.available_actions(actor)?,
908 })
909 }
910
911 pub fn inspect(
912 &self,
913 actor: PersonId,
914 entity: &EntityRef,
915 detail: DetailLevel,
916 ) -> Result<Inspection, CanwuError> {
917 let world = self.world();
918 let actor_state = world.person(actor).ok_or_else(|| {
919 CanwuError::new(
920 ErrorCode::ActorNotFound,
921 format!("actor {actor} was not found"),
922 )
923 })?;
924 let fields = match entity {
925 EntityRef::Army(army_id) => {
926 let record = self
927 .knowledge()
928 .for_actor(actor)
929 .and_then(|knowledge| knowledge.armies.get(army_id));
930 let Some(record) = record else {
931 return Ok(Inspection {
932 entity: entity.clone(),
933 detail,
934 summary: "No reliable information is available about this army".to_owned(),
935 fields: BTreeMap::new(),
936 });
937 };
938 let mut fields = BTreeMap::from([
939 ("known_name".to_owned(), json!(record.known_name)),
940 ("known_location".to_owned(), json!(record.known_location)),
941 (
942 "estimated_strength".to_owned(),
943 json!(record.estimated_strength),
944 ),
945 ("observed_at".to_owned(), json!(record.observed_at)),
946 (
947 "confidence_per_mille".to_owned(),
948 json!(record.confidence_per_mille),
949 ),
950 ]);
951 if matches!(detail, DetailLevel::RawFields) {
952 fields.insert("source".to_owned(), json!(record.source));
953 fields.insert("learned_at".to_owned(), json!(record.learned_at));
954 }
955 fields
956 }
957 EntityRef::Person(person_id) => {
958 if *person_id != actor_state.id {
959 return Ok(no_knowledge_inspection(entity, detail));
960 }
961 let Some(person) = world.person(*person_id) else {
962 return Ok(missing_inspection(entity, detail));
963 };
964 BTreeMap::from([
965 ("name".to_owned(), json!(person.name)),
966 ("roles".to_owned(), json!(person.roles)),
967 ("government".to_owned(), json!(person.government)),
968 (
969 "current_location".to_owned(),
970 json!(person.current_location),
971 ),
972 ("transit".to_owned(), json!(person.transit)),
973 ])
974 }
975 EntityRef::Territory(_)
976 | EntityRef::Domain(_)
977 | EntityRef::Government(_)
978 | EntityRef::Route(_)
979 | EntityRef::Organization(_) => return Ok(no_knowledge_inspection(entity, detail)),
980 EntityRef::Resource(resource_id) => {
981 let letter_id = LetterId::new(resource_id.get());
982 let Some(letter) = world.letter(letter_id) else {
983 return Ok(missing_inspection(entity, detail));
984 };
985 let entitled = letter.sender == actor_state.id
986 || letter.recipient == actor_state.id
987 || letter.carrier == Some(actor_state.id);
988 if !entitled {
989 return Ok(no_knowledge_inspection(entity, detail));
990 }
991 let mut fields = BTreeMap::from([
992 ("sender".to_owned(), json!(letter.sender)),
993 ("recipient".to_owned(), json!(letter.recipient)),
994 ("status".to_owned(), json!(letter.status)),
995 ("carrier".to_owned(), json!(letter.carrier)),
996 ("location".to_owned(), json!(letter.location)),
997 ("delivered_at".to_owned(), json!(letter.delivered_at)),
998 ]);
999 if matches!(detail, DetailLevel::Entity | DetailLevel::RawFields) && entitled {
1000 fields.insert("body".to_owned(), json!(letter.body));
1001 }
1002 fields
1003 }
1004 };
1005 Ok(Inspection {
1006 entity: entity.clone(),
1007 detail,
1008 summary: format!("Actor-relative inspection of {entity}"),
1009 fields,
1010 })
1011 }
1012
1013 pub fn available_actions(&self, actor: PersonId) -> Result<Vec<AvailableAction>, CanwuError> {
1014 let world = self.world();
1015 if world.person(actor).is_none() {
1016 return Err(CanwuError::new(
1017 ErrorCode::ActorNotFound,
1018 format!("actor {actor} was not found"),
1019 ));
1020 }
1021 let mut actions = Vec::new();
1022 if let Some(person) = world.person(actor)
1023 && person.transit.is_none()
1024 {
1025 let cargo: Vec<_> = world
1026 .letters
1027 .iter()
1028 .filter(|letter| {
1029 letter.status == LetterStatus::HeldByPerson && letter.carrier == Some(actor)
1030 })
1031 .map(|letter| letter.id)
1032 .collect();
1033 for route in &world.routes {
1034 if let Some(destination) = route.other_end(person.current_location) {
1035 actions.push(AvailableAction {
1036 action_type: "self_move".to_owned(),
1037 description: format!("Travel to territory {destination}"),
1038 payload: json!({
1039 "subject": EntityRef::Person(actor),
1040 "destination": destination,
1041 "cargo": cargo,
1042 }),
1043 legal_reason: format!("Actor {actor} may move themself"),
1044 });
1045 }
1046 }
1047 }
1048 for army in world.armies.iter().filter(|army| army.commander == actor) {
1049 if army.transit.is_some() {
1050 continue;
1051 }
1052 for route in &world.routes {
1053 if let Some(destination) = route.other_end(army.location) {
1054 actions.push(AvailableAction {
1055 action_type: "move_entity".to_owned(),
1056 description: format!("Move {} to territory {destination}", army.name),
1057 payload: json!({
1058 "subject": EntityRef::Army(army.id),
1059 "destination": destination,
1060 "cargo": Vec::<LetterId>::new(),
1061 }),
1062 legal_reason: format!("Actor {actor} commands army {}", army.id),
1063 });
1064 }
1065 }
1066 }
1067 Ok(actions)
1068 }
1069
1070 pub fn act(
1071 &mut self,
1072 actor: PersonId,
1073 action: SemanticAction,
1074 ) -> Result<CommandReceipt, CanwuError> {
1075 let command = match action {
1076 SemanticAction::SelfMove { destination, cargo } => Command::OrderMovement {
1077 subject: EntityRef::Person(actor),
1078 destination,
1079 cargo,
1080 },
1081 SemanticAction::MoveEntity {
1082 subject,
1083 destination,
1084 cargo,
1085 } => Command::OrderMovement {
1086 subject,
1087 destination,
1088 cargo,
1089 },
1090 SemanticAction::Plugin {
1091 plugin,
1092 action,
1093 payload,
1094 } => Command::Plugin {
1095 plugin,
1096 command: action,
1097 payload,
1098 },
1099 };
1100 self.submit(CommandEnvelope::new(Issuer::Actor(actor), command))
1101 }
1102
1103 #[must_use]
1104 pub fn explain(&self, request: &ExplanationRequest) -> Explanation {
1105 match request {
1106 ExplanationRequest::Event(event_id) => self.explain_event(*event_id),
1107 ExplanationRequest::ArmyMorale(army_id) => self.explain_army_morale(*army_id),
1108 ExplanationRequest::Failure(error) => Explanation {
1109 summary: error.message.clone(),
1110 causal_chain: vec![ExplanationStep {
1111 label: format!("Validation failed: {:?}", error.code),
1112 event: None,
1113 }],
1114 },
1115 }
1116 }
1117
1118 #[must_use]
1119 pub fn describe_capabilities(&self) -> CapabilityDescription {
1120 CapabilityDescription {
1121 operations: vec![
1122 "observe",
1123 "inspect",
1124 "query",
1125 "available_actions",
1126 "act",
1127 "explain",
1128 "wait",
1129 "describe_capabilities",
1130 ]
1131 .into_iter()
1132 .map(str::to_owned)
1133 .collect(),
1134 notes: vec![
1135 "Agent reads are actor-relative and never fall back to ground truth".to_owned(),
1136 "All actions become validated commands".to_owned(),
1137 "Use progressive inspection detail to control response size".to_owned(),
1138 ],
1139 plugin_actions: self
1140 .plugin_descriptors()
1141 .flat_map(|plugin| {
1142 plugin
1143 .commands
1144 .iter()
1145 .map(move |action| format!("{}.{}", plugin.name, action.name))
1146 })
1147 .collect(),
1148 }
1149 }
1150
1151 fn explain_event(&self, event_id: EventId) -> Explanation {
1152 let mut chain = Vec::new();
1153 let events = self.events();
1154 let mut current = event_by_id(events, event_id);
1155 while let Some(event) = current {
1156 chain.push(ExplanationStep {
1157 label: event.summary.clone(),
1158 event: Some(event.id),
1159 });
1160 current = match &event.cause {
1161 Some(CauseRef::Boundary(boundary)) => {
1162 chain.push(ExplanationStep {
1163 label: format!("Committed by boundary {boundary}"),
1164 event: None,
1165 });
1166 None
1167 }
1168 Some(CauseRef::Event(parent)) => event_by_id(events, *parent),
1169 Some(CauseRef::Command(command)) => {
1170 chain.push(ExplanationStep {
1171 label: format!("Accepted command {command}"),
1172 event: None,
1173 });
1174 None
1175 }
1176 Some(CauseRef::System(system)) => {
1177 chain.push(ExplanationStep {
1178 label: format!("Produced by system {system}"),
1179 event: None,
1180 });
1181 None
1182 }
1183 None => None,
1184 };
1185 }
1186 Explanation {
1187 summary: chain.first().map_or_else(
1188 || "Event was not found".to_owned(),
1189 |step| step.label.clone(),
1190 ),
1191 causal_chain: chain,
1192 }
1193 }
1194
1195 fn explain_army_morale(&self, army_id: ArmyId) -> Explanation {
1196 let world = self.world();
1197 let Some(army) = world.army(army_id) else {
1198 return Explanation {
1199 summary: format!("Army {army_id} was not found"),
1200 causal_chain: Vec::new(),
1201 };
1202 };
1203 let provenance = self.events().iter().rev().find(|event| {
1204 matches!(
1205 &event.kind,
1206 EventKind::DebugFieldChanged { entity: EntityRef::Army(id), field, .. }
1207 if *id == army_id && field == "morale"
1208 )
1209 });
1210 provenance.map_or_else(
1211 || Explanation {
1212 summary: format!(
1213 "{} morale is {}; no post-scenario morale-changing event is recorded",
1214 army.name, army.morale
1215 ),
1216 causal_chain: Vec::new(),
1217 },
1218 |event| self.explain_event(event.id),
1219 )
1220 }
1221}
1222
1223fn event_by_id(events: &[SimEvent], event_id: EventId) -> Option<&SimEvent> {
1224 let index = usize::try_from(event_id.get().checked_sub(1)?).ok()?;
1225 events.get(index).filter(|event| event.id == event_id)
1226}
1227
1228impl CompactedCanwu {
1229 pub fn from_checkpoint_and_journal(
1230 checkpoint: SimulationCheckpoint,
1231 segments: Vec<EvidenceJournalSegment>,
1232 ) -> Result<Self, CanwuError> {
1233 Ok(Self {
1234 simulation: CompactedSimulation::from_checkpoint_and_journal(checkpoint, segments)?,
1235 })
1236 }
1237
1238 pub fn from_checkpoint_and_journal_with_plugins(
1239 checkpoint: SimulationCheckpoint,
1240 segments: Vec<EvidenceJournalSegment>,
1241 plugins: &[&dyn SimulationPlugin],
1242 ) -> Result<Self, CanwuError> {
1243 Ok(Self {
1244 simulation: CompactedSimulation::from_checkpoint_and_journal_with_plugins(
1245 checkpoint, segments, plugins,
1246 )?,
1247 })
1248 }
1249
1250 pub fn evidence_cursor(&self) -> Result<EvidenceCursor, CanwuError> {
1251 self.simulation.evidence_cursor()
1252 }
1253
1254 pub fn checkpoint(&self) -> Result<SimulationCheckpoint, CanwuError> {
1255 self.simulation.checkpoint()
1256 }
1257
1258 #[must_use]
1259 pub fn archived_evidence_receipt(
1260 &self,
1261 reference: &EvidenceRef,
1262 ) -> Option<&ArchivedEvidenceReceipt> {
1263 self.simulation.archived_evidence_receipt(reference)
1264 }
1265
1266 pub fn load_archived_evidence_segment(
1267 &self,
1268 reference: &EvidenceRef,
1269 provider: &dyn ArchiveProvider,
1270 ) -> Result<EvidenceJournalSegment, CanwuError> {
1271 self.simulation
1272 .load_archived_evidence_segment(reference, provider)
1273 }
1274
1275 pub fn seal_evidence(&mut self) -> Result<Option<EvidenceJournalSegment>, CanwuError> {
1276 self.simulation.seal_evidence()
1277 }
1278
1279 pub fn prepare_evidence_seal(&self) -> Result<Option<PreparedEvidenceSeal>, CanwuError> {
1280 self.simulation.prepare_evidence_seal()
1281 }
1282
1283 pub fn commit_evidence_seal(
1284 &mut self,
1285 token: &EvidenceSealToken,
1286 provider: &dyn ArchiveProvider,
1287 ) -> Result<(), CanwuError> {
1288 self.simulation.commit_evidence_seal(token, provider)
1289 }
1290
1291 pub fn snapshot_with_segments(
1292 &self,
1293 segments: Vec<EvidenceJournalSegment>,
1294 ) -> Result<SimulationSnapshot, CanwuError> {
1295 self.simulation.snapshot_with_segments(segments)
1296 }
1297
1298 pub fn replay_journal_with_segments(
1299 &self,
1300 segments: Vec<EvidenceJournalSegment>,
1301 ) -> Result<ReplayJournal, CanwuError> {
1302 self.simulation.replay_journal_with_segments(segments)
1303 }
1304
1305 #[must_use]
1306 pub const fn time(&self) -> SimTime {
1307 self.simulation.time()
1308 }
1309
1310 #[must_use]
1311 pub const fn revision(&self) -> u64 {
1312 self.simulation.revision()
1313 }
1314
1315 #[must_use]
1316 pub fn checkpoint_hash(&self) -> &str {
1317 self.simulation.checkpoint_hash()
1318 }
1319
1320 #[must_use]
1321 pub fn boundary_head_hash(&self) -> Option<&str> {
1322 self.simulation.boundary_head_hash()
1323 }
1324
1325 #[must_use]
1326 pub fn world(&self) -> WorldSnapshot {
1327 self.simulation.world()
1328 }
1329
1330 #[must_use]
1331 pub fn knowledge(&self) -> &KnowledgeSnapshot {
1332 self.simulation.knowledge()
1333 }
1334
1335 #[must_use]
1336 pub fn domain_record(&self, reference: &DomainRecordRef) -> Option<&DomainRecord> {
1337 self.simulation.domain_record(reference)
1338 }
1339
1340 #[must_use]
1341 pub fn typed_domain_record<T: DomainRecordType>(
1342 &self,
1343 reference: &TypedDomainRecordRef<T>,
1344 ) -> Option<&DomainRecord> {
1345 self.simulation.typed_domain_record(reference)
1346 }
1347
1348 #[must_use]
1349 pub const fn decision_state(&self) -> &DecisionState {
1350 self.simulation.decision_state()
1351 }
1352
1353 #[must_use]
1354 pub fn decision_ticket(&self, id: DecisionTicketId) -> Option<&DecisionTicket> {
1355 self.simulation.decision_ticket(id)
1356 }
1357
1358 #[must_use]
1359 pub fn decision_traces(&self) -> &[DecisionTrace] {
1360 self.simulation.decision_traces()
1361 }
1362
1363 #[must_use]
1364 pub fn decision_attempts(&self) -> &[DecisionAttemptRecord] {
1365 self.simulation.decision_attempts()
1366 }
1367
1368 pub fn submit(&mut self, envelope: CommandEnvelope) -> Result<CommandReceipt, CanwuError> {
1369 self.simulation.submit(envelope)
1370 }
1371
1372 pub fn process_command(
1373 &mut self,
1374 request: CommandRequest,
1375 ) -> Result<CommandOutcome, CanwuError> {
1376 self.simulation.process_command(request)
1377 }
1378
1379 pub fn enqueue_command(
1380 &mut self,
1381 due_at: SimTime,
1382 priority: i32,
1383 request: CommandRequest,
1384 ) -> Result<IngressReceipt, CanwuError> {
1385 self.simulation.enqueue_command(due_at, priority, request)
1386 }
1387
1388 pub fn enqueue_plugin_ingress(
1389 &mut self,
1390 request: PluginIngressRequest,
1391 ) -> Result<IngressReceipt, CanwuError> {
1392 self.simulation.enqueue_plugin_ingress(request)
1393 }
1394
1395 pub fn prepare_decision(
1396 &self,
1397 decision_request_id: DecisionRequestId,
1398 command_request_id: Option<CommandRequestId>,
1399 ticket_id: DecisionTicketId,
1400 policy: &dyn DecisionPolicy,
1401 ) -> Result<DecisionEvaluation, CanwuError> {
1402 self.simulation
1403 .prepare_decision(decision_request_id, command_request_id, ticket_id, policy)
1404 }
1405
1406 pub fn prepare_decision_at(
1407 &self,
1408 due_at: SimTime,
1409 decision_request_id: DecisionRequestId,
1410 command_request_id: Option<CommandRequestId>,
1411 ticket_id: DecisionTicketId,
1412 policy: &dyn DecisionPolicy,
1413 ) -> Result<DecisionEvaluation, CanwuError> {
1414 self.simulation.prepare_decision_at(
1415 due_at,
1416 decision_request_id,
1417 command_request_id,
1418 ticket_id,
1419 policy,
1420 )
1421 }
1422
1423 pub fn enqueue_decision(
1424 &mut self,
1425 due_at: SimTime,
1426 priority: i32,
1427 request: DecisionIngressRequest,
1428 ) -> Result<IngressReceipt, CanwuError> {
1429 self.simulation.enqueue_decision(due_at, priority, request)
1430 }
1431
1432 pub fn drive_decision(
1433 &mut self,
1434 due_at: SimTime,
1435 priority: i32,
1436 decision_request_id: DecisionRequestId,
1437 command_request_id: Option<CommandRequestId>,
1438 ticket_id: DecisionTicketId,
1439 policy: &dyn DecisionPolicy,
1440 ) -> Result<DecisionEvaluation, CanwuError> {
1441 self.simulation.drive_decision(
1442 due_at,
1443 priority,
1444 decision_request_id,
1445 command_request_id,
1446 ticket_id,
1447 policy,
1448 )
1449 }
1450
1451 pub fn schedule_calendar_boundary(
1452 &mut self,
1453 due_at: SimTime,
1454 cadences: Vec<SystemCadence>,
1455 ) -> Result<IngressReceipt, CanwuError> {
1456 self.simulation.schedule_calendar_boundary(due_at, cadences)
1457 }
1458
1459 pub fn advance(&mut self, duration: SimDuration) -> Result<Vec<SimEvent>, CanwuError> {
1460 self.simulation.advance(duration)
1461 }
1462
1463 pub fn advance_canonical(
1464 &mut self,
1465 duration: SimDuration,
1466 ) -> Result<Vec<BoundaryReceipt>, CanwuError> {
1467 self.simulation.advance_canonical(duration)
1468 }
1469
1470 pub fn step_canonical(&mut self) -> Result<Option<BoundaryReceipt>, CanwuError> {
1471 self.simulation.step_canonical()
1472 }
1473
1474 pub fn settle_boundary(
1475 &mut self,
1476 request: BoundaryRequest,
1477 ) -> Result<BoundaryReceipt, CanwuError> {
1478 self.simulation.settle_boundary(request)
1479 }
1480}
1481
1482#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1483#[serde(rename_all = "snake_case")]
1484pub enum QueryEntity {
1485 Person,
1486 Government,
1487 Territory,
1488 Route,
1489 Army,
1490 Event,
1491}
1492
1493#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1494#[serde(rename_all = "snake_case")]
1495pub enum FilterOperator {
1496 Equal,
1497 Contains,
1498}
1499
1500#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1501pub struct QueryFilter {
1502 pub field: String,
1503 pub operator: FilterOperator,
1504 pub value: Value,
1505}
1506
1507#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1508pub struct Query {
1509 pub entity: QueryEntity,
1510 pub filters: Vec<QueryFilter>,
1511 pub select: Vec<String>,
1512 pub limit: usize,
1513}
1514
1515impl Query {
1516 #[must_use]
1517 pub const fn all(entity: QueryEntity) -> Self {
1518 Self {
1519 entity,
1520 filters: Vec::new(),
1521 select: Vec::new(),
1522 limit: 100,
1523 }
1524 }
1525}
1526
1527#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
1528pub struct QueryResult {
1529 pub rows: Vec<BTreeMap<String, Value>>,
1530 pub truncated: bool,
1531}
1532
1533fn run_query(world: &WorldSnapshot, events: &[SimEvent], query: &Query) -> QueryResult {
1534 let rows: Vec<_> = match query.entity {
1535 QueryEntity::Person => world
1536 .people
1537 .iter()
1538 .map(|person| value_to_row(&json!(person)))
1539 .collect(),
1540 QueryEntity::Government => world
1541 .governments
1542 .iter()
1543 .map(|government| value_to_row(&json!(government)))
1544 .collect(),
1545 QueryEntity::Territory => world
1546 .territories
1547 .iter()
1548 .map(|territory| value_to_row(&json!(territory)))
1549 .collect(),
1550 QueryEntity::Route => world
1551 .routes
1552 .iter()
1553 .map(|route| value_to_row(&json!(route)))
1554 .collect(),
1555 QueryEntity::Army => world
1556 .armies
1557 .iter()
1558 .map(|army| value_to_row(&json!(army)))
1559 .collect(),
1560 QueryEntity::Event => events
1561 .iter()
1562 .map(|event| value_to_row(&json!(event)))
1563 .collect(),
1564 };
1565 finalize_query(rows, query)
1566}
1567
1568fn run_actor_query(
1569 world: &WorldSnapshot,
1570 actor: PersonId,
1571 knowledge: Option<&ActorKnowledge>,
1572 query: &Query,
1573) -> QueryResult {
1574 match query.entity {
1575 QueryEntity::Army => {
1576 let rows = knowledge.map_or_else(Vec::new, |knowledge| {
1577 knowledge
1578 .armies
1579 .values()
1580 .map(|record| value_to_row(&json!(record)))
1581 .collect()
1582 });
1583 finalize_query(rows, query)
1584 }
1585 QueryEntity::Person => {
1586 let rows = world
1587 .person(actor)
1588 .map_or_else(Vec::new, |person| vec![value_to_row(&json!(person))]);
1589 finalize_query(rows, query)
1590 }
1591 QueryEntity::Event => QueryResult::default(),
1592 QueryEntity::Government | QueryEntity::Territory | QueryEntity::Route => {
1593 QueryResult::default()
1594 }
1595 }
1596}
1597
1598fn finalize_query(rows: Vec<BTreeMap<String, Value>>, query: &Query) -> QueryResult {
1599 let filtered: Vec<_> = rows
1600 .into_iter()
1601 .filter(|row| {
1602 query
1603 .filters
1604 .iter()
1605 .all(|filter| matches_filter(row, filter))
1606 })
1607 .collect();
1608 let truncated = filtered.len() > query.limit;
1609 let rows = filtered
1610 .into_iter()
1611 .take(query.limit)
1612 .map(|row| select_fields(row, &query.select))
1613 .collect();
1614 QueryResult { rows, truncated }
1615}
1616
1617fn matches_filter(row: &BTreeMap<String, Value>, filter: &QueryFilter) -> bool {
1618 let Some(actual) = row.get(&filter.field) else {
1619 return false;
1620 };
1621 match filter.operator {
1622 FilterOperator::Equal => actual == &filter.value,
1623 FilterOperator::Contains => value_text(actual)
1624 .to_lowercase()
1625 .contains(&value_text(&filter.value).to_lowercase()),
1626 }
1627}
1628
1629fn value_text(value: &Value) -> String {
1630 value
1631 .as_str()
1632 .map_or_else(|| value.to_string(), str::to_owned)
1633}
1634
1635fn select_fields(mut row: BTreeMap<String, Value>, select: &[String]) -> BTreeMap<String, Value> {
1636 if select.is_empty() {
1637 return row;
1638 }
1639 row.retain(|field, _| select.contains(field));
1640 row
1641}
1642
1643fn value_to_row(value: &Value) -> BTreeMap<String, Value> {
1644 value.as_object().map_or_else(BTreeMap::new, |object| {
1645 object
1646 .iter()
1647 .map(|(key, value)| (key.clone(), value.clone()))
1648 .collect()
1649 })
1650}
1651
1652#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1653#[serde(rename_all = "snake_case")]
1654pub enum ObservationFocus {
1655 CurrentSituation,
1656 Military,
1657 Changes,
1658}
1659
1660#[derive(Clone, Debug, Eq, PartialEq)]
1664pub enum ObservationPrincipal {
1665 Person(PersonId),
1666 Institution(EntityRef),
1667 Public,
1668 Research,
1669 Developer,
1670}
1671
1672impl ObservationPrincipal {
1673 const fn person(&self) -> Option<PersonId> {
1674 match self {
1675 Self::Person(actor) => Some(*actor),
1676 Self::Institution(_) | Self::Public | Self::Research | Self::Developer => None,
1677 }
1678 }
1679}
1680
1681#[derive(Clone, Debug, Eq, PartialEq)]
1682pub struct ViewerContext {
1683 principal: ObservationPrincipal,
1684 observation: ObservationPolicy,
1685 checkpoint_hash: String,
1686}
1687
1688impl ViewerContext {
1689 #[must_use]
1690 pub const fn principal(&self) -> &ObservationPrincipal {
1691 &self.principal
1692 }
1693
1694 #[must_use]
1695 pub const fn actor(&self) -> Option<PersonId> {
1696 self.principal.person()
1697 }
1698
1699 #[must_use]
1700 pub const fn observation(&self) -> ObservationPolicy {
1701 self.observation
1702 }
1703}
1704
1705#[derive(Clone, Debug)]
1706struct KnowledgeViewContext {
1707 principal: ObservationPrincipal,
1708}
1709
1710pub struct CanwuViewer<'a> {
1713 canwu: &'a Canwu,
1714 context: KnowledgeViewContext,
1715}
1716
1717impl CanwuViewer<'_> {
1718 #[must_use]
1719 pub const fn principal(&self) -> &ObservationPrincipal {
1720 &self.context.principal
1721 }
1722
1723 pub fn query_knowledge(
1727 &self,
1728 query: &KnowledgeQuery,
1729 ) -> Result<KnowledgeQueryResult, CanwuError> {
1730 let holder = match &self.context.principal {
1731 ObservationPrincipal::Person(actor) => KnowledgeHolderRef::Person(*actor),
1732 ObservationPrincipal::Institution(entity) => KnowledgeHolderRef::Entity(entity.clone()),
1733 ObservationPrincipal::Public => return Err(invalid_knowledge_authority()),
1734 ObservationPrincipal::Research | ObservationPrincipal::Developer => {
1735 return Err(CanwuError::new(
1736 ErrorCode::InvalidKnowledgeAuthority,
1737 "diagnostic viewers must select a holder explicitly",
1738 ));
1739 }
1740 };
1741 self.canwu.admin_query_knowledge(holder, query)
1742 }
1743
1744 pub fn query_holder_knowledge(
1747 &self,
1748 holder: KnowledgeHolderRef,
1749 query: &KnowledgeQuery,
1750 ) -> Result<KnowledgeQueryResult, CanwuError> {
1751 match self.context.principal {
1752 ObservationPrincipal::Research | ObservationPrincipal::Developer => {}
1753 ObservationPrincipal::Person(_)
1754 | ObservationPrincipal::Institution(_)
1755 | ObservationPrincipal::Public => return Err(invalid_knowledge_authority()),
1756 }
1757 if !knowledge_holder_exists(self.canwu, &holder) {
1758 return Err(CanwuError::new(
1759 ErrorCode::InvalidKnowledgeHolder,
1760 "the requested knowledge holder does not exist",
1761 ));
1762 }
1763 self.canwu.admin_query_knowledge(holder, query)
1764 }
1765
1766 pub fn audit_knowledge_record(
1769 &self,
1770 holder: &KnowledgeHolderRef,
1771 record: HolderKnowledgeRecordId,
1772 ) -> Result<KnowledgeRecord, CanwuError> {
1773 match self.context.principal {
1774 ObservationPrincipal::Research | ObservationPrincipal::Developer => {}
1775 ObservationPrincipal::Person(_)
1776 | ObservationPrincipal::Institution(_)
1777 | ObservationPrincipal::Public => return Err(invalid_knowledge_authority()),
1778 }
1779 if !knowledge_holder_exists(self.canwu, holder) {
1780 return Err(CanwuError::new(
1781 ErrorCode::InvalidKnowledgeHolder,
1782 "the requested knowledge holder does not exist",
1783 ));
1784 }
1785 let index = usize::try_from(record.get().saturating_sub(1)).map_err(|_| {
1786 CanwuError::new(
1787 ErrorCode::KnowledgeRecordNotFound,
1788 "holder-relative knowledge record ID is outside the supported range",
1789 )
1790 })?;
1791 self.canwu
1792 .knowledge()
1793 .for_holder(holder)
1794 .and_then(|records| records.values().nth(index))
1795 .cloned()
1796 .ok_or_else(|| {
1797 CanwuError::new(
1798 ErrorCode::KnowledgeRecordNotFound,
1799 "holder-relative knowledge record was not found",
1800 )
1801 })
1802 }
1803
1804 pub fn observe(&self, request: &ObserveRequest) -> Result<AgentContext, CanwuError> {
1805 let ObservationPrincipal::Person(actor) = self.context.principal else {
1806 return Err(CanwuError::new(
1807 ErrorCode::InvalidAuthority,
1808 "agent observation requires a person principal",
1809 ));
1810 };
1811 self.canwu.observe(actor, request)
1812 }
1813
1814 #[must_use]
1815 pub fn visible_changes_since(&self, since: SimTime) -> Vec<VisibleChange> {
1816 let context = ViewerContext {
1817 principal: self.context.principal.clone(),
1818 observation: observation_for_principal(&self.context.principal),
1819 checkpoint_hash: self.canwu.checkpoint_hash().to_owned(),
1820 };
1821 self.canwu
1822 .events()
1823 .iter()
1824 .filter(|event| event.timestamp > since)
1825 .filter_map(|event| {
1826 let audience = self.canwu.simulation.event_audience(event);
1827 visible_change(&context, event, &audience)
1828 })
1829 .collect()
1830 }
1831}
1832
1833const fn observation_for_principal(principal: &ObservationPrincipal) -> ObservationPolicy {
1834 match principal {
1835 ObservationPrincipal::Person(_) | ObservationPrincipal::Institution(_) => {
1836 ObservationPolicy::ActorBound
1837 }
1838 ObservationPrincipal::Public => ObservationPolicy::PublicObserver,
1839 ObservationPrincipal::Research => ObservationPolicy::ResearchFull,
1840 ObservationPrincipal::Developer => ObservationPolicy::DeveloperDiagnostic,
1841 }
1842}
1843
1844fn invalid_knowledge_authority() -> CanwuError {
1845 CanwuError::new(
1846 ErrorCode::InvalidKnowledgeAuthority,
1847 "this observation principal cannot read a private knowledge ledger",
1848 )
1849}
1850
1851fn knowledge_holder_exists(canwu: &Canwu, holder: &KnowledgeHolderRef) -> bool {
1852 match holder {
1853 KnowledgeHolderRef::Person(actor) => canwu.world().person(*actor).is_some(),
1854 KnowledgeHolderRef::Entity(entity) => match entity {
1855 EntityRef::Army(id) => canwu.world().army(*id).is_some(),
1856 EntityRef::Government(id) => canwu.world().government(*id).is_some(),
1857 EntityRef::Person(id) => canwu.world().person(*id).is_some(),
1858 EntityRef::Domain(reference) => canwu
1859 .domain_record(reference)
1860 .is_some_and(|record| !record.is_deleted()),
1861 EntityRef::Organization(_)
1862 | EntityRef::Resource(_)
1863 | EntityRef::Route(_)
1864 | EntityRef::Territory(_) => false,
1865 },
1866 }
1867}
1868
1869fn map_knowledge_query_error(error: KnowledgeQueryError) -> CanwuError {
1870 match error {
1871 KnowledgeQueryError::ReadCutUnavailable => CanwuError::new(
1872 ErrorCode::KnowledgeReadCutUnavailable,
1873 "knowledge cursor read cut is no longer available",
1874 ),
1875 KnowledgeQueryError::InvalidLimit => CanwuError::new(
1876 ErrorCode::KnowledgeLimitExceeded,
1877 "knowledge query page size is outside the supported range",
1878 ),
1879 KnowledgeQueryError::InvalidCursor
1880 | KnowledgeQueryError::InvalidLedger
1881 | KnowledgeQueryError::Encoding => CanwuError::new(
1882 ErrorCode::InvalidKnowledgeRecord,
1883 "knowledge query, cursor, or ledger is invalid",
1884 ),
1885 }
1886}
1887
1888#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1889pub struct ObserveRequest {
1890 pub focus: ObservationFocus,
1891 pub since: Option<SimTime>,
1892}
1893
1894impl Default for ObserveRequest {
1895 fn default() -> Self {
1896 Self {
1897 focus: ObservationFocus::CurrentSituation,
1898 since: None,
1899 }
1900 }
1901}
1902
1903#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1904pub struct AgentIdentity {
1905 pub person: PersonId,
1906 pub name: String,
1907 pub roles: Vec<String>,
1908}
1909
1910#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1911pub struct KnownArmyView {
1912 pub army: ArmyId,
1913 pub name: String,
1914 pub known_location: Option<TerritoryId>,
1915 pub estimated_strength: EstimateRange,
1916 pub information_age_minutes: i64,
1917 pub confidence_per_mille: u16,
1918 pub source: KnowledgeSource,
1919}
1920
1921fn known_army_view(now: SimTime, record: &ArmyKnowledge) -> Result<KnownArmyView, CanwuError> {
1922 let information_age = now.checked_sub(record.observed_at).ok_or_else(|| {
1923 CanwuError::new(
1924 ErrorCode::InvalidDuration,
1925 "knowledge age exceeds the supported simulation-duration range",
1926 )
1927 })?;
1928 Ok(KnownArmyView {
1929 army: record.army,
1930 name: record
1931 .known_name
1932 .clone()
1933 .unwrap_or_else(|| format!("Army {}", record.army)),
1934 known_location: record.known_location,
1935 estimated_strength: record.estimated_strength,
1936 information_age_minutes: information_age.as_minutes(),
1937 confidence_per_mille: record.confidence_per_mille,
1938 source: record.source.clone(),
1939 })
1940}
1941
1942#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1943pub struct VisibleChange {
1944 pub timestamp: SimTime,
1945 pub summary: String,
1946 pub source_event: EventId,
1947}
1948
1949fn visible_change(
1950 viewer: &ViewerContext,
1951 event: &SimEvent,
1952 plugin_audience: &EventAudience,
1953) -> Option<VisibleChange> {
1954 let visible = match &event.kind {
1955 EventKind::MoveOrdered { .. } => viewer
1956 .principal
1957 .person()
1958 .is_some_and(|actor| event.affected_entities.contains(&EntityRef::Person(actor))),
1959 EventKind::PersonMoveOrdered { .. } => viewer
1960 .principal
1961 .person()
1962 .is_some_and(|actor| event.affected_entities.contains(&EntityRef::Person(actor))),
1963 EventKind::KnowledgeUpdated { recipient, .. } => {
1964 viewer.principal.person() == Some(*recipient)
1965 }
1966 EventKind::KnowledgePublished { holder, .. } => {
1967 principal_matches_holder(&viewer.principal, holder)
1968 }
1969 EventKind::ArmyArrived { .. }
1970 | EventKind::PersonArrived { .. }
1971 | EventKind::LetterDelivered { .. }
1972 | EventKind::ReportDispatched { .. }
1973 | EventKind::DebugFieldChanged { .. } => matches!(
1974 viewer.observation,
1975 ObservationPolicy::ResearchFull | ObservationPolicy::DeveloperDiagnostic
1976 ),
1977 EventKind::Plugin { .. } => event_visible_to(viewer, event, plugin_audience),
1978 };
1979 visible.then(|| VisibleChange {
1980 timestamp: event.timestamp,
1981 summary: event.summary.clone(),
1982 source_event: event.id,
1983 })
1984}
1985
1986fn event_visible_to(viewer: &ViewerContext, event: &SimEvent, audience: &EventAudience) -> bool {
1987 if matches!(
1988 viewer.observation,
1989 ObservationPolicy::ResearchFull | ObservationPolicy::DeveloperDiagnostic
1990 ) {
1991 return true;
1992 }
1993 match audience {
1994 EventAudience::Public => true,
1995 EventAudience::Actor(actor) => viewer.principal.person() == Some(*actor),
1996 EventAudience::Actors(actors) => viewer
1997 .principal
1998 .person()
1999 .is_some_and(|actor| actors.binary_search(&actor).is_ok()),
2000 EventAudience::KnowledgeHolder(holder) => {
2001 principal_matches_holder(&viewer.principal, holder)
2002 }
2003 EventAudience::AffectedActors => viewer
2004 .principal
2005 .person()
2006 .is_some_and(|actor| event.affected_entities.contains(&EntityRef::Person(actor))),
2007 EventAudience::Private => false,
2008 }
2009}
2010
2011fn principal_matches_holder(principal: &ObservationPrincipal, holder: &KnowledgeHolderRef) -> bool {
2012 match (principal, holder) {
2013 (ObservationPrincipal::Person(actor), KnowledgeHolderRef::Person(holder)) => {
2014 actor == holder
2015 }
2016 (ObservationPrincipal::Institution(institution), KnowledgeHolderRef::Entity(holder)) => {
2017 institution == holder
2018 }
2019 (ObservationPrincipal::Research | ObservationPrincipal::Developer, _) => true,
2020 (
2021 ObservationPrincipal::Person(_)
2022 | ObservationPrincipal::Institution(_)
2023 | ObservationPrincipal::Public,
2024 _,
2025 ) => false,
2026 }
2027}
2028
2029#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2030pub struct PendingCommitment {
2031 pub summary: String,
2032 pub due_at: SimTime,
2033}
2034
2035#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
2036pub struct AvailableAction {
2037 pub action_type: String,
2038 pub description: String,
2039 pub payload: Value,
2040 pub legal_reason: String,
2041}
2042
2043#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
2044pub struct AgentContext {
2045 pub identity: AgentIdentity,
2046 pub current_time: SimTime,
2047 pub current_location: TerritoryId,
2048 pub focus: ObservationFocus,
2049 pub known_armies: Vec<KnownArmyView>,
2050 pub changes_since: Vec<VisibleChange>,
2051 pub pending_actions: Vec<PendingCommitment>,
2052 pub available_actions: Vec<AvailableAction>,
2053}
2054
2055#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2056#[serde(rename_all = "snake_case")]
2057pub enum DetailLevel {
2058 Summary,
2059 Domain,
2060 Entity,
2061 RawFields,
2062}
2063
2064#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
2065pub struct Inspection {
2066 pub entity: EntityRef,
2067 pub detail: DetailLevel,
2068 pub summary: String,
2069 pub fields: BTreeMap<String, Value>,
2070}
2071
2072fn missing_inspection(entity: &EntityRef, detail: DetailLevel) -> Inspection {
2073 Inspection {
2074 entity: entity.clone(),
2075 detail,
2076 summary: format!("{entity} was not found"),
2077 fields: BTreeMap::new(),
2078 }
2079}
2080
2081fn no_knowledge_inspection(entity: &EntityRef, detail: DetailLevel) -> Inspection {
2082 Inspection {
2083 entity: entity.clone(),
2084 detail,
2085 summary: "No actor-scoped knowledge is available for this entity".to_owned(),
2086 fields: BTreeMap::new(),
2087 }
2088}
2089
2090#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
2091#[serde(tag = "type", rename_all = "snake_case")]
2092pub enum SemanticAction {
2093 SelfMove {
2094 destination: TerritoryId,
2095 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2096 cargo: Vec<LetterId>,
2097 },
2098 MoveEntity {
2099 subject: EntityRef,
2100 destination: TerritoryId,
2101 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2102 cargo: Vec<LetterId>,
2103 },
2104 Plugin {
2105 plugin: String,
2106 action: String,
2107 payload: Value,
2108 },
2109}
2110
2111#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
2112#[serde(tag = "type", content = "value", rename_all = "snake_case")]
2113pub enum ExplanationRequest {
2114 Event(EventId),
2115 ArmyMorale(ArmyId),
2116 Failure(CanwuError),
2117}
2118
2119#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2120pub struct ExplanationStep {
2121 pub label: String,
2122 pub event: Option<EventId>,
2123}
2124
2125#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2126pub struct Explanation {
2127 pub summary: String,
2128 pub causal_chain: Vec<ExplanationStep>,
2129}
2130
2131#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2132pub struct CapabilityDescription {
2133 pub operations: Vec<String>,
2134 pub notes: Vec<String>,
2135 pub plugin_actions: Vec<String>,
2136}
2137
2138#[cfg(test)]
2139mod tests {
2140 use super::*;
2141
2142 fn manifest_for_configuration(
2143 scenario: &Scenario,
2144 configuration: &RunConfiguration,
2145 ) -> RunManifest {
2146 let scenario_manifest =
2147 ArtifactManifest::for_scenario("fixture", "viewer-scenario", "1", scenario)
2148 .expect("scenario manifest should hash");
2149 let configuration_manifest = ArtifactManifest::for_run_configuration(
2150 "fixture",
2151 "viewer-configuration",
2152 "1",
2153 configuration,
2154 )
2155 .expect("run configuration manifest should hash");
2156 RunManifest::declared(scenario_manifest, configuration_manifest)
2157 }
2158
2159 struct VisibilityPlugin {
2160 audience: EventAudience,
2161 }
2162
2163 #[allow(clippy::unnecessary_wraps)]
2164 fn visibility_system(
2165 _view: &SimulationView<'_>,
2166 event: &SimEvent,
2167 ) -> Result<Vec<SystemDirective>, CanwuError> {
2168 if !matches!(event.kind, EventKind::MoveOrdered { .. }) {
2169 return Ok(Vec::new());
2170 }
2171 Ok(vec![SystemDirective::Emit {
2172 event_type: "notice".to_owned(),
2173 summary: "a plugin visibility notice".to_owned(),
2174 affected: vec![EntityRef::Person(PersonId::new(1))],
2175 }])
2176 }
2177
2178 impl SimulationPlugin for VisibilityPlugin {
2179 fn name(&self) -> &'static str {
2180 "visibility-test"
2181 }
2182
2183 fn version(&self) -> &'static str {
2184 "test-v1"
2185 }
2186
2187 fn semantic_hash(&self) -> &'static str {
2188 "0000000000000000000000000000000000000000000000000000000000000001"
2189 }
2190
2191 fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
2192 registrar.register_event_audience("notice", self.audience.clone())?;
2193 registrar.register_system(
2194 SystemContract::event_driven(
2195 "emit-notice",
2196 BoundaryPhase::PerspectiveAndReportMaterialization,
2197 ),
2198 visibility_system,
2199 )
2200 }
2201 }
2202
2203 #[test]
2204 fn plugin_event_visibility_respects_public_actor_and_private_audiences() {
2205 let ids = Canwu::demo_ids();
2206 let event = SimEvent {
2207 id: EventId::new(1),
2208 timestamp: SimTime::EPOCH,
2209 kind: EventKind::Plugin {
2210 plugin: "visibility-test".to_owned(),
2211 event_type: "notice".to_owned(),
2212 },
2213 affected_entities: vec![EntityRef::Person(ids.commander)],
2214 summary: "notice".to_owned(),
2215 cause: None,
2216 correlation_id: 1,
2217 };
2218 let actor = ViewerContext {
2219 principal: ObservationPrincipal::Person(ids.commander),
2220 observation: ObservationPolicy::ActorBound,
2221 checkpoint_hash: String::new(),
2222 };
2223 let observer = ViewerContext {
2224 principal: ObservationPrincipal::Person(ids.observer),
2225 observation: ObservationPolicy::ActorBound,
2226 checkpoint_hash: String::new(),
2227 };
2228 let public_observer = ViewerContext {
2229 principal: ObservationPrincipal::Public,
2230 observation: ObservationPolicy::PublicObserver,
2231 checkpoint_hash: String::new(),
2232 };
2233 let research = ViewerContext {
2234 principal: ObservationPrincipal::Research,
2235 observation: ObservationPolicy::ResearchFull,
2236 checkpoint_hash: String::new(),
2237 };
2238
2239 assert!(visible_change(&actor, &event, &EventAudience::Public).is_some());
2240 assert!(visible_change(&public_observer, &event, &EventAudience::Public).is_some());
2241 assert!(visible_change(&actor, &event, &EventAudience::Actor(ids.commander)).is_some());
2242 assert!(visible_change(&observer, &event, &EventAudience::Actor(ids.commander)).is_none());
2243 assert!(visible_change(&observer, &event, &EventAudience::Private).is_none());
2244 assert!(visible_change(&research, &event, &EventAudience::Private).is_some());
2245 }
2246
2247 #[test]
2248 fn observe_changes_since_uses_persisted_plugin_audience() {
2249 let ids = Canwu::demo_ids();
2250 let mut canwu = Canwu::demo(35).expect("demo should load");
2251 canwu
2252 .register_plugin(&VisibilityPlugin {
2253 audience: EventAudience::Public,
2254 })
2255 .expect("visibility plugin should register");
2256 let since = SimTime::from_minutes(-1);
2257 canwu
2258 .act(
2259 ids.commander,
2260 SemanticAction::MoveEntity {
2261 subject: EntityRef::Army(ids.army),
2262 destination: ids.eastern_territory,
2263 cargo: Vec::new(),
2264 },
2265 )
2266 .expect("movement should emit plugin notice");
2267
2268 let observer = canwu
2269 .observe(
2270 ids.observer,
2271 &ObserveRequest {
2272 focus: ObservationFocus::Changes,
2273 since: Some(since),
2274 },
2275 )
2276 .expect("observer should be authorized");
2277 assert!(
2278 observer
2279 .changes_since
2280 .iter()
2281 .any(|change| change.summary == "a plugin visibility notice")
2282 );
2283
2284 let snapshot_json = canwu
2285 .snapshot_json()
2286 .expect("audience declaration should serialize");
2287 let restored = Canwu::from_snapshot_json_with_plugins(
2288 &snapshot_json,
2289 &[&VisibilityPlugin {
2290 audience: EventAudience::Public,
2291 }],
2292 )
2293 .expect("audience declaration should survive snapshot loading");
2294 let restored_observer = restored
2295 .observe(
2296 ids.observer,
2297 &ObserveRequest {
2298 focus: ObservationFocus::Changes,
2299 since: Some(since),
2300 },
2301 )
2302 .expect("restored observer should be authorized");
2303 assert!(
2304 restored_observer
2305 .changes_since
2306 .iter()
2307 .any(|change| change.summary == "a plugin visibility notice")
2308 );
2309 }
2310
2311 #[test]
2312 fn observe_with_viewer_revalidates_input_control_context() {
2313 let canwu = Canwu::demo(35).expect("demo should load");
2314 let escalated = ViewerContext {
2315 principal: ObservationPrincipal::Research,
2316 observation: ObservationPolicy::ResearchFull,
2317 checkpoint_hash: canwu.checkpoint_hash().to_owned(),
2318 };
2319
2320 let error = canwu
2321 .observe_with_viewer(&escalated, &ObserveRequest::default())
2322 .expect_err("a caller cannot self-escalate the observation policy");
2323 assert_eq!(error.code, ErrorCode::InvalidAuthority);
2324 }
2325
2326 #[test]
2327 #[allow(clippy::too_many_lines)]
2328 fn restricted_viewer_derives_principal_and_rejects_public_private_reads() {
2329 let (scenario, ids) = canwu_sim::demo_scenario();
2330 let actor = Canwu::demo(69).expect("actor viewer fixture should initialize");
2331 let actor_viewer = actor
2332 .viewer_for_actor(ids.commander)
2333 .expect("legacy character viewer should derive");
2334 let error = actor_viewer
2335 .audit_knowledge_record(
2336 &KnowledgeHolderRef::Person(ids.commander),
2337 HolderKnowledgeRecordId::new(1),
2338 )
2339 .expect_err("actor viewers cannot read audit-bearing records");
2340 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
2341
2342 let public_configuration = RunConfiguration::read_only_observer();
2343 let public_manifest = manifest_for_configuration(&scenario, &public_configuration);
2344 let public = Canwu::new_with_run_configuration(
2345 71,
2346 scenario.clone(),
2347 public_manifest,
2348 public_configuration,
2349 )
2350 .expect("public viewer fixture should initialize");
2351 let public_viewer = public.viewer().expect("public principal should derive");
2352 assert_eq!(public_viewer.principal(), &ObservationPrincipal::Public);
2353 let error = public_viewer
2354 .query_knowledge(&KnowledgeQuery::default())
2355 .expect_err("public principal cannot read a private ledger");
2356 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
2357 let error = public_viewer
2358 .query_holder_knowledge(
2359 KnowledgeHolderRef::Person(ids.commander),
2360 &KnowledgeQuery::default(),
2361 )
2362 .expect_err("an arbitrary valid actor ID cannot upgrade a public viewer");
2363 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
2364 let error = public_viewer
2365 .audit_knowledge_record(
2366 &KnowledgeHolderRef::Person(ids.commander),
2367 HolderKnowledgeRecordId::new(1),
2368 )
2369 .expect_err("public viewers cannot read audit-bearing records");
2370 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
2371
2372 let institution_configuration = RunConfiguration {
2373 format_version: RUN_CONFIGURATION_FORMAT_VERSION,
2374 purpose: RunPurpose::Play,
2375 controller: ControllerPolicy::HumanRoleBound,
2376 seat: SeatPolicy::InstitutionBound,
2377 observation: ObservationPolicy::ActorBound,
2378 interaction: InteractionPolicy::EraInternalCommands,
2379 trace: TracePolicy::Causal,
2380 seat_binding: Some(SeatBinding {
2381 seat_id: "institution-seat".to_owned(),
2382 controller_id: "institution-controller".to_owned(),
2383 actor: Some(ids.commander),
2384 institution: Some(EntityRef::Government(ids.government)),
2385 permission_profile_id: "institution-profile".to_owned(),
2386 }),
2387 declared_interventions: Vec::new(),
2388 diagnostic_commands_enabled: false,
2389 require_idempotency_keys: true,
2390 };
2391 let institution_manifest =
2392 manifest_for_configuration(&scenario, &institution_configuration);
2393 let institution = Canwu::new_with_run_configuration(
2394 73,
2395 scenario.clone(),
2396 institution_manifest,
2397 institution_configuration,
2398 )
2399 .expect("institution viewer fixture should initialize");
2400 let institution_viewer = institution
2401 .viewer()
2402 .expect("institution principal should derive");
2403 assert_eq!(
2404 institution_viewer.principal(),
2405 &ObservationPrincipal::Institution(EntityRef::Government(ids.government))
2406 );
2407 assert_eq!(
2408 institution_viewer
2409 .query_knowledge(&KnowledgeQuery::default())
2410 .expect("institution may query only its bound ledger")
2411 .holder,
2412 KnowledgeHolderRef::Entity(EntityRef::Government(ids.government))
2413 );
2414 assert!(institution.viewer_for_actor(ids.commander).is_err());
2415 let error = institution_viewer
2416 .audit_knowledge_record(
2417 &KnowledgeHolderRef::Entity(EntityRef::Government(ids.government)),
2418 HolderKnowledgeRecordId::new(1),
2419 )
2420 .expect_err("institution viewers cannot read audit-bearing records");
2421 assert_eq!(error.code, ErrorCode::InvalidKnowledgeAuthority);
2422
2423 let mut research_configuration = RunConfiguration::read_only_observer();
2424 research_configuration.observation = ObservationPolicy::ResearchFull;
2425 let research_manifest = manifest_for_configuration(&scenario, &research_configuration);
2426 let research = Canwu::new_with_run_configuration(
2427 79,
2428 scenario,
2429 research_manifest,
2430 research_configuration,
2431 )
2432 .expect("research viewer fixture should initialize");
2433 let research_viewer = research.viewer().expect("research principal should derive");
2434 assert_eq!(research_viewer.principal(), &ObservationPrincipal::Research);
2435 assert_eq!(
2436 research_viewer
2437 .query_holder_knowledge(
2438 KnowledgeHolderRef::Person(ids.commander),
2439 &KnowledgeQuery::default(),
2440 )
2441 .expect("research may explicitly select an existing holder")
2442 .holder,
2443 KnowledgeHolderRef::Person(ids.commander)
2444 );
2445 }
2446
2447 #[test]
2448 fn detached_viewer_context_is_bound_to_the_authorized_checkpoint() {
2449 let mut canwu = Canwu::demo(83).expect("demo should load");
2450 let ids = Canwu::demo_ids();
2451 let context = canwu
2452 .viewer_context(ids.commander)
2453 .expect("the commander should receive a detached viewer context");
2454
2455 canwu
2456 .act(
2457 ids.commander,
2458 SemanticAction::MoveEntity {
2459 subject: EntityRef::Army(ids.army),
2460 destination: ids.eastern_territory,
2461 cargo: Vec::new(),
2462 },
2463 )
2464 .expect("the authoritative checkpoint should advance");
2465 let error = canwu
2466 .observe_with_viewer(&context, &ObserveRequest::default())
2467 .expect_err("a context from an older checkpoint must be rejected");
2468 assert_eq!(error.code, ErrorCode::InvalidAuthority);
2469
2470 let refreshed = canwu
2471 .viewer_context(ids.commander)
2472 .expect("the current checkpoint should issue a fresh context");
2473 canwu
2474 .observe_with_viewer(&refreshed, &ObserveRequest::default())
2475 .expect("the refreshed context should remain authorized");
2476 }
2477
2478 #[test]
2479 fn actor_relative_observation_does_not_leak_arrival() {
2480 let mut canwu = Canwu::demo(35).expect("demo should load");
2481 let ids = Canwu::demo_ids();
2482 canwu
2483 .act(
2484 ids.commander,
2485 SemanticAction::MoveEntity {
2486 subject: EntityRef::Army(ids.army),
2487 destination: ids.eastern_territory,
2488 cargo: Vec::new(),
2489 },
2490 )
2491 .expect("commander can move army");
2492 canwu
2493 .advance(SimDuration::days(1))
2494 .expect("arrival should execute");
2495
2496 assert_eq!(
2497 canwu.world().army(ids.army).expect("army exists").location,
2498 ids.eastern_territory
2499 );
2500 let observer = canwu
2501 .observe(ids.observer, &ObserveRequest::default())
2502 .expect("observer exists");
2503 assert_eq!(
2504 observer.known_armies[0].known_location,
2505 Some(ids.central_territory)
2506 );
2507 let person_rows = canwu
2508 .query_as(ids.observer, &Query::all(QueryEntity::Person))
2509 .expect("actor query should succeed");
2510 assert_eq!(person_rows.rows.len(), 1);
2511 assert_eq!(person_rows.rows[0].get("id"), Some(&json!(ids.observer)));
2512 for entity in [
2513 QueryEntity::Government,
2514 QueryEntity::Territory,
2515 QueryEntity::Route,
2516 ] {
2517 assert!(
2518 canwu
2519 .query_as(ids.observer, &Query::all(entity))
2520 .expect("actor query should succeed")
2521 .rows
2522 .is_empty()
2523 );
2524 }
2525 assert!(
2526 canwu
2527 .inspect(
2528 ids.observer,
2529 &EntityRef::Person(ids.commander),
2530 DetailLevel::RawFields,
2531 )
2532 .expect("inspection should succeed")
2533 .fields
2534 .is_empty()
2535 );
2536 assert!(
2537 canwu
2538 .inspect(
2539 ids.observer,
2540 &EntityRef::Territory(ids.eastern_territory),
2541 DetailLevel::RawFields,
2542 )
2543 .expect("inspection should succeed")
2544 .fields
2545 .is_empty()
2546 );
2547
2548 canwu
2549 .advance(SimDuration::days(3))
2550 .expect("report should arrive");
2551 let updated = canwu
2552 .observe(ids.observer, &ObserveRequest::default())
2553 .expect("observer exists");
2554 assert_eq!(
2555 updated.known_armies[0].known_location,
2556 Some(ids.eastern_territory)
2557 );
2558 }
2559
2560 #[test]
2561 fn self_move_is_an_actor_bound_order_movement() {
2562 let mut canwu = Canwu::demo(35).expect("demo should load");
2563 let ids = Canwu::demo_ids();
2564 let actions = canwu
2565 .available_actions(ids.commander)
2566 .expect("commander actions should be available");
2567 assert!(actions.iter().any(|action| {
2568 action.action_type == "self_move"
2569 && action.payload["destination"] == json!(ids.eastern_territory)
2570 }));
2571
2572 canwu
2573 .act(
2574 ids.commander,
2575 SemanticAction::SelfMove {
2576 destination: ids.eastern_territory,
2577 cargo: Vec::new(),
2578 },
2579 )
2580 .expect("a person may order their own movement");
2581 assert!(
2582 canwu
2583 .world()
2584 .person(ids.commander)
2585 .expect("commander exists")
2586 .transit
2587 .is_some()
2588 );
2589 }
2590
2591 #[test]
2592 fn debug_mutation_uses_validated_command_and_provenance() {
2593 let mut canwu = Canwu::demo(35).expect("demo should load");
2594 let ids = Canwu::demo_ids();
2595 let result = canwu.submit(CommandEnvelope::new(
2596 Issuer::Debug,
2597 Command::DebugSetArmyMorale {
2598 army: ids.army,
2599 morale: 37,
2600 },
2601 ));
2602 let receipt = result.expect("debug command should validate");
2603 assert_eq!(
2604 canwu.world().army(ids.army).expect("army exists").morale,
2605 37
2606 );
2607 let explanation = canwu.explain(&ExplanationRequest::Event(receipt.emitted_events[0]));
2608 assert!(explanation.causal_chain.len() >= 2);
2609 }
2610
2611 #[test]
2612 fn public_checkpoint_journal_round_trip_is_exact() {
2613 let mut canwu = Canwu::demo(35).expect("demo should load");
2614 let ids = Canwu::demo_ids();
2615 canwu
2616 .submit(CommandEnvelope::new(
2617 Issuer::Actor(ids.commander),
2618 Command::OrderMovement {
2619 subject: EntityRef::Army(ids.army),
2620 destination: ids.eastern_territory,
2621 cargo: Vec::new(),
2622 },
2623 ))
2624 .expect("movement should be accepted");
2625 canwu
2626 .advance(SimDuration::days(1))
2627 .expect("scheduled work should execute");
2628
2629 let checkpoint = canwu.checkpoint().expect("current state should checkpoint");
2630 assert!(checkpoint.state.events.is_empty());
2631 assert_eq!(
2632 checkpoint.journal_end,
2633 canwu
2634 .evidence_cursor()
2635 .expect("journal cursor should be representable")
2636 );
2637 let json = canwu
2638 .checkpoint_journal_json()
2639 .expect("checkpoint journal should serialize");
2640 let restored = Canwu::from_checkpoint_journal_json(&json)
2641 .expect("checkpoint journal should restore through the public facade");
2642 assert_eq!(restored.snapshot(), canwu.snapshot());
2643
2644 canwu
2645 .settle_boundary(BoundaryRequest::at(canwu.time()))
2646 .expect("a public boundary should complete the live evidence tail");
2647 let expected = canwu.snapshot();
2648 let mut compact = canwu
2649 .into_compacted()
2650 .expect("the public facade should enter compact mode");
2651 let segment = compact
2652 .seal_evidence()
2653 .expect("the public compact facade should seal evidence")
2654 .expect("the public compact facade should return a segment");
2655 let compact_checkpoint = compact
2656 .checkpoint()
2657 .expect("the public compact facade should checkpoint");
2658 assert_eq!(
2659 compact
2660 .snapshot_with_segments(vec![segment.clone()])
2661 .expect("the public compact facade should reconstruct its snapshot"),
2662 expected
2663 );
2664 let restored_compact =
2665 CompactedCanwu::from_checkpoint_and_journal(compact_checkpoint, vec![segment])
2666 .expect("the public compact facade should restore from its archive");
2667 assert_eq!(
2668 restored_compact
2669 .snapshot_with_segments(Vec::new())
2670 .expect("the restored compact facade should retain validated evidence"),
2671 expected
2672 );
2673 }
2674}