Skip to main content

canwu_sim/runtime/
view.rs

1use super::{
2    ActorKnowledge, Army, ArmyId, BoundaryId, BoundaryKnowledgeChange, CanwuError, CommandId,
3    CommandRecord, DomainRecord, DomainRecordKind, DomainRecordRef, DomainRecordType,
4    DomainRecordVersionRef, EntityRef, ErrorCode, EventId, EvidenceRef, Government, GovernmentId,
5    HashSet, IngressId, IngressPayload, IngressRecord, KnowledgeHolderRef, KnowledgeQuery,
6    KnowledgeRecord, KnowledgeRecordId, Person, PersonId, PluginComponentKey,
7    PluginComponentRecord, RandomOperationTarget, RandomStreamKey, RefCell, ReservationAllocation,
8    ReservationRef, Route, RouteId, RuntimeCurrentState, RuntimeEvidence, RuntimeState, SimEvent,
9    SimTime, StateKey, Territory, TerritoryId, TypedDomainRecordRef, Value, component_key, random,
10    records, validation,
11};
12use std::collections::{BTreeMap, BTreeSet};
13
14pub(super) enum SimulationViewState<'a> {
15    Runtime(&'a RuntimeState),
16    Boundary {
17        current: &'a RuntimeCurrentState,
18        now: SimTime,
19        runtime: &'a RuntimeState,
20    },
21}
22
23impl SimulationViewState<'_> {
24    const fn current(&self) -> &RuntimeCurrentState {
25        match self {
26            Self::Runtime(state) => &state.current,
27            Self::Boundary { current, .. } => current,
28        }
29    }
30
31    const fn now(&self) -> SimTime {
32        match self {
33            Self::Runtime(state) => state.scheduler.now,
34            Self::Boundary { now, .. } => *now,
35        }
36    }
37
38    const fn evidence(&self) -> &RuntimeEvidence {
39        match self {
40            Self::Runtime(state) => &state.evidence,
41            Self::Boundary { runtime, .. } => &runtime.evidence,
42        }
43    }
44
45    const fn runtime(&self) -> &RuntimeState {
46        match self {
47            Self::Runtime(state) | Self::Boundary { runtime: state, .. } => state,
48        }
49    }
50}
51
52pub struct SimulationView<'a> {
53    pub(super) state: SimulationViewState<'a>,
54    pub(super) state_owners: &'a BTreeMap<StateKey, String>,
55    pub(super) reader: Option<&'a str>,
56    pub(super) allowed_reads: Option<&'a [StateKey]>,
57    pub(super) allowed_ingress: Option<&'a HashSet<IngressId>>,
58    pub(super) ingress_plugin: Option<&'a str>,
59    pub(super) component_overlay: Option<&'a BTreeMap<PluginComponentKey, PluginComponentRecord>>,
60    pub(super) proposed_components: Option<&'a BTreeMap<PluginComponentKey, PluginComponentRecord>>,
61    pub(super) record_overlay: Option<&'a BTreeMap<DomainRecordRef, DomainRecord>>,
62    pub(super) proposed_records: Option<&'a BTreeMap<DomainRecordRef, DomainRecord>>,
63    pub(super) boundary_id: Option<BoundaryId>,
64    pub(super) proposal_evidence: Option<&'a BTreeSet<EvidenceRef>>,
65    pub(super) knowledge_overlay:
66        Option<&'a BTreeMap<KnowledgeHolderRef, BTreeMap<KnowledgeRecordId, KnowledgeRecord>>>,
67    pub(super) allocations: Option<&'a BTreeMap<ReservationRef, ReservationAllocation>>,
68    pub(super) allowed_reservations: Option<&'a [ReservationRef]>,
69    pub(super) random_session: Option<RefCell<random::RandomSession>>,
70}
71
72impl SimulationView<'_> {
73    #[must_use]
74    pub const fn time(&self) -> SimTime {
75        self.state.now()
76    }
77
78    pub fn army(&self, id: ArmyId) -> Result<Option<&Army>, CanwuError> {
79        self.require_read(&StateKey::core_armies())?;
80        Ok(self.state.current().armies.get(&id))
81    }
82
83    pub fn person(&self, id: PersonId) -> Result<Option<&Person>, CanwuError> {
84        self.require_read(&StateKey::core_people())?;
85        Ok(self.state.current().people.get(&id))
86    }
87
88    pub fn government(&self, id: GovernmentId) -> Result<Option<&Government>, CanwuError> {
89        self.require_read(&StateKey::core_governments())?;
90        Ok(self.state.current().governments.get(&id))
91    }
92
93    pub fn territory(&self, id: TerritoryId) -> Result<Option<&Territory>, CanwuError> {
94        self.require_read(&StateKey::core_territories())?;
95        Ok(self.state.current().territories.get(&id))
96    }
97
98    pub fn route(&self, id: RouteId) -> Result<Option<&Route>, CanwuError> {
99        self.require_read(&StateKey::core_routes())?;
100        Ok(self.state.current().routes.get(&id))
101    }
102
103    pub fn actor_knowledge(&self, actor: PersonId) -> Result<Option<&ActorKnowledge>, CanwuError> {
104        self.require_read(&StateKey::core_knowledge())?;
105        Ok(self.state.current().knowledge.for_actor(actor))
106    }
107
108    /// Queries holder-relative records for an omniscient plugin system.
109    ///
110    /// This enforces the declared `canwu.core.knowledge` read and returns an
111    /// owned projection. It is not an actor-facing authorization API.
112    pub fn knowledge_records(
113        &self,
114        holder: KnowledgeHolderRef,
115        query: &KnowledgeQuery,
116    ) -> Result<canwu_knowledge::KnowledgeQueryResult, CanwuError> {
117        self.require_read(&StateKey::core_knowledge())?;
118        let result = if let Some(overlay) = self.knowledge_overlay {
119            self.state.current().knowledge.query_with_overlay(
120                holder,
121                query,
122                self.boundary_id,
123                overlay,
124            )
125        } else {
126            self.state
127                .current()
128                .knowledge
129                .query_current(holder, query, self.boundary_id)
130        };
131        result.map_err(|error| match error {
132            canwu_knowledge::KnowledgeQueryError::ReadCutUnavailable => CanwuError::new(
133                ErrorCode::KnowledgeReadCutUnavailable,
134                "knowledge cursor read cut is no longer available",
135            ),
136            canwu_knowledge::KnowledgeQueryError::InvalidLimit => CanwuError::new(
137                ErrorCode::KnowledgeLimitExceeded,
138                "knowledge query page size is outside the supported range",
139            ),
140            canwu_knowledge::KnowledgeQueryError::InvalidCursor
141            | canwu_knowledge::KnowledgeQueryError::InvalidLedger
142            | canwu_knowledge::KnowledgeQueryError::Encoding => CanwuError::new(
143                ErrorCode::InvalidKnowledgeRecord,
144                "knowledge query, cursor, or ledger is invalid",
145            ),
146        })
147    }
148
149    /// Resolves an exact command ID from the retained runtime journal in O(1).
150    ///
151    /// An archived command remains valid identity evidence, but its payload is
152    /// no longer available through this view: lookup returns
153    /// [`ErrorCode::EvidenceContentUnavailable`]. `None` means the ID has
154    /// neither retained content nor a committed archive receipt.
155    pub fn command(&self, id: CommandId) -> Result<Option<&CommandRecord>, CanwuError> {
156        self.require_read(&StateKey::core_commands())?;
157        let retained = self.state.evidence().retained_command(id);
158        if retained.is_none()
159            && self
160                .state
161                .evidence()
162                .archived_evidence_receipts
163                .contains_key(&EvidenceRef::Command(id))
164        {
165            return Err(CanwuError::new(
166                ErrorCode::EvidenceContentUnavailable,
167                "command identity is archived; payload inspection requires an archive provider",
168            ));
169        }
170        Ok(retained)
171    }
172
173    /// Resolves an exact event ID from the retained runtime journal in O(1).
174    ///
175    /// An archived event remains valid identity evidence, but its payload is
176    /// no longer available through this view: lookup returns
177    /// [`ErrorCode::EvidenceContentUnavailable`]. `None` means the ID has
178    /// neither retained content nor a committed archive receipt.
179    pub fn event(&self, id: EventId) -> Result<Option<&SimEvent>, CanwuError> {
180        self.require_read(&StateKey::core_events())?;
181        let retained = self.state.evidence().retained_event(id);
182        if retained.is_none()
183            && self
184                .state
185                .evidence()
186                .archived_evidence_receipts
187                .contains_key(&EvidenceRef::Event(id))
188        {
189            return Err(CanwuError::new(
190                ErrorCode::EvidenceContentUnavailable,
191                "event identity is archived; payload inspection requires an archive provider",
192            ));
193        }
194        Ok(retained)
195    }
196
197    pub fn ingress(&self, id: IngressId) -> Result<Option<&IngressRecord>, CanwuError> {
198        self.require_read(&StateKey::core_ingress())?;
199        if self
200            .allowed_ingress
201            .is_none_or(|allowed| !allowed.contains(&id))
202        {
203            return Ok(None);
204        }
205        let record = self.state.evidence().retained_ingress(id);
206        if record.is_none()
207            && self
208                .state
209                .evidence()
210                .archived_evidence_receipts
211                .contains_key(&EvidenceRef::Ingress(id))
212        {
213            return Err(CanwuError::new(
214                ErrorCode::EvidenceContentUnavailable,
215                "ingress identity is archived; payload inspection requires an archive provider",
216            ));
217        }
218        if let (Some(owner), Some(record)) = (self.ingress_plugin, record)
219            && !matches!(
220                &record.payload,
221                IngressPayload::Plugin { plugin, .. } if plugin == owner
222            )
223        {
224            return Ok(None);
225        }
226        Ok(record)
227    }
228
229    pub fn domain_record(
230        &self,
231        reference: &DomainRecordRef,
232    ) -> Result<Option<&DomainRecord>, CanwuError> {
233        self.require_read(&records::record_state_key(&reference.kind))?;
234        Ok(self
235            .record_overlay
236            .and_then(|overlay| overlay.get(reference))
237            .or_else(|| self.state.current().domain_records.get(reference)))
238    }
239
240    pub fn typed_domain_record<T: DomainRecordType>(
241        &self,
242        reference: &TypedDomainRecordRef<T>,
243    ) -> Result<Option<&DomainRecord>, CanwuError> {
244        self.domain_record(reference.as_untyped())
245    }
246
247    pub fn proposed_domain_record(
248        &self,
249        reference: &DomainRecordRef,
250    ) -> Result<Option<&DomainRecord>, CanwuError> {
251        self.require_read(&records::record_state_key(&reference.kind))?;
252        Ok(self
253            .proposed_records
254            .and_then(|records| records.get(reference)))
255    }
256
257    pub fn proposed_typed_domain_record<T: DomainRecordType>(
258        &self,
259        reference: &TypedDomainRecordRef<T>,
260    ) -> Result<Option<&DomainRecord>, CanwuError> {
261        self.proposed_domain_record(reference.as_untyped())
262    }
263
264    /// Returns the exact evidence reference assigned to a domain-record
265    /// version proposed earlier in the current boundary.
266    pub fn proposed_domain_record_version(
267        &self,
268        reference: &DomainRecordRef,
269    ) -> Result<Option<DomainRecordVersionRef>, CanwuError> {
270        self.require_read(&records::record_state_key(&reference.kind))?;
271        Ok(self.proposal_evidence.and_then(|evidence| {
272            evidence.iter().find_map(|item| match item {
273                EvidenceRef::DomainRecordVersion(version) if version.record == *reference => {
274                    Some(version.clone())
275                }
276                _ => None,
277            })
278        }))
279    }
280
281    /// Returns whether an exact domain-record version reference is valid at
282    /// this proposal-visible cut.
283    ///
284    /// This validates both the record identity/version and its establishment
285    /// source. Earlier same-boundary proposals are considered before retained
286    /// or archived runtime evidence.
287    pub fn domain_record_version_evidence_exists(
288        &self,
289        reference: &DomainRecordVersionRef,
290    ) -> Result<bool, CanwuError> {
291        self.require_read(&records::record_state_key(&reference.record.kind))?;
292        if let Some(proposed) = self.proposed_domain_record_version(&reference.record)? {
293            return Ok(proposed == *reference);
294        }
295        Ok(!matches!(
296            validation::resolve_evidence_reference(
297                &validation::RuntimeValidationContext::new(self.state.runtime()),
298                &EvidenceRef::DomainRecordVersion(reference.clone()),
299            ),
300            validation::EvidenceAvailability::Missing
301        ))
302    }
303
304    /// Returns a bounded, deterministic projection of records of one kind.
305    ///
306    /// Same-boundary overlays take precedence over current state. Records are
307    /// ordered by their canonical reference, so result order is replay stable.
308    pub fn domain_records_of_kind(
309        &self,
310        kind: &DomainRecordKind,
311        limit: usize,
312    ) -> Result<Vec<DomainRecord>, CanwuError> {
313        self.domain_records_of_kind_after(kind, None, limit)
314    }
315
316    /// Returns one bounded deterministic page of records after a canonical
317    /// record-reference cursor.
318    ///
319    /// The cursor is exclusive and must name the same kind. This keeps plugin
320    /// scans bounded without imposing a 10,000-record lifetime ceiling on a
321    /// domain kind. Same-boundary overlays retain the same precedence as
322    /// [`Self::domain_records_of_kind`].
323    pub fn domain_records_of_kind_after(
324        &self,
325        kind: &DomainRecordKind,
326        after: Option<&DomainRecordRef>,
327        limit: usize,
328    ) -> Result<Vec<DomainRecord>, CanwuError> {
329        const MAX_DOMAIN_RECORD_QUERY_LIMIT: usize = 10_000;
330        self.require_read(&records::record_state_key(kind))?;
331        if limit == 0 || limit > MAX_DOMAIN_RECORD_QUERY_LIMIT {
332            return Err(CanwuError::new(
333                ErrorCode::ValueOutOfRange,
334                format!(
335                    "domain-record query limit must be between 1 and {MAX_DOMAIN_RECORD_QUERY_LIMIT}"
336                ),
337            ));
338        }
339        if after.is_some_and(|cursor| cursor.kind != *kind) {
340            return Err(CanwuError::new(
341                ErrorCode::InvalidPayload,
342                "domain-record page cursor has the wrong kind",
343            ));
344        }
345
346        let mut records = BTreeMap::new();
347        for (reference, record) in &self.state.current().domain_records {
348            if reference.kind == *kind {
349                records.insert(reference.clone(), record.clone());
350            }
351        }
352        for overlay in [self.record_overlay, self.proposed_records]
353            .into_iter()
354            .flatten()
355        {
356            for (reference, record) in overlay {
357                if reference.kind == *kind {
358                    records.insert(reference.clone(), record.clone());
359                }
360            }
361        }
362        Ok(records
363            .into_iter()
364            .filter(|(reference, _)| after.is_none_or(|cursor| reference > cursor))
365            .map(|(_, record)| record)
366            .take(limit)
367            .collect())
368    }
369
370    /// Finds committed knowledge changes produced with an exact correlation.
371    ///
372    /// This supports next-boundary operation finalization without granting a
373    /// plugin unrestricted access to unrelated knowledge payloads.
374    pub fn knowledge_changes_by_correlation(
375        &self,
376        plugin: &str,
377        producer_correlation: &str,
378    ) -> Result<Vec<BoundaryKnowledgeChange>, CanwuError> {
379        self.require_read(&StateKey::core_knowledge())?;
380        Ok(self
381            .state
382            .evidence()
383            .boundaries
384            .iter()
385            .flat_map(|boundary| &boundary.knowledge_changes)
386            .filter(|change| {
387                change.plugin == plugin
388                    && change.producer_correlation.as_deref() == Some(producer_correlation)
389            })
390            .cloned()
391            .collect())
392    }
393
394    /// Finds committed knowledge changes whose producer correlation begins
395    /// with a deterministic operation prefix.
396    ///
397    /// The prefix remains plugin-scoped. This is intended for bounded
398    /// multi-holder operation finalization where every holder batch must keep
399    /// a unique full correlation value.
400    pub fn knowledge_changes_by_correlation_prefix(
401        &self,
402        plugin: &str,
403        producer_correlation_prefix: &str,
404    ) -> Result<Vec<BoundaryKnowledgeChange>, CanwuError> {
405        self.require_read(&StateKey::core_knowledge())?;
406        Ok(self
407            .state
408            .evidence()
409            .boundaries
410            .iter()
411            .flat_map(|boundary| &boundary.knowledge_changes)
412            .filter(|change| {
413                change.plugin == plugin
414                    && change
415                        .producer_correlation
416                        .as_deref()
417                        .is_some_and(|value| value.starts_with(producer_correlation_prefix))
418            })
419            .cloned()
420            .collect())
421    }
422
423    pub fn reservation(
424        &self,
425        reservation: &ReservationRef,
426    ) -> Result<Option<&ReservationAllocation>, CanwuError> {
427        let reader = self.reader.unwrap_or("unscoped caller");
428        if self
429            .allowed_reservations
430            .is_none_or(|allowed| !allowed.contains(reservation))
431        {
432            return Err(CanwuError::new(
433                ErrorCode::UndeclaredStateRead,
434                format!(
435                    "system {reader} did not declare reservation read {}.{}.{}",
436                    reservation.plugin, reservation.system, reservation.request
437                ),
438            ));
439        }
440        Ok(self.allocations.and_then(|values| values.get(reservation)))
441    }
442
443    pub fn random_range(
444        &self,
445        stream: &RandomStreamKey,
446        upper_exclusive: u64,
447        purpose: &str,
448    ) -> Result<u64, CanwuError> {
449        let Some(session) = &self.random_session else {
450            return Err(CanwuError::new(
451                ErrorCode::UndeclaredRandomStream,
452                format!(
453                    "system {} has no declared random streams",
454                    self.reader.unwrap_or("unscoped caller")
455                ),
456            ));
457        };
458        session.borrow_mut().range(stream, upper_exclusive, purpose)
459    }
460
461    #[allow(clippy::too_many_arguments)]
462    pub fn random_range_for_operation(
463        &self,
464        stream: &RandomStreamKey,
465        evidence: EvidenceRef,
466        operation_kind: &str,
467        application_operation_id: &str,
468        target: RandomOperationTarget,
469        draw_slot: u32,
470        upper_exclusive: u64,
471        purpose: &str,
472    ) -> Result<u64, CanwuError> {
473        let available = self
474            .proposal_evidence
475            .is_some_and(|values| values.contains(&evidence))
476            || validation::resolve_evidence_reference(
477                &validation::RuntimeValidationContext::new(self.state.runtime()),
478                &evidence,
479            ) == validation::EvidenceAvailability::Retained;
480        if !available {
481            return Err(CanwuError::new(
482                ErrorCode::InvalidRandomOperationEvidence,
483                "operation-keyed random draw references unavailable evidence",
484            ));
485        }
486        let Some(session) = &self.random_session else {
487            return Err(CanwuError::new(
488                ErrorCode::UndeclaredRandomStream,
489                format!(
490                    "system {} has no declared random streams",
491                    self.reader.unwrap_or("unscoped caller")
492                ),
493            ));
494        };
495        session.borrow_mut().range_for_operation(
496            stream,
497            evidence,
498            operation_kind,
499            application_operation_id,
500            target,
501            draw_slot,
502            upper_exclusive,
503            purpose,
504        )
505    }
506
507    pub fn component(
508        &self,
509        state: &StateKey,
510        entity: &EntityRef,
511        component: &str,
512    ) -> Result<Option<&Value>, CanwuError> {
513        self.require_read(state)?;
514        let Some(owner) = self.state_owners.get(state) else {
515            return Err(CanwuError::new(
516                ErrorCode::UndeclaredStateRead,
517                format!(
518                    "state {}.{} has no registered owner",
519                    state.namespace, state.name
520                ),
521            ));
522        };
523        let key = component_key(owner, state, entity, component);
524        Ok(self
525            .component_overlay
526            .and_then(|overlay| overlay.get(&key))
527            .or_else(|| self.state.current().plugin_components.get(&key))
528            .map(|record| &record.value))
529    }
530
531    pub fn proposed_component(
532        &self,
533        state: &StateKey,
534        entity: &EntityRef,
535        component: &str,
536    ) -> Result<Option<&Value>, CanwuError> {
537        self.require_read(state)?;
538        let Some(owner) = self.state_owners.get(state) else {
539            return Err(CanwuError::new(
540                ErrorCode::UndeclaredStateRead,
541                format!(
542                    "state {}.{} has no registered owner",
543                    state.namespace, state.name
544                ),
545            ));
546        };
547        let key = component_key(owner, state, entity, component);
548        Ok(self
549            .proposed_components
550            .and_then(|proposals| proposals.get(&key))
551            .map(|record| &record.value))
552    }
553
554    fn require_read(&self, state: &StateKey) -> Result<(), CanwuError> {
555        if self
556            .allowed_reads
557            .is_some_and(|reads| !reads.contains(state))
558        {
559            return Err(CanwuError::new(
560                ErrorCode::UndeclaredStateRead,
561                format!(
562                    "{} did not declare read access to {}.{}",
563                    self.reader.unwrap_or("internal system"),
564                    state.namespace,
565                    state.name
566                ),
567            ));
568        }
569        Ok(())
570    }
571
572    pub(super) fn finish_random_session(self) -> Option<random::RandomExecution> {
573        self.random_session
574            .map(RefCell::into_inner)
575            .map(random::RandomSession::finish)
576    }
577}