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