1#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)]
4
5pub use canwu_core::{
6 ArmyId, BoundaryId, CommandAttemptId, CommandId, CommandRequestId, CoreEntityKind,
7 DomainEntityKindClass, DomainEntityType, DomainKindClass, DomainRecordKind, DomainRecordRef,
8 DomainRecordType, DomainValueKindClass, DomainValueType, EntityRef, EventId, GovernmentId,
9 IngressId, PersonId, RandomDrawId, RouteId, SchemaRegistry, TerritoryId, TypeSchema,
10 TypedDomainRecordRef,
11};
12pub use canwu_event::{CauseRef, EventAudience, EventKind, SimEvent};
13pub use canwu_knowledge::{
14 ActorKnowledge, ArmyKnowledge, EstimateRange, KnowledgeSnapshot, KnowledgeSource,
15};
16pub use canwu_sim::{
17 ADMISSION_CURSOR_FORMAT_VERSION, ArtifactManifest, BoundaryChange, BoundaryContext,
18 BoundaryDirective, BoundaryEmission, BoundaryEmissionKind, BoundaryIngressGeneration,
19 BoundaryPhase, BoundaryProposal, BoundaryReceipt, BoundaryRecord, BoundaryRequest,
20 BoundarySystemContract, BoundarySystemHandler, CHECKPOINT_JOURNAL_FORMAT_VERSION,
21 COMMITMENT_FORMAT_VERSION, CanwuError, CheckpointJournal, Command, CommandAttemptOutcome,
22 CommandAttemptRecord, CommandAuthority, CommandContext, CommandEnvelope, CommandIngress,
23 CommandOutcome, CommandPolicyContext, CommandReceipt, CommandRecord, CommandRejection,
24 CommandRequest, CommitmentRoots, CompactedSimulation, ControllerPolicy, DecisionOrigin,
25 DemoIds, DomainRecord, DomainRecordChange, DomainRecordClass, DomainRecordDraft,
26 DomainRecordLifecycle, DomainRecordMutation, DomainRecordOperation, DomainRecordSchema,
27 DomainReference, DomainReferenceSchema, DomainReferenceTarget, DomainReferenceTargetKind,
28 ENGINE_VERSION, ErrorCode, EvidenceCursor, EvidenceJournalSegment, IngressClass,
29 IngressPayload, IngressReceipt, IngressRecord, InteractionPolicy, Issuer, ObservationPolicy,
30 PayloadProperty, PayloadSchema, PayloadValueType, PluginActionDescriptor, PluginCommandHandler,
31 PluginDescriptor, PluginIngressDescriptor, PluginIngressRequest, PluginRegistrar,
32 PluginRegistry, RUN_CONFIGURATION_FORMAT_VERSION, RUN_MANIFEST_FORMAT_VERSION, RandomAlgorithm,
33 RandomDrawOutcome, RandomDrawProducer, RandomDrawRecord, RandomStreamKey, RandomStreamState,
34 ReplayJournal, ReservationAllocation, ReservationDisposition, ReservationOffer,
35 ReservationOfferRecord, ReservationPoolKey, ReservationRef, ReservationRequest,
36 ReservationRequestRecord, RunConfiguration, RunConfigurationSnapshot, RunManifest, RunPurpose,
37 SNAPSHOT_FORMAT_VERSION, STATE_REVISION_FORMAT_VERSION, Scenario, SeatBinding, SeatPolicy,
38 SimulationCheckpoint, SimulationPlugin, SimulationSnapshot, SimulationSystemHandler,
39 SimulationView, StateKey, StateVisibility, SystemCadence, SystemContract, SystemDirective,
40 TracePolicy,
41};
42pub use canwu_time::{SimDuration, SimTime};
43pub use canwu_world::{
44 Army, Government, MapPoint, Person, Route, Territory, TransitState, WorldDiff, WorldSnapshot,
45};
46
47use canwu_sim::Simulation;
48use serde::{Deserialize, Serialize};
49use serde_json::{Value, json};
50use std::collections::BTreeMap;
51
52pub struct Canwu {
54 simulation: Simulation,
55}
56
57pub struct CompactedCanwu {
59 simulation: CompactedSimulation,
60}
61
62impl Canwu {
63 #[must_use]
64 pub const fn version() -> &'static str {
65 ENGINE_VERSION
66 }
67
68 pub fn new(seed: u64, scenario: Scenario) -> Result<Self, CanwuError> {
69 Ok(Self {
70 simulation: Simulation::new(seed, scenario)?,
71 })
72 }
73
74 pub fn into_compacted(self) -> Result<CompactedCanwu, CanwuError> {
76 Ok(CompactedCanwu {
77 simulation: self.simulation.into_compacted()?,
78 })
79 }
80
81 pub fn new_with_plugins(
82 seed: u64,
83 scenario: Scenario,
84 plugins: &[&dyn SimulationPlugin],
85 ) -> Result<Self, CanwuError> {
86 Ok(Self {
87 simulation: Simulation::new_with_plugins(seed, scenario, plugins)?,
88 })
89 }
90
91 pub fn new_with_manifest(
92 seed: u64,
93 scenario: Scenario,
94 run_manifest: RunManifest,
95 ) -> Result<Self, CanwuError> {
96 Ok(Self {
97 simulation: Simulation::new_with_manifest(seed, scenario, run_manifest)?,
98 })
99 }
100
101 pub fn new_with_manifest_and_plugins(
102 seed: u64,
103 scenario: Scenario,
104 run_manifest: RunManifest,
105 plugins: &[&dyn SimulationPlugin],
106 ) -> Result<Self, CanwuError> {
107 Ok(Self {
108 simulation: Simulation::new_with_manifest_and_plugins(
109 seed,
110 scenario,
111 run_manifest,
112 plugins,
113 )?,
114 })
115 }
116
117 pub fn new_with_run_configuration(
118 seed: u64,
119 scenario: Scenario,
120 run_manifest: RunManifest,
121 run_configuration: RunConfiguration,
122 ) -> Result<Self, CanwuError> {
123 Ok(Self {
124 simulation: Simulation::new_with_run_configuration(
125 seed,
126 scenario,
127 run_manifest,
128 run_configuration,
129 )?,
130 })
131 }
132
133 pub fn new_with_run_configuration_and_plugins(
134 seed: u64,
135 scenario: Scenario,
136 run_manifest: RunManifest,
137 run_configuration: RunConfiguration,
138 plugins: &[&dyn SimulationPlugin],
139 ) -> Result<Self, CanwuError> {
140 Ok(Self {
141 simulation: Simulation::new_with_run_configuration_and_plugins(
142 seed,
143 scenario,
144 run_manifest,
145 run_configuration,
146 plugins,
147 )?,
148 })
149 }
150
151 pub fn demo(seed: u64) -> Result<Self, CanwuError> {
152 let (simulation, _) = Simulation::demo(seed)?;
153 Ok(Self { simulation })
154 }
155
156 #[must_use]
157 pub fn demo_ids() -> DemoIds {
158 let (_, ids) = canwu_sim::demo_scenario();
159 ids
160 }
161
162 #[must_use]
163 pub const fn time(&self) -> SimTime {
164 self.simulation.time()
165 }
166
167 #[must_use]
168 pub const fn run_manifest(&self) -> &RunManifest {
169 self.simulation.run_manifest()
170 }
171
172 #[must_use]
173 pub const fn run_configuration(&self) -> &RunConfigurationSnapshot {
174 self.simulation.run_configuration()
175 }
176
177 #[must_use]
178 pub fn revision(&self) -> u64 {
185 self.simulation.revision()
186 }
187
188 #[must_use]
189 pub fn run_manifest_hash(&self) -> &str {
190 self.simulation.run_manifest_hash()
191 }
192
193 #[must_use]
194 pub fn checkpoint_hash(&self) -> &str {
195 self.simulation.checkpoint_hash()
196 }
197
198 pub fn authoritative_state_hash(&self) -> Result<String, CanwuError> {
199 self.simulation.authoritative_state_hash()
200 }
201
202 #[must_use]
203 pub fn world(&self) -> WorldSnapshot {
204 self.simulation.world()
205 }
206
207 #[must_use]
208 pub fn knowledge(&self) -> &KnowledgeSnapshot {
209 self.simulation.knowledge()
210 }
211
212 #[must_use]
213 pub fn events(&self) -> &[SimEvent] {
214 self.simulation.events()
215 }
216
217 #[must_use]
218 pub fn commands(&self) -> &[CommandRecord] {
219 self.simulation.command_log()
220 }
221
222 #[must_use]
223 pub fn boundaries(&self) -> &[BoundaryRecord] {
224 self.simulation.boundaries()
225 }
226
227 #[must_use]
228 pub fn command_attempts(&self) -> &[CommandAttemptRecord] {
229 self.simulation.command_attempts()
230 }
231
232 #[must_use]
233 pub fn ingress_log(&self) -> &[IngressRecord] {
234 self.simulation.ingress_log()
235 }
236
237 #[must_use]
238 pub fn domain_record(&self, reference: &DomainRecordRef) -> Option<&DomainRecord> {
239 self.simulation.domain_record(reference)
240 }
241
242 #[must_use]
243 pub fn typed_domain_record<T: DomainRecordType>(
244 &self,
245 reference: &TypedDomainRecordRef<T>,
246 ) -> Option<&DomainRecord> {
247 self.simulation.typed_domain_record(reference)
248 }
249
250 pub fn domain_records(&self) -> impl Iterator<Item = &DomainRecord> {
251 self.simulation.domain_records()
252 }
253
254 #[must_use]
255 pub fn random_draws(&self) -> &[RandomDrawRecord] {
256 self.simulation.random_draws()
257 }
258
259 #[must_use]
260 pub fn boundary_head_hash(&self) -> Option<&str> {
261 self.simulation.boundary_head_hash()
262 }
263
264 #[must_use]
265 pub const fn schema(&self) -> &SchemaRegistry {
266 self.simulation.schema()
267 }
268
269 pub fn plugin_descriptors(&self) -> impl Iterator<Item = &PluginDescriptor> {
270 self.simulation.plugin_descriptors()
271 }
272
273 #[must_use]
274 pub fn replay_journal(&self) -> ReplayJournal {
275 self.simulation.replay_journal()
276 }
277
278 pub fn evidence_cursor(&self) -> Result<EvidenceCursor, CanwuError> {
279 self.simulation.evidence_cursor()
280 }
281
282 pub fn checkpoint(&self) -> Result<SimulationCheckpoint, CanwuError> {
283 self.simulation.checkpoint()
284 }
285
286 pub fn journal_segment_since(
287 &self,
288 start: EvidenceCursor,
289 ) -> Result<EvidenceJournalSegment, CanwuError> {
290 self.simulation.journal_segment_since(start)
291 }
292
293 pub fn checkpoint_journal(&self) -> Result<CheckpointJournal, CanwuError> {
294 self.simulation.checkpoint_journal()
295 }
296
297 pub fn checkpoint_journal_json(&self) -> Result<String, CanwuError> {
298 self.simulation.checkpoint_journal_json()
299 }
300
301 pub fn register_plugin<P: SimulationPlugin + ?Sized>(
302 &mut self,
303 plugin: &P,
304 ) -> Result<(), CanwuError> {
305 self.simulation.register_plugin(plugin)
306 }
307
308 pub fn submit(&mut self, command: CommandEnvelope) -> Result<CommandReceipt, CanwuError> {
309 self.simulation.submit(command)
310 }
311
312 pub fn process_command(
313 &mut self,
314 request: CommandRequest,
315 ) -> Result<CommandOutcome, CanwuError> {
316 self.simulation.process_command(request)
317 }
318
319 pub fn enqueue_command(
320 &mut self,
321 due_at: SimTime,
322 priority: i32,
323 request: CommandRequest,
324 ) -> Result<IngressReceipt, CanwuError> {
325 self.simulation.enqueue_command(due_at, priority, request)
326 }
327
328 pub fn enqueue_plugin_ingress(
329 &mut self,
330 request: PluginIngressRequest,
331 ) -> Result<IngressReceipt, CanwuError> {
332 self.simulation.enqueue_plugin_ingress(request)
333 }
334
335 pub fn schedule_calendar_boundary(
336 &mut self,
337 due_at: SimTime,
338 cadences: Vec<SystemCadence>,
339 ) -> Result<IngressReceipt, CanwuError> {
340 self.simulation.schedule_calendar_boundary(due_at, cadences)
341 }
342
343 pub fn advance_canonical(
344 &mut self,
345 duration: SimDuration,
346 ) -> Result<Vec<BoundaryReceipt>, CanwuError> {
347 self.simulation.advance_canonical(duration)
348 }
349
350 pub fn step_canonical(&mut self) -> Result<Option<BoundaryReceipt>, CanwuError> {
351 self.simulation.step_canonical()
352 }
353
354 pub fn advance(&mut self, duration: SimDuration) -> Result<Vec<SimEvent>, CanwuError> {
355 self.simulation.advance(duration)
356 }
357
358 pub fn settle_boundary(
359 &mut self,
360 request: BoundaryRequest,
361 ) -> Result<BoundaryReceipt, CanwuError> {
362 self.simulation.settle_boundary(request)
363 }
364
365 pub fn wait(&mut self, duration: SimDuration) -> Result<Vec<SimEvent>, CanwuError> {
366 self.advance(duration)
367 }
368
369 pub fn step(&mut self) -> Result<Vec<SimEvent>, CanwuError> {
370 self.simulation.step()
371 }
372
373 #[must_use]
374 pub fn snapshot(&self) -> SimulationSnapshot {
375 self.simulation.snapshot()
376 }
377
378 pub fn snapshot_json(&self) -> Result<String, CanwuError> {
379 self.simulation.snapshot_json()
380 }
381
382 pub fn from_snapshot_json(json: &str) -> Result<Self, CanwuError> {
383 Ok(Self {
384 simulation: Simulation::from_snapshot_json(json)?,
385 })
386 }
387
388 pub fn from_snapshot_json_with_plugins(
389 json: &str,
390 plugins: &[&dyn SimulationPlugin],
391 ) -> Result<Self, CanwuError> {
392 Ok(Self {
393 simulation: Simulation::from_snapshot_json_with_plugins(json, plugins)?,
394 })
395 }
396
397 pub fn from_checkpoint_and_journal(
398 checkpoint: SimulationCheckpoint,
399 segments: Vec<EvidenceJournalSegment>,
400 ) -> Result<Self, CanwuError> {
401 Ok(Self {
402 simulation: Simulation::from_checkpoint_and_journal(checkpoint, segments)?,
403 })
404 }
405
406 pub fn from_checkpoint_journal(bundle: CheckpointJournal) -> Result<Self, CanwuError> {
407 Ok(Self {
408 simulation: Simulation::from_checkpoint_journal(bundle)?,
409 })
410 }
411
412 pub fn from_checkpoint_journal_with_plugins(
413 bundle: CheckpointJournal,
414 plugins: &[&dyn SimulationPlugin],
415 ) -> Result<Self, CanwuError> {
416 Ok(Self {
417 simulation: Simulation::from_checkpoint_journal_with_plugins(bundle, plugins)?,
418 })
419 }
420
421 pub fn from_checkpoint_journal_json(json: &str) -> Result<Self, CanwuError> {
422 Ok(Self {
423 simulation: Simulation::from_checkpoint_journal_json(json)?,
424 })
425 }
426
427 pub fn from_checkpoint_journal_json_with_plugins(
428 json: &str,
429 plugins: &[&dyn SimulationPlugin],
430 ) -> Result<Self, CanwuError> {
431 Ok(Self {
432 simulation: Simulation::from_checkpoint_journal_json_with_plugins(json, plugins)?,
433 })
434 }
435
436 pub fn replay(
437 seed: u64,
438 scenario: Scenario,
439 commands: &[CommandRecord],
440 final_time: SimTime,
441 ) -> Result<Self, CanwuError> {
442 Ok(Self {
443 simulation: Simulation::replay(seed, scenario, commands, final_time)?,
444 })
445 }
446
447 pub fn replay_with_plugins(
448 seed: u64,
449 scenario: Scenario,
450 plugins: &[&dyn SimulationPlugin],
451 commands: &[CommandRecord],
452 final_time: SimTime,
453 ) -> Result<Self, CanwuError> {
454 Ok(Self {
455 simulation: Simulation::replay_with_plugins(
456 seed, scenario, plugins, commands, final_time,
457 )?,
458 })
459 }
460
461 pub fn replay_with_boundaries(
462 seed: u64,
463 scenario: Scenario,
464 plugins: &[&dyn SimulationPlugin],
465 commands: &[CommandRecord],
466 boundaries: &[BoundaryRecord],
467 final_time: SimTime,
468 ) -> Result<Self, CanwuError> {
469 Ok(Self {
470 simulation: Simulation::replay_with_boundaries(
471 seed, scenario, plugins, commands, boundaries, final_time,
472 )?,
473 })
474 }
475
476 pub fn replay_with_run_manifest(
477 seed: u64,
478 scenario: Scenario,
479 run_manifest: RunManifest,
480 plugins: &[&dyn SimulationPlugin],
481 commands: &[CommandRecord],
482 boundaries: &[BoundaryRecord],
483 final_time: SimTime,
484 ) -> Result<Self, CanwuError> {
485 Ok(Self {
486 simulation: Simulation::replay_with_run_manifest(
487 seed,
488 scenario,
489 run_manifest,
490 plugins,
491 commands,
492 boundaries,
493 final_time,
494 )?,
495 })
496 }
497
498 #[allow(clippy::too_many_arguments)]
499 pub fn replay_with_run_configuration(
500 seed: u64,
501 scenario: Scenario,
502 run_manifest: RunManifest,
503 run_configuration: RunConfiguration,
504 plugins: &[&dyn SimulationPlugin],
505 commands: &[CommandRecord],
506 command_attempts: &[CommandAttemptRecord],
507 boundaries: &[BoundaryRecord],
508 final_time: SimTime,
509 ) -> Result<Self, CanwuError> {
510 Ok(Self {
511 simulation: Simulation::replay_with_run_configuration(
512 seed,
513 scenario,
514 run_manifest,
515 run_configuration,
516 plugins,
517 commands,
518 command_attempts,
519 boundaries,
520 final_time,
521 )?,
522 })
523 }
524
525 pub fn replay_from_journal(
526 scenario: Scenario,
527 plugins: &[&dyn SimulationPlugin],
528 journal: &ReplayJournal,
529 ) -> Result<Self, CanwuError> {
530 Ok(Self {
531 simulation: Simulation::replay_from_journal(scenario, plugins, journal)?,
532 })
533 }
534
535 #[must_use]
536 pub fn fork(&self) -> Self {
537 Self {
538 simulation: self.simulation.fork(),
539 }
540 }
541
542 #[must_use]
543 pub fn diff(&self, other: &Self) -> WorldDiff {
544 WorldDiff::between(&self.world(), &other.world())
545 }
546
547 #[must_use]
548 pub fn query(&self, query: &Query) -> QueryResult {
549 run_query(&self.world(), self.events(), query)
550 }
551
552 pub fn query_as(&self, actor: PersonId, query: &Query) -> Result<QueryResult, CanwuError> {
553 if self.world().person(actor).is_none() {
554 return Err(CanwuError::new(
555 ErrorCode::ActorNotFound,
556 format!("actor {actor} was not found"),
557 ));
558 }
559 Ok(run_actor_query(
560 &self.world(),
561 actor,
562 self.knowledge().for_actor(actor),
563 query,
564 ))
565 }
566
567 pub fn viewer_context(&self, actor: PersonId) -> Result<ViewerContext, CanwuError> {
573 if self.world().person(actor).is_none() {
574 return Err(CanwuError::new(
575 ErrorCode::ActorNotFound,
576 format!("actor {actor} was not found"),
577 ));
578 }
579 let observation = match self.run_configuration().declared() {
580 Some(configuration) => {
581 if configuration.observation == ObservationPolicy::ActorBound
582 && configuration
583 .seat_binding
584 .as_ref()
585 .and_then(|binding| binding.actor)
586 != Some(actor)
587 {
588 return Err(CanwuError::new(
589 ErrorCode::InvalidAuthority,
590 format!("actor {actor} is not bound to the active observation seat"),
591 ));
592 }
593 configuration.observation
594 }
595 None => ObservationPolicy::ActorBound,
596 };
597 Ok(ViewerContext { actor, observation })
598 }
599
600 pub fn observe(
601 &self,
602 actor: PersonId,
603 request: &ObserveRequest,
604 ) -> Result<AgentContext, CanwuError> {
605 let viewer = self.viewer_context(actor)?;
606 self.observe_with_viewer(&viewer, request)
607 }
608
609 pub fn observe_with_viewer(
615 &self,
616 viewer: &ViewerContext,
617 request: &ObserveRequest,
618 ) -> Result<AgentContext, CanwuError> {
619 let authorized = self.viewer_context(viewer.actor)?;
620 if authorized != *viewer {
621 return Err(CanwuError::new(
622 ErrorCode::InvalidAuthority,
623 format!(
624 "actor {} is not authorized for this observation context",
625 viewer.actor
626 ),
627 ));
628 }
629 let actor = viewer.actor;
630 let world = self.world();
631 let person = world.person(actor).ok_or_else(|| {
632 CanwuError::new(
633 ErrorCode::ActorNotFound,
634 format!("actor {actor} was not found"),
635 )
636 })?;
637 let knowledge = self.knowledge().for_actor(actor);
638 let known_armies = match knowledge {
639 Some(records) => records
640 .armies
641 .values()
642 .map(|record| known_army_view(self.time(), record))
643 .collect::<Result<Vec<_>, _>>()?,
644 None => Vec::new(),
645 };
646 let changes_since = request.since.map_or_else(Vec::new, |since| {
647 self.events()
648 .iter()
649 .filter(|event| event.timestamp > since)
650 .filter_map(|event| {
651 let audience = self.simulation.event_audience(event);
652 visible_change(viewer, event, &audience)
653 })
654 .collect()
655 });
656 let pending_actions = world
657 .armies
658 .iter()
659 .filter(|army| army.commander == actor)
660 .filter_map(|army| {
661 army.transit.as_ref().map(|transit| PendingCommitment {
662 summary: format!(
663 "{} is moving from {} to {}",
664 army.name, transit.from, transit.to
665 ),
666 due_at: transit.arrives_at,
667 })
668 })
669 .collect();
670 Ok(AgentContext {
671 identity: AgentIdentity {
672 person: person.id,
673 name: person.name.clone(),
674 roles: person.roles.clone(),
675 },
676 current_time: self.time(),
677 current_location: person.current_location,
678 focus: request.focus.clone(),
679 known_armies,
680 changes_since,
681 pending_actions,
682 available_actions: self.available_actions(actor)?,
683 })
684 }
685
686 pub fn inspect(
687 &self,
688 actor: PersonId,
689 entity: &EntityRef,
690 detail: DetailLevel,
691 ) -> Result<Inspection, CanwuError> {
692 let world = self.world();
693 let actor_state = world.person(actor).ok_or_else(|| {
694 CanwuError::new(
695 ErrorCode::ActorNotFound,
696 format!("actor {actor} was not found"),
697 )
698 })?;
699 let fields = match entity {
700 EntityRef::Army(army_id) => {
701 let record = self
702 .knowledge()
703 .for_actor(actor)
704 .and_then(|knowledge| knowledge.armies.get(army_id));
705 let Some(record) = record else {
706 return Ok(Inspection {
707 entity: entity.clone(),
708 detail,
709 summary: "No reliable information is available about this army".to_owned(),
710 fields: BTreeMap::new(),
711 });
712 };
713 let mut fields = BTreeMap::from([
714 ("known_name".to_owned(), json!(record.known_name)),
715 ("known_location".to_owned(), json!(record.known_location)),
716 (
717 "estimated_strength".to_owned(),
718 json!(record.estimated_strength),
719 ),
720 ("observed_at".to_owned(), json!(record.observed_at)),
721 (
722 "confidence_per_mille".to_owned(),
723 json!(record.confidence_per_mille),
724 ),
725 ]);
726 if matches!(detail, DetailLevel::RawFields) {
727 fields.insert("source".to_owned(), json!(record.source));
728 fields.insert("learned_at".to_owned(), json!(record.learned_at));
729 }
730 fields
731 }
732 EntityRef::Person(person_id) => {
733 if *person_id != actor_state.id {
734 return Ok(no_knowledge_inspection(entity, detail));
735 }
736 let Some(person) = world.person(*person_id) else {
737 return Ok(missing_inspection(entity, detail));
738 };
739 BTreeMap::from([
740 ("name".to_owned(), json!(person.name)),
741 ("roles".to_owned(), json!(person.roles)),
742 ("government".to_owned(), json!(person.government)),
743 (
744 "current_location".to_owned(),
745 json!(person.current_location),
746 ),
747 ])
748 }
749 EntityRef::Territory(_)
750 | EntityRef::Domain(_)
751 | EntityRef::Government(_)
752 | EntityRef::Route(_)
753 | EntityRef::Organization(_)
754 | EntityRef::Resource(_) => return Ok(no_knowledge_inspection(entity, detail)),
755 };
756 Ok(Inspection {
757 entity: entity.clone(),
758 detail,
759 summary: format!("Actor-relative inspection of {entity}"),
760 fields,
761 })
762 }
763
764 pub fn available_actions(&self, actor: PersonId) -> Result<Vec<AvailableAction>, CanwuError> {
765 let world = self.world();
766 if world.person(actor).is_none() {
767 return Err(CanwuError::new(
768 ErrorCode::ActorNotFound,
769 format!("actor {actor} was not found"),
770 ));
771 }
772 let mut actions = Vec::new();
773 for army in world.armies.iter().filter(|army| army.commander == actor) {
774 if army.transit.is_some() {
775 continue;
776 }
777 for route in &world.routes {
778 if let Some(destination) = route.other_end(army.location) {
779 actions.push(AvailableAction {
780 action_type: "move_army".to_owned(),
781 description: format!("Move {} to territory {destination}", army.name),
782 payload: json!({
783 "army": army.id,
784 "destination": destination,
785 }),
786 legal_reason: format!("Actor {actor} commands army {}", army.id),
787 });
788 }
789 }
790 }
791 Ok(actions)
792 }
793
794 pub fn act(
795 &mut self,
796 actor: PersonId,
797 action: SemanticAction,
798 ) -> Result<CommandReceipt, CanwuError> {
799 let command = match action {
800 SemanticAction::MoveArmy { army, destination } => {
801 Command::MoveArmy { army, destination }
802 }
803 SemanticAction::Plugin {
804 plugin,
805 action,
806 payload,
807 } => Command::Plugin {
808 plugin,
809 command: action,
810 payload,
811 },
812 };
813 self.submit(CommandEnvelope::new(Issuer::Actor(actor), command))
814 }
815
816 #[must_use]
817 pub fn explain(&self, request: &ExplanationRequest) -> Explanation {
818 match request {
819 ExplanationRequest::Event(event_id) => self.explain_event(*event_id),
820 ExplanationRequest::ArmyMorale(army_id) => self.explain_army_morale(*army_id),
821 ExplanationRequest::Failure(error) => Explanation {
822 summary: error.message.clone(),
823 causal_chain: vec![ExplanationStep {
824 label: format!("Validation failed: {:?}", error.code),
825 event: None,
826 }],
827 },
828 }
829 }
830
831 #[must_use]
832 pub fn describe_capabilities(&self) -> CapabilityDescription {
833 CapabilityDescription {
834 operations: vec![
835 "observe",
836 "inspect",
837 "query",
838 "available_actions",
839 "act",
840 "explain",
841 "wait",
842 "describe_capabilities",
843 ]
844 .into_iter()
845 .map(str::to_owned)
846 .collect(),
847 notes: vec![
848 "Agent reads are actor-relative and never fall back to ground truth".to_owned(),
849 "All actions become validated commands".to_owned(),
850 "Use progressive inspection detail to control response size".to_owned(),
851 ],
852 plugin_actions: self
853 .plugin_descriptors()
854 .flat_map(|plugin| {
855 plugin
856 .commands
857 .iter()
858 .map(move |action| format!("{}.{}", plugin.name, action.name))
859 })
860 .collect(),
861 }
862 }
863
864 fn explain_event(&self, event_id: EventId) -> Explanation {
865 let mut chain = Vec::new();
866 let events = self.events();
867 let mut current = event_by_id(events, event_id);
868 while let Some(event) = current {
869 chain.push(ExplanationStep {
870 label: event.summary.clone(),
871 event: Some(event.id),
872 });
873 current = match &event.cause {
874 Some(CauseRef::Boundary(boundary)) => {
875 chain.push(ExplanationStep {
876 label: format!("Committed by boundary {boundary}"),
877 event: None,
878 });
879 None
880 }
881 Some(CauseRef::Event(parent)) => event_by_id(events, *parent),
882 Some(CauseRef::Command(command)) => {
883 chain.push(ExplanationStep {
884 label: format!("Accepted command {command}"),
885 event: None,
886 });
887 None
888 }
889 Some(CauseRef::System(system)) => {
890 chain.push(ExplanationStep {
891 label: format!("Produced by system {system}"),
892 event: None,
893 });
894 None
895 }
896 None => None,
897 };
898 }
899 Explanation {
900 summary: chain.first().map_or_else(
901 || "Event was not found".to_owned(),
902 |step| step.label.clone(),
903 ),
904 causal_chain: chain,
905 }
906 }
907
908 fn explain_army_morale(&self, army_id: ArmyId) -> Explanation {
909 let world = self.world();
910 let Some(army) = world.army(army_id) else {
911 return Explanation {
912 summary: format!("Army {army_id} was not found"),
913 causal_chain: Vec::new(),
914 };
915 };
916 let provenance = self.events().iter().rev().find(|event| {
917 matches!(
918 &event.kind,
919 EventKind::DebugFieldChanged { entity: EntityRef::Army(id), field, .. }
920 if *id == army_id && field == "morale"
921 )
922 });
923 provenance.map_or_else(
924 || Explanation {
925 summary: format!(
926 "{} morale is {}; no post-scenario morale-changing event is recorded",
927 army.name, army.morale
928 ),
929 causal_chain: Vec::new(),
930 },
931 |event| self.explain_event(event.id),
932 )
933 }
934}
935
936fn event_by_id(events: &[SimEvent], event_id: EventId) -> Option<&SimEvent> {
937 let index = usize::try_from(event_id.get().checked_sub(1)?).ok()?;
938 events.get(index).filter(|event| event.id == event_id)
939}
940
941impl CompactedCanwu {
942 pub fn from_checkpoint_and_journal(
943 checkpoint: SimulationCheckpoint,
944 segments: Vec<EvidenceJournalSegment>,
945 ) -> Result<Self, CanwuError> {
946 Ok(Self {
947 simulation: CompactedSimulation::from_checkpoint_and_journal(checkpoint, segments)?,
948 })
949 }
950
951 pub fn from_checkpoint_and_journal_with_plugins(
952 checkpoint: SimulationCheckpoint,
953 segments: Vec<EvidenceJournalSegment>,
954 plugins: &[&dyn SimulationPlugin],
955 ) -> Result<Self, CanwuError> {
956 Ok(Self {
957 simulation: CompactedSimulation::from_checkpoint_and_journal_with_plugins(
958 checkpoint, segments, plugins,
959 )?,
960 })
961 }
962
963 pub fn evidence_cursor(&self) -> Result<EvidenceCursor, CanwuError> {
964 self.simulation.evidence_cursor()
965 }
966
967 pub fn checkpoint(&self) -> Result<SimulationCheckpoint, CanwuError> {
968 self.simulation.checkpoint()
969 }
970
971 pub fn seal_evidence(&mut self) -> Result<Option<EvidenceJournalSegment>, CanwuError> {
972 self.simulation.seal_evidence()
973 }
974
975 pub fn snapshot_with_segments(
976 &self,
977 segments: Vec<EvidenceJournalSegment>,
978 ) -> Result<SimulationSnapshot, CanwuError> {
979 self.simulation.snapshot_with_segments(segments)
980 }
981
982 pub fn replay_journal_with_segments(
983 &self,
984 segments: Vec<EvidenceJournalSegment>,
985 ) -> Result<ReplayJournal, CanwuError> {
986 self.simulation.replay_journal_with_segments(segments)
987 }
988
989 #[must_use]
990 pub const fn time(&self) -> SimTime {
991 self.simulation.time()
992 }
993
994 #[must_use]
995 pub const fn revision(&self) -> u64 {
996 self.simulation.revision()
997 }
998
999 #[must_use]
1000 pub fn checkpoint_hash(&self) -> &str {
1001 self.simulation.checkpoint_hash()
1002 }
1003
1004 #[must_use]
1005 pub fn boundary_head_hash(&self) -> Option<&str> {
1006 self.simulation.boundary_head_hash()
1007 }
1008
1009 #[must_use]
1010 pub fn world(&self) -> WorldSnapshot {
1011 self.simulation.world()
1012 }
1013
1014 #[must_use]
1015 pub fn knowledge(&self) -> &KnowledgeSnapshot {
1016 self.simulation.knowledge()
1017 }
1018
1019 #[must_use]
1020 pub fn domain_record(&self, reference: &DomainRecordRef) -> Option<&DomainRecord> {
1021 self.simulation.domain_record(reference)
1022 }
1023
1024 #[must_use]
1025 pub fn typed_domain_record<T: DomainRecordType>(
1026 &self,
1027 reference: &TypedDomainRecordRef<T>,
1028 ) -> Option<&DomainRecord> {
1029 self.simulation.typed_domain_record(reference)
1030 }
1031
1032 pub fn submit(&mut self, envelope: CommandEnvelope) -> Result<CommandReceipt, CanwuError> {
1033 self.simulation.submit(envelope)
1034 }
1035
1036 pub fn process_command(
1037 &mut self,
1038 request: CommandRequest,
1039 ) -> Result<CommandOutcome, CanwuError> {
1040 self.simulation.process_command(request)
1041 }
1042
1043 pub fn enqueue_command(
1044 &mut self,
1045 due_at: SimTime,
1046 priority: i32,
1047 request: CommandRequest,
1048 ) -> Result<IngressReceipt, CanwuError> {
1049 self.simulation.enqueue_command(due_at, priority, request)
1050 }
1051
1052 pub fn enqueue_plugin_ingress(
1053 &mut self,
1054 request: PluginIngressRequest,
1055 ) -> Result<IngressReceipt, CanwuError> {
1056 self.simulation.enqueue_plugin_ingress(request)
1057 }
1058
1059 pub fn schedule_calendar_boundary(
1060 &mut self,
1061 due_at: SimTime,
1062 cadences: Vec<SystemCadence>,
1063 ) -> Result<IngressReceipt, CanwuError> {
1064 self.simulation.schedule_calendar_boundary(due_at, cadences)
1065 }
1066
1067 pub fn advance(&mut self, duration: SimDuration) -> Result<Vec<SimEvent>, CanwuError> {
1068 self.simulation.advance(duration)
1069 }
1070
1071 pub fn advance_canonical(
1072 &mut self,
1073 duration: SimDuration,
1074 ) -> Result<Vec<BoundaryReceipt>, CanwuError> {
1075 self.simulation.advance_canonical(duration)
1076 }
1077
1078 pub fn step_canonical(&mut self) -> Result<Option<BoundaryReceipt>, CanwuError> {
1079 self.simulation.step_canonical()
1080 }
1081
1082 pub fn settle_boundary(
1083 &mut self,
1084 request: BoundaryRequest,
1085 ) -> Result<BoundaryReceipt, CanwuError> {
1086 self.simulation.settle_boundary(request)
1087 }
1088}
1089
1090#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1091#[serde(rename_all = "snake_case")]
1092pub enum QueryEntity {
1093 Person,
1094 Government,
1095 Territory,
1096 Route,
1097 Army,
1098 Event,
1099}
1100
1101#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1102#[serde(rename_all = "snake_case")]
1103pub enum FilterOperator {
1104 Equal,
1105 Contains,
1106}
1107
1108#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1109pub struct QueryFilter {
1110 pub field: String,
1111 pub operator: FilterOperator,
1112 pub value: Value,
1113}
1114
1115#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1116pub struct Query {
1117 pub entity: QueryEntity,
1118 pub filters: Vec<QueryFilter>,
1119 pub select: Vec<String>,
1120 pub limit: usize,
1121}
1122
1123impl Query {
1124 #[must_use]
1125 pub const fn all(entity: QueryEntity) -> Self {
1126 Self {
1127 entity,
1128 filters: Vec::new(),
1129 select: Vec::new(),
1130 limit: 100,
1131 }
1132 }
1133}
1134
1135#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
1136pub struct QueryResult {
1137 pub rows: Vec<BTreeMap<String, Value>>,
1138 pub truncated: bool,
1139}
1140
1141fn run_query(world: &WorldSnapshot, events: &[SimEvent], query: &Query) -> QueryResult {
1142 let rows: Vec<_> = match query.entity {
1143 QueryEntity::Person => world
1144 .people
1145 .iter()
1146 .map(|person| value_to_row(&json!(person)))
1147 .collect(),
1148 QueryEntity::Government => world
1149 .governments
1150 .iter()
1151 .map(|government| value_to_row(&json!(government)))
1152 .collect(),
1153 QueryEntity::Territory => world
1154 .territories
1155 .iter()
1156 .map(|territory| value_to_row(&json!(territory)))
1157 .collect(),
1158 QueryEntity::Route => world
1159 .routes
1160 .iter()
1161 .map(|route| value_to_row(&json!(route)))
1162 .collect(),
1163 QueryEntity::Army => world
1164 .armies
1165 .iter()
1166 .map(|army| value_to_row(&json!(army)))
1167 .collect(),
1168 QueryEntity::Event => events
1169 .iter()
1170 .map(|event| value_to_row(&json!(event)))
1171 .collect(),
1172 };
1173 finalize_query(rows, query)
1174}
1175
1176fn run_actor_query(
1177 world: &WorldSnapshot,
1178 actor: PersonId,
1179 knowledge: Option<&ActorKnowledge>,
1180 query: &Query,
1181) -> QueryResult {
1182 match query.entity {
1183 QueryEntity::Army => {
1184 let rows = knowledge.map_or_else(Vec::new, |knowledge| {
1185 knowledge
1186 .armies
1187 .values()
1188 .map(|record| value_to_row(&json!(record)))
1189 .collect()
1190 });
1191 finalize_query(rows, query)
1192 }
1193 QueryEntity::Person => {
1194 let rows = world
1195 .person(actor)
1196 .map_or_else(Vec::new, |person| vec![value_to_row(&json!(person))]);
1197 finalize_query(rows, query)
1198 }
1199 QueryEntity::Event => QueryResult::default(),
1200 QueryEntity::Government | QueryEntity::Territory | QueryEntity::Route => {
1201 QueryResult::default()
1202 }
1203 }
1204}
1205
1206fn finalize_query(rows: Vec<BTreeMap<String, Value>>, query: &Query) -> QueryResult {
1207 let filtered: Vec<_> = rows
1208 .into_iter()
1209 .filter(|row| {
1210 query
1211 .filters
1212 .iter()
1213 .all(|filter| matches_filter(row, filter))
1214 })
1215 .collect();
1216 let truncated = filtered.len() > query.limit;
1217 let rows = filtered
1218 .into_iter()
1219 .take(query.limit)
1220 .map(|row| select_fields(row, &query.select))
1221 .collect();
1222 QueryResult { rows, truncated }
1223}
1224
1225fn matches_filter(row: &BTreeMap<String, Value>, filter: &QueryFilter) -> bool {
1226 let Some(actual) = row.get(&filter.field) else {
1227 return false;
1228 };
1229 match filter.operator {
1230 FilterOperator::Equal => actual == &filter.value,
1231 FilterOperator::Contains => value_text(actual)
1232 .to_lowercase()
1233 .contains(&value_text(&filter.value).to_lowercase()),
1234 }
1235}
1236
1237fn value_text(value: &Value) -> String {
1238 value
1239 .as_str()
1240 .map_or_else(|| value.to_string(), str::to_owned)
1241}
1242
1243fn select_fields(mut row: BTreeMap<String, Value>, select: &[String]) -> BTreeMap<String, Value> {
1244 if select.is_empty() {
1245 return row;
1246 }
1247 row.retain(|field, _| select.contains(field));
1248 row
1249}
1250
1251fn value_to_row(value: &Value) -> BTreeMap<String, Value> {
1252 value.as_object().map_or_else(BTreeMap::new, |object| {
1253 object
1254 .iter()
1255 .map(|(key, value)| (key.clone(), value.clone()))
1256 .collect()
1257 })
1258}
1259
1260#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1261#[serde(rename_all = "snake_case")]
1262pub enum ObservationFocus {
1263 CurrentSituation,
1264 Military,
1265 Changes,
1266}
1267
1268#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1272pub struct ViewerContext {
1273 actor: PersonId,
1274 observation: ObservationPolicy,
1275}
1276
1277impl ViewerContext {
1278 #[must_use]
1279 pub const fn actor(self) -> PersonId {
1280 self.actor
1281 }
1282
1283 #[must_use]
1284 pub const fn observation(self) -> ObservationPolicy {
1285 self.observation
1286 }
1287}
1288
1289#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1290pub struct ObserveRequest {
1291 pub focus: ObservationFocus,
1292 pub since: Option<SimTime>,
1293}
1294
1295impl Default for ObserveRequest {
1296 fn default() -> Self {
1297 Self {
1298 focus: ObservationFocus::CurrentSituation,
1299 since: None,
1300 }
1301 }
1302}
1303
1304#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1305pub struct AgentIdentity {
1306 pub person: PersonId,
1307 pub name: String,
1308 pub roles: Vec<String>,
1309}
1310
1311#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1312pub struct KnownArmyView {
1313 pub army: ArmyId,
1314 pub name: String,
1315 pub known_location: Option<TerritoryId>,
1316 pub estimated_strength: EstimateRange,
1317 pub information_age_minutes: i64,
1318 pub confidence_per_mille: u16,
1319 pub source: KnowledgeSource,
1320}
1321
1322fn known_army_view(now: SimTime, record: &ArmyKnowledge) -> Result<KnownArmyView, CanwuError> {
1323 let information_age = now.checked_sub(record.observed_at).ok_or_else(|| {
1324 CanwuError::new(
1325 ErrorCode::InvalidDuration,
1326 "knowledge age exceeds the supported simulation-duration range",
1327 )
1328 })?;
1329 Ok(KnownArmyView {
1330 army: record.army,
1331 name: record
1332 .known_name
1333 .clone()
1334 .unwrap_or_else(|| format!("Army {}", record.army)),
1335 known_location: record.known_location,
1336 estimated_strength: record.estimated_strength,
1337 information_age_minutes: information_age.as_minutes(),
1338 confidence_per_mille: record.confidence_per_mille,
1339 source: record.source.clone(),
1340 })
1341}
1342
1343#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1344pub struct VisibleChange {
1345 pub timestamp: SimTime,
1346 pub summary: String,
1347 pub source_event: EventId,
1348}
1349
1350fn visible_change(
1351 viewer: &ViewerContext,
1352 event: &SimEvent,
1353 plugin_audience: &EventAudience,
1354) -> Option<VisibleChange> {
1355 let actor = viewer.actor;
1356 let visible = match &event.kind {
1357 EventKind::MoveOrdered { .. } => {
1358 event.affected_entities.contains(&EntityRef::Person(actor))
1359 }
1360 EventKind::KnowledgeUpdated { recipient, .. } => *recipient == actor,
1361 EventKind::ArmyArrived { .. }
1362 | EventKind::ReportDispatched { .. }
1363 | EventKind::DebugFieldChanged { .. } => matches!(
1364 viewer.observation,
1365 ObservationPolicy::ResearchFull | ObservationPolicy::DeveloperDiagnostic
1366 ),
1367 EventKind::Plugin { .. } => event_visible_to(viewer, event, plugin_audience),
1368 };
1369 visible.then(|| VisibleChange {
1370 timestamp: event.timestamp,
1371 summary: event.summary.clone(),
1372 source_event: event.id,
1373 })
1374}
1375
1376fn event_visible_to(viewer: &ViewerContext, event: &SimEvent, audience: &EventAudience) -> bool {
1377 if matches!(
1378 viewer.observation,
1379 ObservationPolicy::ResearchFull | ObservationPolicy::DeveloperDiagnostic
1380 ) {
1381 return true;
1382 }
1383 match audience {
1384 EventAudience::Public => true,
1385 EventAudience::Actor(actor) => *actor == viewer.actor,
1386 EventAudience::Actors(actors) => actors.binary_search(&viewer.actor).is_ok(),
1387 EventAudience::AffectedActors => event
1388 .affected_entities
1389 .contains(&EntityRef::Person(viewer.actor)),
1390 EventAudience::Private => false,
1391 }
1392}
1393
1394#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1395pub struct PendingCommitment {
1396 pub summary: String,
1397 pub due_at: SimTime,
1398}
1399
1400#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1401pub struct AvailableAction {
1402 pub action_type: String,
1403 pub description: String,
1404 pub payload: Value,
1405 pub legal_reason: String,
1406}
1407
1408#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1409pub struct AgentContext {
1410 pub identity: AgentIdentity,
1411 pub current_time: SimTime,
1412 pub current_location: TerritoryId,
1413 pub focus: ObservationFocus,
1414 pub known_armies: Vec<KnownArmyView>,
1415 pub changes_since: Vec<VisibleChange>,
1416 pub pending_actions: Vec<PendingCommitment>,
1417 pub available_actions: Vec<AvailableAction>,
1418}
1419
1420#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1421#[serde(rename_all = "snake_case")]
1422pub enum DetailLevel {
1423 Summary,
1424 Domain,
1425 Entity,
1426 RawFields,
1427}
1428
1429#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1430pub struct Inspection {
1431 pub entity: EntityRef,
1432 pub detail: DetailLevel,
1433 pub summary: String,
1434 pub fields: BTreeMap<String, Value>,
1435}
1436
1437fn missing_inspection(entity: &EntityRef, detail: DetailLevel) -> Inspection {
1438 Inspection {
1439 entity: entity.clone(),
1440 detail,
1441 summary: format!("{entity} was not found"),
1442 fields: BTreeMap::new(),
1443 }
1444}
1445
1446fn no_knowledge_inspection(entity: &EntityRef, detail: DetailLevel) -> Inspection {
1447 Inspection {
1448 entity: entity.clone(),
1449 detail,
1450 summary: "No actor-scoped knowledge is available for this entity".to_owned(),
1451 fields: BTreeMap::new(),
1452 }
1453}
1454
1455#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1456#[serde(tag = "type", rename_all = "snake_case")]
1457pub enum SemanticAction {
1458 MoveArmy {
1459 army: ArmyId,
1460 destination: TerritoryId,
1461 },
1462 Plugin {
1463 plugin: String,
1464 action: String,
1465 payload: Value,
1466 },
1467}
1468
1469#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1470#[serde(tag = "type", content = "value", rename_all = "snake_case")]
1471pub enum ExplanationRequest {
1472 Event(EventId),
1473 ArmyMorale(ArmyId),
1474 Failure(CanwuError),
1475}
1476
1477#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1478pub struct ExplanationStep {
1479 pub label: String,
1480 pub event: Option<EventId>,
1481}
1482
1483#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1484pub struct Explanation {
1485 pub summary: String,
1486 pub causal_chain: Vec<ExplanationStep>,
1487}
1488
1489#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1490pub struct CapabilityDescription {
1491 pub operations: Vec<String>,
1492 pub notes: Vec<String>,
1493 pub plugin_actions: Vec<String>,
1494}
1495
1496#[cfg(test)]
1497mod tests {
1498 use super::*;
1499
1500 struct VisibilityPlugin {
1501 audience: EventAudience,
1502 }
1503
1504 #[allow(clippy::unnecessary_wraps)]
1505 fn visibility_system(
1506 _view: &SimulationView<'_>,
1507 event: &SimEvent,
1508 ) -> Result<Vec<SystemDirective>, CanwuError> {
1509 if !matches!(event.kind, EventKind::MoveOrdered { .. }) {
1510 return Ok(Vec::new());
1511 }
1512 Ok(vec![SystemDirective::Emit {
1513 event_type: "notice".to_owned(),
1514 summary: "a plugin visibility notice".to_owned(),
1515 affected: vec![EntityRef::Person(PersonId::new(1))],
1516 }])
1517 }
1518
1519 impl SimulationPlugin for VisibilityPlugin {
1520 fn name(&self) -> &'static str {
1521 "visibility-test"
1522 }
1523
1524 fn version(&self) -> &'static str {
1525 "test-v1"
1526 }
1527
1528 fn semantic_hash(&self) -> &'static str {
1529 "0000000000000000000000000000000000000000000000000000000000000001"
1530 }
1531
1532 fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> {
1533 registrar.register_event_audience("notice", self.audience.clone())?;
1534 registrar.register_system(
1535 SystemContract::event_driven(
1536 "emit-notice",
1537 BoundaryPhase::PerspectiveAndReportMaterialization,
1538 ),
1539 visibility_system,
1540 )
1541 }
1542 }
1543
1544 #[test]
1545 fn plugin_event_visibility_respects_public_actor_and_private_audiences() {
1546 let ids = Canwu::demo_ids();
1547 let event = SimEvent {
1548 id: EventId::new(1),
1549 timestamp: SimTime::EPOCH,
1550 kind: EventKind::Plugin {
1551 plugin: "visibility-test".to_owned(),
1552 event_type: "notice".to_owned(),
1553 },
1554 affected_entities: vec![EntityRef::Person(ids.commander)],
1555 summary: "notice".to_owned(),
1556 cause: None,
1557 correlation_id: 1,
1558 };
1559 let actor = ViewerContext {
1560 actor: ids.commander,
1561 observation: ObservationPolicy::ActorBound,
1562 };
1563 let observer = ViewerContext {
1564 actor: ids.observer,
1565 observation: ObservationPolicy::ActorBound,
1566 };
1567 let public_observer = ViewerContext {
1568 actor: ids.observer,
1569 observation: ObservationPolicy::PublicObserver,
1570 };
1571 let research = ViewerContext {
1572 actor: ids.observer,
1573 observation: ObservationPolicy::ResearchFull,
1574 };
1575
1576 assert!(visible_change(&actor, &event, &EventAudience::Public).is_some());
1577 assert!(visible_change(&public_observer, &event, &EventAudience::Public).is_some());
1578 assert!(visible_change(&actor, &event, &EventAudience::Actor(ids.commander)).is_some());
1579 assert!(visible_change(&observer, &event, &EventAudience::Actor(ids.commander)).is_none());
1580 assert!(visible_change(&observer, &event, &EventAudience::Private).is_none());
1581 assert!(visible_change(&research, &event, &EventAudience::Private).is_some());
1582 }
1583
1584 #[test]
1585 fn observe_changes_since_uses_persisted_plugin_audience() {
1586 let ids = Canwu::demo_ids();
1587 let mut canwu = Canwu::demo(35).expect("demo should load");
1588 canwu
1589 .register_plugin(&VisibilityPlugin {
1590 audience: EventAudience::Public,
1591 })
1592 .expect("visibility plugin should register");
1593 let since = SimTime::from_minutes(-1);
1594 canwu
1595 .act(
1596 ids.commander,
1597 SemanticAction::MoveArmy {
1598 army: ids.army,
1599 destination: ids.eastern_territory,
1600 },
1601 )
1602 .expect("movement should emit plugin notice");
1603
1604 let observer = canwu
1605 .observe(
1606 ids.observer,
1607 &ObserveRequest {
1608 focus: ObservationFocus::Changes,
1609 since: Some(since),
1610 },
1611 )
1612 .expect("observer should be authorized");
1613 assert!(
1614 observer
1615 .changes_since
1616 .iter()
1617 .any(|change| change.summary == "a plugin visibility notice")
1618 );
1619
1620 let snapshot_json = canwu
1621 .snapshot_json()
1622 .expect("audience declaration should serialize");
1623 let restored = Canwu::from_snapshot_json_with_plugins(
1624 &snapshot_json,
1625 &[&VisibilityPlugin {
1626 audience: EventAudience::Public,
1627 }],
1628 )
1629 .expect("audience declaration should survive snapshot loading");
1630 let restored_observer = restored
1631 .observe(
1632 ids.observer,
1633 &ObserveRequest {
1634 focus: ObservationFocus::Changes,
1635 since: Some(since),
1636 },
1637 )
1638 .expect("restored observer should be authorized");
1639 assert!(
1640 restored_observer
1641 .changes_since
1642 .iter()
1643 .any(|change| change.summary == "a plugin visibility notice")
1644 );
1645 }
1646
1647 #[test]
1648 fn observe_with_viewer_revalidates_input_control_context() {
1649 let ids = Canwu::demo_ids();
1650 let canwu = Canwu::demo(35).expect("demo should load");
1651 let escalated = ViewerContext {
1652 actor: ids.observer,
1653 observation: ObservationPolicy::ResearchFull,
1654 };
1655
1656 let error = canwu
1657 .observe_with_viewer(&escalated, &ObserveRequest::default())
1658 .expect_err("a caller cannot self-escalate the observation policy");
1659 assert_eq!(error.code, ErrorCode::InvalidAuthority);
1660 }
1661
1662 #[test]
1663 fn actor_relative_observation_does_not_leak_arrival() {
1664 let mut canwu = Canwu::demo(35).expect("demo should load");
1665 let ids = Canwu::demo_ids();
1666 canwu
1667 .act(
1668 ids.commander,
1669 SemanticAction::MoveArmy {
1670 army: ids.army,
1671 destination: ids.eastern_territory,
1672 },
1673 )
1674 .expect("commander can move army");
1675 canwu
1676 .advance(SimDuration::days(1))
1677 .expect("arrival should execute");
1678
1679 assert_eq!(
1680 canwu.world().army(ids.army).expect("army exists").location,
1681 ids.eastern_territory
1682 );
1683 let observer = canwu
1684 .observe(ids.observer, &ObserveRequest::default())
1685 .expect("observer exists");
1686 assert_eq!(
1687 observer.known_armies[0].known_location,
1688 Some(ids.central_territory)
1689 );
1690 let person_rows = canwu
1691 .query_as(ids.observer, &Query::all(QueryEntity::Person))
1692 .expect("actor query should succeed");
1693 assert_eq!(person_rows.rows.len(), 1);
1694 assert_eq!(person_rows.rows[0].get("id"), Some(&json!(ids.observer)));
1695 for entity in [
1696 QueryEntity::Government,
1697 QueryEntity::Territory,
1698 QueryEntity::Route,
1699 ] {
1700 assert!(
1701 canwu
1702 .query_as(ids.observer, &Query::all(entity))
1703 .expect("actor query should succeed")
1704 .rows
1705 .is_empty()
1706 );
1707 }
1708 assert!(
1709 canwu
1710 .inspect(
1711 ids.observer,
1712 &EntityRef::Person(ids.commander),
1713 DetailLevel::RawFields,
1714 )
1715 .expect("inspection should succeed")
1716 .fields
1717 .is_empty()
1718 );
1719 assert!(
1720 canwu
1721 .inspect(
1722 ids.observer,
1723 &EntityRef::Territory(ids.eastern_territory),
1724 DetailLevel::RawFields,
1725 )
1726 .expect("inspection should succeed")
1727 .fields
1728 .is_empty()
1729 );
1730
1731 canwu
1732 .advance(SimDuration::days(3))
1733 .expect("report should arrive");
1734 let updated = canwu
1735 .observe(ids.observer, &ObserveRequest::default())
1736 .expect("observer exists");
1737 assert_eq!(
1738 updated.known_armies[0].known_location,
1739 Some(ids.eastern_territory)
1740 );
1741 }
1742
1743 #[test]
1744 fn debug_mutation_uses_validated_command_and_provenance() {
1745 let mut canwu = Canwu::demo(35).expect("demo should load");
1746 let ids = Canwu::demo_ids();
1747 let result = canwu.submit(CommandEnvelope::new(
1748 Issuer::Debug,
1749 Command::DebugSetArmyMorale {
1750 army: ids.army,
1751 morale: 37,
1752 },
1753 ));
1754 let receipt = result.expect("debug command should validate");
1755 assert_eq!(
1756 canwu.world().army(ids.army).expect("army exists").morale,
1757 37
1758 );
1759 let explanation = canwu.explain(&ExplanationRequest::Event(receipt.emitted_events[0]));
1760 assert!(explanation.causal_chain.len() >= 2);
1761 }
1762
1763 #[test]
1764 fn public_checkpoint_journal_round_trip_is_exact() {
1765 let mut canwu = Canwu::demo(35).expect("demo should load");
1766 let ids = Canwu::demo_ids();
1767 canwu
1768 .submit(CommandEnvelope::new(
1769 Issuer::Actor(ids.commander),
1770 Command::MoveArmy {
1771 army: ids.army,
1772 destination: ids.eastern_territory,
1773 },
1774 ))
1775 .expect("movement should be accepted");
1776 canwu
1777 .advance(SimDuration::days(1))
1778 .expect("scheduled work should execute");
1779
1780 let checkpoint = canwu.checkpoint().expect("current state should checkpoint");
1781 assert!(checkpoint.state.events.is_empty());
1782 assert_eq!(
1783 checkpoint.journal_end,
1784 canwu
1785 .evidence_cursor()
1786 .expect("journal cursor should be representable")
1787 );
1788 let json = canwu
1789 .checkpoint_journal_json()
1790 .expect("checkpoint journal should serialize");
1791 let restored = Canwu::from_checkpoint_journal_json(&json)
1792 .expect("checkpoint journal should restore through the public facade");
1793 assert_eq!(restored.snapshot(), canwu.snapshot());
1794
1795 canwu
1796 .settle_boundary(BoundaryRequest::at(canwu.time()))
1797 .expect("a public boundary should complete the live evidence tail");
1798 let expected = canwu.snapshot();
1799 let mut compact = canwu
1800 .into_compacted()
1801 .expect("the public facade should enter compact mode");
1802 let segment = compact
1803 .seal_evidence()
1804 .expect("the public compact facade should seal evidence")
1805 .expect("the public compact facade should return a segment");
1806 let compact_checkpoint = compact
1807 .checkpoint()
1808 .expect("the public compact facade should checkpoint");
1809 assert_eq!(
1810 compact
1811 .snapshot_with_segments(vec![segment.clone()])
1812 .expect("the public compact facade should reconstruct its snapshot"),
1813 expected
1814 );
1815 let restored_compact =
1816 CompactedCanwu::from_checkpoint_and_journal(compact_checkpoint, vec![segment])
1817 .expect("the public compact facade should restore from its archive");
1818 assert_eq!(
1819 restored_compact
1820 .snapshot_with_segments(Vec::new())
1821 .expect("the restored compact facade should retain validated evidence"),
1822 expected
1823 );
1824 }
1825}