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