Skip to main content

canwu_api/
lib.rs

1//! Public programmatic, query, semantic-agent, explanation, and debug interfaces.
2
3#![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
52/// Main in-process API. All returned world values are detached snapshots.
53pub struct Canwu {
54    simulation: Simulation,
55}
56
57/// Public facade for a live runtime whose sealed evidence segments are stored by the caller.
58pub 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    /// Enters the explicit compact-journal interface without discarding evidence.
75    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    /// Returns the persisted authoritative transaction revision.
179    ///
180    /// Accepted commands, persisted expected rejections, and completed
181    /// settlement boundaries each advance it exactly once. Failed work, exact
182    /// retries, bare clock movement, queued but unadmitted ingress, and plugin
183    /// setup do not advance it; combine it with command expected-time guards.
184    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    /// Builds an authorized viewer context from the persisted run policy.
568    ///
569    /// Callers cannot select a stronger observation policy through an
570    /// observation request; the run configuration is the input-control
571    /// boundary for actor-relative versus research projections.
572    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    /// Projects the simulation for a previously authorized viewer context.
610    ///
611    /// The context controls only the player-facing projection. Plugin system
612    /// subscriptions and state read permissions remain enforced by the
613    /// simulation runtime and are not widened by this method.
614    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/// An observation identity authorized by the run's persisted observation
1269/// policy. This type is intentionally constructed through
1270/// [`Canwu::viewer_context`] so an observation request cannot self-escalate.
1271#[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}