Skip to main content

canwu_sim/runtime/
view.rs

1use super::{
2    ActorKnowledge, Army, ArmyId, BoundaryId, BoundaryKnowledgeChange, CanwuError, CauseRef,
3    CommandId, CommandRecord, DecisionAttemptRecord, DecisionControllerBinding, DecisionRequestId,
4    DecisionTicket, DecisionTicketId, DomainRecord, DomainRecordKind, DomainRecordRef,
5    DomainRecordType, DomainRecordVersionRef, EntityRef, ErrorCode, EventId, EvidenceRef,
6    Government, GovernmentId, HashSet, IngressId, IngressPayload, IngressQueueKey, IngressRecord,
7    KnowledgeHolderRef, KnowledgeQuery, KnowledgeRecord, KnowledgeRecordId, Person, PersonId,
8    PluginComponentKey, PluginComponentRecord, RandomOperationTarget, RandomStreamKey, RefCell,
9    ReservationAllocation, ReservationRef, Route, RouteId, RuntimeCurrentState, RuntimeEvidence,
10    RuntimeState, SimEvent, SimTime, StateKey, Territory, TerritoryId, TypedDomainRecordRef, Value,
11    component_key, domain_record_candidates, random, records, retained_domain_record_version,
12    validate_domain_record_page_request, validation,
13};
14use std::collections::{BTreeMap, BTreeSet};
15
16pub(super) enum SimulationViewState<'a> {
17    Runtime(&'a RuntimeState),
18    Boundary {
19        current: &'a RuntimeCurrentState,
20        now: SimTime,
21        runtime: &'a RuntimeState,
22    },
23}
24
25impl SimulationViewState<'_> {
26    const fn current(&self) -> &RuntimeCurrentState {
27        match self {
28            Self::Runtime(state) => &state.current,
29            Self::Boundary { current, .. } => current,
30        }
31    }
32
33    const fn now(&self) -> SimTime {
34        match self {
35            Self::Runtime(state) => state.scheduler.now,
36            Self::Boundary { now, .. } => *now,
37        }
38    }
39
40    const fn evidence(&self) -> &RuntimeEvidence {
41        match self {
42            Self::Runtime(state) => &state.evidence,
43            Self::Boundary { runtime, .. } => &runtime.evidence,
44        }
45    }
46
47    const fn runtime(&self) -> &RuntimeState {
48        match self {
49            Self::Runtime(state) | Self::Boundary { runtime: state, .. } => state,
50        }
51    }
52}
53
54pub struct SimulationView<'a> {
55    pub(super) state: SimulationViewState<'a>,
56    pub(super) state_owners: &'a BTreeMap<StateKey, String>,
57    pub(super) reader: Option<&'a str>,
58    pub(super) allowed_reads: Option<&'a [StateKey]>,
59    pub(super) allowed_ingress: Option<&'a HashSet<IngressId>>,
60    pub(super) ingress_plugin: Option<&'a str>,
61    pub(super) component_overlay: Option<&'a BTreeMap<PluginComponentKey, PluginComponentRecord>>,
62    pub(super) proposed_components: Option<&'a BTreeMap<PluginComponentKey, PluginComponentRecord>>,
63    pub(super) record_overlay: Option<&'a BTreeMap<DomainRecordRef, DomainRecord>>,
64    pub(super) proposed_records: Option<&'a BTreeMap<DomainRecordRef, DomainRecord>>,
65    pub(super) boundary_id: Option<BoundaryId>,
66    pub(super) proposal_evidence: Option<&'a BTreeSet<EvidenceRef>>,
67    pub(super) knowledge_overlay:
68        Option<&'a BTreeMap<KnowledgeHolderRef, BTreeMap<KnowledgeRecordId, KnowledgeRecord>>>,
69    pub(super) allocations: Option<&'a BTreeMap<ReservationRef, ReservationAllocation>>,
70    pub(super) allowed_reservations: Option<&'a [ReservationRef]>,
71    pub(super) random_session: Option<RefCell<random::RandomSession>>,
72    pub(super) plugin_archive_provider: &'a dyn super::PluginArchiveObjectProvider,
73}
74
75impl SimulationView<'_> {
76    /// Loads a package-owned cold object through the host provider attached to
77    /// this runtime. Package code remains responsible for authenticating the
78    /// bytes against its committed archive root before using them.
79    pub fn plugin_archive_object(
80        &self,
81        namespace: &str,
82        object_id: &str,
83    ) -> Result<Option<Vec<u8>>, CanwuError> {
84        self.plugin_archive_provider
85            .load_plugin_archive_object(namespace, object_id)
86    }
87
88    #[must_use]
89    pub const fn time(&self) -> SimTime {
90        self.state.now()
91    }
92
93    pub fn army(&self, id: ArmyId) -> Result<Option<&Army>, CanwuError> {
94        self.require_read(&StateKey::core_armies())?;
95        Ok(self.state.current().armies.get(&id))
96    }
97
98    pub fn person(&self, id: PersonId) -> Result<Option<&Person>, CanwuError> {
99        self.require_read(&StateKey::core_people())?;
100        Ok(self.state.current().people.get(&id))
101    }
102
103    pub fn government(&self, id: GovernmentId) -> Result<Option<&Government>, CanwuError> {
104        self.require_read(&StateKey::core_governments())?;
105        Ok(self.state.current().governments.get(&id))
106    }
107
108    pub fn territory(&self, id: TerritoryId) -> Result<Option<&Territory>, CanwuError> {
109        self.require_read(&StateKey::core_territories())?;
110        Ok(self.state.current().territories.get(&id))
111    }
112
113    pub fn route(&self, id: RouteId) -> Result<Option<&Route>, CanwuError> {
114        self.require_read(&StateKey::core_routes())?;
115        Ok(self.state.current().routes.get(&id))
116    }
117
118    pub fn actor_knowledge(&self, actor: PersonId) -> Result<Option<&ActorKnowledge>, CanwuError> {
119        self.require_read(&StateKey::core_knowledge())?;
120        Ok(self.state.current().knowledge.for_actor(actor))
121    }
122
123    /// Counts records in a knowledge namespace at the current proposal-visible cut.
124    pub fn knowledge_record_count_in_namespace(
125        &self,
126        namespace: &str,
127    ) -> Result<usize, CanwuError> {
128        self.require_read(&StateKey::core_knowledge())?;
129        let settled = self
130            .state
131            .current()
132            .knowledge
133            .record_count_in_namespace(namespace);
134        let proposed = self.knowledge_overlay.map_or(0, |overlay| {
135            overlay
136                .values()
137                .flat_map(BTreeMap::values)
138                .filter(|record| record.schema.kind.namespace == namespace)
139                .count()
140        });
141        settled.checked_add(proposed).ok_or_else(|| {
142            CanwuError::new(
143                ErrorCode::ValueOutOfRange,
144                "knowledge namespace record count overflowed",
145            )
146        })
147    }
148
149    /// Queries holder-relative records for an omniscient plugin system.
150    ///
151    /// This enforces the declared `canwu.core.knowledge` read and returns an
152    /// owned projection. It is not an actor-facing authorization API.
153    pub fn knowledge_records(
154        &self,
155        holder: KnowledgeHolderRef,
156        query: &KnowledgeQuery,
157    ) -> Result<canwu_knowledge::KnowledgeQueryResult, CanwuError> {
158        self.require_read(&StateKey::core_knowledge())?;
159        let result = if let Some(overlay) = self.knowledge_overlay {
160            self.state.current().knowledge.query_with_overlay(
161                holder,
162                query,
163                self.boundary_id,
164                overlay,
165            )
166        } else {
167            self.state
168                .current()
169                .knowledge
170                .query_current(holder, query, self.boundary_id)
171        };
172        result.map_err(|error| match error {
173            canwu_knowledge::KnowledgeQueryError::ReadCutUnavailable => CanwuError::new(
174                ErrorCode::KnowledgeReadCutUnavailable,
175                "knowledge cursor read cut is no longer available",
176            ),
177            canwu_knowledge::KnowledgeQueryError::InvalidLimit => CanwuError::new(
178                ErrorCode::KnowledgeLimitExceeded,
179                "knowledge query page size is outside the supported range",
180            ),
181            canwu_knowledge::KnowledgeQueryError::InvalidCursor
182            | canwu_knowledge::KnowledgeQueryError::InvalidLedger
183            | canwu_knowledge::KnowledgeQueryError::Encoding => CanwuError::new(
184                ErrorCode::InvalidKnowledgeRecord,
185                "knowledge query, cursor, or ledger is invalid",
186            ),
187        })
188    }
189
190    /// Resolves an exact command ID from the retained runtime journal in O(1).
191    ///
192    /// An archived command remains valid identity evidence, but its payload is
193    /// no longer available through this view: lookup returns
194    /// [`ErrorCode::EvidenceContentUnavailable`]. `None` means the ID has
195    /// neither retained content nor a committed archive receipt.
196    pub fn command(&self, id: CommandId) -> Result<Option<&CommandRecord>, CanwuError> {
197        self.require_read(&StateKey::core_commands())?;
198        let retained = self.state.evidence().retained_command(id);
199        if retained.is_none()
200            && self
201                .state
202                .evidence()
203                .archived_evidence_receipts
204                .contains_key(&EvidenceRef::Command(id))
205        {
206            return Err(CanwuError::new(
207                ErrorCode::EvidenceContentUnavailable,
208                "command identity is archived; payload inspection requires an archive provider",
209            ));
210        }
211        Ok(retained)
212    }
213
214    /// Resolves an exact event ID from the retained runtime journal in O(1).
215    ///
216    /// An archived event remains valid identity evidence, but its payload is
217    /// no longer available through this view: lookup returns
218    /// [`ErrorCode::EvidenceContentUnavailable`]. `None` means the ID has
219    /// neither retained content nor a committed archive receipt.
220    pub fn event(&self, id: EventId) -> Result<Option<&SimEvent>, CanwuError> {
221        self.require_read(&StateKey::core_events())?;
222        let retained = self.state.evidence().retained_event(id);
223        if retained.is_none()
224            && self
225                .state
226                .evidence()
227                .archived_evidence_receipts
228                .contains_key(&EvidenceRef::Event(id))
229        {
230            return Err(CanwuError::new(
231                ErrorCode::EvidenceContentUnavailable,
232                "event identity is archived; payload inspection requires an archive provider",
233            ));
234        }
235        Ok(retained)
236    }
237
238    pub fn ingress(&self, id: IngressId) -> Result<Option<&IngressRecord>, CanwuError> {
239        self.require_read(&StateKey::core_ingress())?;
240        if self
241            .allowed_ingress
242            .is_none_or(|allowed| !allowed.contains(&id))
243        {
244            return Ok(None);
245        }
246        let record = self.state.evidence().retained_ingress(id);
247        if record.is_none()
248            && self
249                .state
250                .evidence()
251                .archived_evidence_receipts
252                .contains_key(&EvidenceRef::Ingress(id))
253        {
254            return Err(CanwuError::new(
255                ErrorCode::EvidenceContentUnavailable,
256                "ingress identity is archived; payload inspection requires an archive provider",
257            ));
258        }
259        if let (Some(owner), Some(record)) = (self.ingress_plugin, record)
260            && !matches!(
261                &record.payload,
262                IngressPayload::Plugin { plugin, .. } if plugin == owner
263            )
264        {
265            return Ok(None);
266        }
267        Ok(record)
268    }
269
270    /// Matches retained, plugin-generated ingress provenance without exposing its payload.
271    ///
272    /// Durable evidence may cite ingress admitted at an earlier boundary, but a
273    /// generated record still waiting in the scheduler is not yet admissible.
274    /// Format-7 archive receipts retain a Merkle-bound compact producer proof,
275    /// so archived payload bytes do not need to return to the hot path.
276    pub fn plugin_ingress_matches(
277        &self,
278        id: IngressId,
279        plugin: &str,
280        packet_type: &str,
281    ) -> Result<bool, CanwuError> {
282        self.require_read(&StateKey::core_ingress())?;
283        let record = self.state.evidence().retained_ingress(id);
284        if record.is_none()
285            && let Some(receipt) = self
286                .state
287                .evidence()
288                .archived_evidence_receipts
289                .get(&EvidenceRef::Ingress(id))
290        {
291            if self
292                .state
293                .runtime()
294                .scheduler
295                .pending_ingress
296                .iter()
297                .any(|key| key.id == id)
298            {
299                return Ok(false);
300            }
301            return Ok(receipt
302                .plugin_ingress_provenance
303                .as_ref()
304                .is_some_and(|provenance| {
305                    provenance.plugin == plugin && provenance.packet_type == packet_type
306                }));
307        }
308        let Some(record) = record else {
309            return Ok(false);
310        };
311        if self
312            .state
313            .runtime()
314            .scheduler
315            .pending_ingress
316            .contains(&IngressQueueKey::from_record(record))
317        {
318            return Ok(false);
319        }
320        if !matches!(
321            &record.payload,
322            IngressPayload::Plugin {
323                plugin: actual_plugin,
324                packet_type: actual_packet_type,
325                ..
326            } if actual_plugin == plugin && actual_packet_type == packet_type
327        ) {
328            return Ok(false);
329        }
330        let Some(CauseRef::Boundary(boundary_id)) = record.cause.as_ref() else {
331            return Ok(false);
332        };
333        let boundary = self.state.evidence().retained_boundary(*boundary_id);
334        if boundary.is_none()
335            && self
336                .state
337                .evidence()
338                .archived_evidence_receipts
339                .contains_key(&EvidenceRef::Boundary(*boundary_id))
340        {
341            return Err(CanwuError::new(
342                ErrorCode::EvidenceContentUnavailable,
343                "ingress producer boundary is archived; provenance inspection requires an archive provider",
344            ));
345        }
346        Ok(boundary.is_some_and(|boundary| {
347            boundary
348                .generated_ingress
349                .iter()
350                .any(|generation| generation.ingress == id && generation.plugin == plugin)
351        }))
352    }
353
354    /// Matches a retained plugin ingress to an exact provider payload and
355    /// delivery time. Payload inspection is deliberately limited to retained
356    /// records: archived receipts prove producer identity, but cannot safely
357    /// be reused to authorize a different legal proposal without the original
358    /// bytes.
359    pub fn plugin_ingress_payload_matches(
360        &self,
361        id: IngressId,
362        plugin: &str,
363        packet_type: &str,
364        occurred_at: SimTime,
365        expected_payload: &Value,
366    ) -> Result<bool, CanwuError> {
367        self.require_read(&StateKey::core_ingress())?;
368        let Some(record) = self.state.evidence().retained_ingress(id) else {
369            if self
370                .state
371                .evidence()
372                .archived_evidence_receipts
373                .contains_key(&EvidenceRef::Ingress(id))
374            {
375                return Err(CanwuError::new(
376                    ErrorCode::EvidenceContentUnavailable,
377                    "provider ingress payload is archived; exact legal signal binding requires retained content",
378                ));
379            }
380            return Ok(false);
381        };
382        if self
383            .state
384            .runtime()
385            .scheduler
386            .pending_ingress
387            .contains(&IngressQueueKey::from_record(record))
388        {
389            return Ok(false);
390        }
391        let IngressPayload::Plugin {
392            plugin: actual_plugin,
393            packet_type: actual_packet_type,
394            payload,
395            ..
396        } = &record.payload
397        else {
398            return Ok(false);
399        };
400        if actual_plugin != plugin
401            || actual_packet_type != packet_type
402            || record.due_at != occurred_at
403            || payload != expected_payload
404        {
405            return Ok(false);
406        }
407        let Some(CauseRef::Boundary(boundary_id)) = record.cause.as_ref() else {
408            return Ok(false);
409        };
410        let boundary = self.state.evidence().retained_boundary(*boundary_id);
411        if boundary.is_none()
412            && self
413                .state
414                .evidence()
415                .archived_evidence_receipts
416                .contains_key(&EvidenceRef::Boundary(*boundary_id))
417        {
418            return Err(CanwuError::new(
419                ErrorCode::EvidenceContentUnavailable,
420                "provider ingress producer boundary is archived; exact legal signal binding requires retained content",
421            ));
422        }
423        Ok(boundary.is_some_and(|boundary| {
424            boundary
425                .generated_ingress
426                .iter()
427                .any(|generation| generation.ingress == id && generation.plugin == plugin)
428        }))
429    }
430
431    /// Returns the retained outcome for one exact decision request.
432    pub fn decision_attempt(
433        &self,
434        request_id: DecisionRequestId,
435    ) -> Result<Option<&DecisionAttemptRecord>, CanwuError> {
436        self.require_read(&StateKey::core_decisions())?;
437        Ok(self.state.current().decisions.attempt(request_id))
438    }
439
440    /// Returns one current decision-controller binding after an explicit core read.
441    pub fn decision_controller(
442        &self,
443        id: &str,
444    ) -> Result<Option<&DecisionControllerBinding>, CanwuError> {
445        self.require_read(&StateKey::core_decisions())?;
446        Ok(self.state.current().decisions.controller(id))
447    }
448
449    /// Returns one current decision ticket after an explicit core read.
450    pub fn decision_ticket(
451        &self,
452        id: DecisionTicketId,
453    ) -> Result<Option<&DecisionTicket>, CanwuError> {
454        self.require_read(&StateKey::core_decisions())?;
455        Ok(self.state.current().decisions.ticket(id))
456    }
457
458    pub fn domain_record(
459        &self,
460        reference: &DomainRecordRef,
461    ) -> Result<Option<&DomainRecord>, CanwuError> {
462        self.require_domain_record_read(reference)?;
463        Ok(self
464            .record_overlay
465            .and_then(|overlay| overlay.get(reference))
466            .or_else(|| self.state.current().domain_records.get(reference)))
467    }
468
469    pub fn typed_domain_record<T: DomainRecordType>(
470        &self,
471        reference: &TypedDomainRecordRef<T>,
472    ) -> Result<Option<&DomainRecord>, CanwuError> {
473        self.domain_record(reference.as_untyped())
474    }
475
476    pub fn proposed_domain_record(
477        &self,
478        reference: &DomainRecordRef,
479    ) -> Result<Option<&DomainRecord>, CanwuError> {
480        self.require_read(&records::record_state_key(&reference.kind))?;
481        Ok(self
482            .proposed_records
483            .and_then(|records| records.get(reference)))
484    }
485
486    pub fn proposed_typed_domain_record<T: DomainRecordType>(
487        &self,
488        reference: &TypedDomainRecordRef<T>,
489    ) -> Result<Option<&DomainRecord>, CanwuError> {
490        self.proposed_domain_record(reference.as_untyped())
491    }
492
493    /// Returns the exact evidence reference assigned to a domain-record
494    /// version proposed earlier in the current boundary.
495    pub fn proposed_domain_record_version(
496        &self,
497        reference: &DomainRecordRef,
498    ) -> Result<Option<DomainRecordVersionRef>, CanwuError> {
499        self.require_read(&records::record_state_key(&reference.kind))?;
500        Ok(self.proposal_evidence.and_then(|evidence| {
501            evidence.iter().find_map(|item| match item {
502                EvidenceRef::DomainRecordVersion(version) if version.record == *reference => {
503                    Some(version.clone())
504                }
505                _ => None,
506            })
507        }))
508    }
509
510    /// Returns the exact evidence reference for the currently visible version
511    /// of a domain record.  Strategic aggregation runs after atomic commit,
512    /// therefore it cannot use [`Self::proposed_domain_record_version`].
513    ///
514    /// The current boundary overlay/proposal is preferred, followed by the
515    /// runtime's verified current-record provenance index. The index is
516    /// maintained at commit time and rebuilt from canonical evidence on
517    /// restore, so lookup does not scan retained or archived history.
518    pub fn current_domain_record_version(
519        &self,
520        reference: &DomainRecordRef,
521    ) -> Result<Option<DomainRecordVersionRef>, CanwuError> {
522        self.require_read(&records::record_state_key(&reference.kind))?;
523        let Some(record) = self.domain_record(reference)? else {
524            return Ok(None);
525        };
526        if let Some(proposed) = self.proposal_evidence.and_then(|evidence| {
527            evidence.iter().find_map(|item| match item {
528                EvidenceRef::DomainRecordVersion(version)
529                    if version.record == *reference && version.version == record.version =>
530                {
531                    Some(version.clone())
532                }
533                _ => None,
534            })
535        }) {
536            return Ok(Some(proposed));
537        }
538        let current = super::current_domain_record_version(self.state.runtime(), reference)?;
539        if current
540            .as_ref()
541            .is_some_and(|current| current.version != record.version)
542        {
543            return Err(CanwuError::new(
544                ErrorCode::InvalidSnapshot,
545                "visible domain-record version disagrees with the runtime provenance index",
546            ));
547        }
548        Ok(current)
549    }
550
551    /// Returns whether an exact domain-record version reference is valid at
552    /// this proposal-visible cut.
553    ///
554    /// This validates both the record identity/version and its establishment
555    /// source. Earlier same-boundary proposals are considered before retained
556    /// or archived runtime evidence.
557    pub fn domain_record_version_evidence_exists(
558        &self,
559        reference: &DomainRecordVersionRef,
560    ) -> Result<bool, CanwuError> {
561        self.require_domain_record_read(&reference.record)?;
562        if self
563            .proposed_domain_record_version(&reference.record)?
564            .is_some_and(|proposed| proposed == *reference)
565        {
566            return Ok(true);
567        }
568        Ok(!matches!(
569            validation::resolve_evidence_reference(
570                &validation::RuntimeValidationContext::new(self.state.runtime()),
571                &EvidenceRef::DomainRecordVersion(reference.clone()),
572            ),
573            validation::EvidenceAvailability::Missing
574        ))
575    }
576
577    /// Checks that an exact domain-record version is both valid evidence and current.
578    pub fn domain_record_version_is_current(
579        &self,
580        reference: &DomainRecordVersionRef,
581    ) -> Result<bool, CanwuError> {
582        self.require_domain_record_read(&reference.record)?;
583        let current = self
584            .record_overlay
585            .and_then(|overlay| overlay.get(&reference.record))
586            .or_else(|| self.state.current().domain_records.get(&reference.record));
587        Ok(
588            current.is_some_and(|record| record.version == reference.version)
589                && self.domain_record_version_evidence_exists(reference)?,
590        )
591    }
592
593    /// Returns whether a generic evidence identity is retained or archived.
594    ///
595    /// Domain-record versions proposed earlier in this boundary are visible.
596    /// Archived identities count as existing even when their bodies are no
597    /// longer retained.
598    pub fn evidence_exists(&self, reference: &EvidenceRef) -> Result<bool, CanwuError> {
599        match reference {
600            EvidenceRef::Command(_) | EvidenceRef::CommandAttempt(_) => {
601                self.require_read(&StateKey::core_commands())?;
602            }
603            EvidenceRef::Event(_) => self.require_read(&StateKey::core_events())?,
604            EvidenceRef::Ingress(_) => self.require_read(&StateKey::core_ingress())?,
605            EvidenceRef::Boundary(_) | EvidenceRef::RandomDraw(_) => {
606                self.require_read(&StateKey::core_evidence())?;
607            }
608            EvidenceRef::DomainRecordVersion(version) => {
609                return self.domain_record_version_evidence_exists(version);
610            }
611        }
612        Ok(!matches!(
613            validation::resolve_evidence_reference(
614                &validation::RuntimeValidationContext::new(self.state.runtime()),
615                reference,
616            ),
617            validation::EvidenceAvailability::Missing
618        ))
619    }
620
621    /// Returns when retained or earlier same-boundary evidence first became
622    /// authoritative at this proposal-visible cut.
623    ///
624    /// Archived identity receipts do not retain a precise semantic time, so
625    /// they return `None` and callers that require temporal ordering must fail
626    /// closed or load the archived evidence body.
627    pub fn evidence_time(&self, reference: &EvidenceRef) -> Result<Option<SimTime>, CanwuError> {
628        if !self.evidence_exists(reference)? {
629            return Ok(None);
630        }
631        if self
632            .proposal_evidence
633            .is_some_and(|evidence| evidence.contains(reference))
634        {
635            return Ok(Some(self.time()));
636        }
637        Ok(super::retained_evidence_time(
638            self.state.runtime(),
639            reference,
640        ))
641    }
642
643    /// Resolves the retained record body for one exact domain-record version.
644    ///
645    /// Archived receipts prove that a version existed but do not contain its
646    /// body, so this returns `None` when the corresponding evidence segment is
647    /// not live in the runtime.
648    pub fn domain_record_version(
649        &self,
650        reference: &DomainRecordVersionRef,
651    ) -> Result<Option<DomainRecord>, CanwuError> {
652        self.require_read(&records::record_state_key(&reference.record.kind))?;
653        if let Some(proposed) = self.proposed_domain_record_version(&reference.record)?
654            && proposed == *reference
655        {
656            return Ok(self
657                .proposed_records
658                .and_then(|records| records.get(&reference.record))
659                .or_else(|| {
660                    self.record_overlay
661                        .and_then(|records| records.get(&reference.record))
662                })
663                .cloned());
664        }
665        Ok(retained_domain_record_version(
666            self.state.runtime(),
667            reference,
668        ))
669    }
670
671    /// Returns a bounded, deterministic projection of records of one kind.
672    ///
673    /// Same-boundary overlays take precedence over current state. Records are
674    /// ordered by their canonical reference, so result order is replay stable.
675    pub fn domain_records_of_kind(
676        &self,
677        kind: &DomainRecordKind,
678        limit: usize,
679    ) -> Result<Vec<DomainRecord>, CanwuError> {
680        self.domain_records_of_kind_after(kind, None, limit)
681    }
682
683    /// Returns one bounded deterministic page of records after a canonical
684    /// record-reference cursor.
685    ///
686    /// The cursor is exclusive and must name the same kind. This keeps plugin
687    /// scans bounded without imposing a 10,000-record lifetime ceiling on a
688    /// domain kind. Same-boundary overlays retain the same precedence as
689    /// [`Self::domain_records_of_kind`].
690    pub fn domain_records_of_kind_after(
691        &self,
692        kind: &DomainRecordKind,
693        after: Option<&DomainRecordRef>,
694        limit: usize,
695    ) -> Result<Vec<DomainRecord>, CanwuError> {
696        self.require_read(&records::record_state_key(kind))?;
697        validate_domain_record_page_request(kind, after, limit)?;
698
699        let mut records =
700            domain_record_candidates(&self.state.current().domain_records, kind, after, limit);
701        for overlay in [self.record_overlay, self.proposed_records]
702            .into_iter()
703            .flatten()
704        {
705            for (reference, record) in domain_record_candidates(overlay, kind, after, limit) {
706                records.insert(reference, record);
707            }
708        }
709        Ok(records.into_values().take(limit).collect())
710    }
711
712    /// Finds committed knowledge changes produced with an exact correlation.
713    ///
714    /// This supports next-boundary operation finalization without granting a
715    /// plugin unrestricted access to unrelated knowledge payloads.
716    pub fn knowledge_changes_by_correlation(
717        &self,
718        plugin: &str,
719        producer_correlation: &str,
720    ) -> Result<Vec<BoundaryKnowledgeChange>, CanwuError> {
721        self.require_read(&StateKey::core_knowledge())?;
722        Ok(self
723            .state
724            .evidence()
725            .boundaries
726            .iter()
727            .flat_map(|boundary| &boundary.knowledge_changes)
728            .filter(|change| {
729                change.plugin == plugin
730                    && change.producer_correlation.as_deref() == Some(producer_correlation)
731            })
732            .cloned()
733            .collect())
734    }
735
736    /// Finds committed knowledge changes whose producer correlation begins
737    /// with a deterministic operation prefix.
738    ///
739    /// The prefix remains plugin-scoped. This is intended for bounded
740    /// multi-holder operation finalization where every holder batch must keep
741    /// a unique full correlation value.
742    pub fn knowledge_changes_by_correlation_prefix(
743        &self,
744        plugin: &str,
745        producer_correlation_prefix: &str,
746    ) -> Result<Vec<BoundaryKnowledgeChange>, CanwuError> {
747        self.require_read(&StateKey::core_knowledge())?;
748        Ok(self
749            .state
750            .evidence()
751            .boundaries
752            .iter()
753            .flat_map(|boundary| &boundary.knowledge_changes)
754            .filter(|change| {
755                change.plugin == plugin
756                    && change
757                        .producer_correlation
758                        .as_deref()
759                        .is_some_and(|value| value.starts_with(producer_correlation_prefix))
760            })
761            .cloned()
762            .collect())
763    }
764
765    pub fn reservation(
766        &self,
767        reservation: &ReservationRef,
768    ) -> Result<Option<&ReservationAllocation>, CanwuError> {
769        let reader = self.reader.unwrap_or("unscoped caller");
770        if self
771            .allowed_reservations
772            .is_none_or(|allowed| !allowed.contains(reservation))
773        {
774            return Err(CanwuError::new(
775                ErrorCode::UndeclaredStateRead,
776                format!(
777                    "system {reader} did not declare reservation read {}.{}.{}",
778                    reservation.plugin, reservation.system, reservation.request
779                ),
780            ));
781        }
782        Ok(self.allocations.and_then(|values| values.get(reservation)))
783    }
784
785    pub fn random_range(
786        &self,
787        stream: &RandomStreamKey,
788        upper_exclusive: u64,
789        purpose: &str,
790    ) -> Result<u64, CanwuError> {
791        let Some(session) = &self.random_session else {
792            return Err(CanwuError::new(
793                ErrorCode::UndeclaredRandomStream,
794                format!(
795                    "system {} has no declared random streams",
796                    self.reader.unwrap_or("unscoped caller")
797                ),
798            ));
799        };
800        session.borrow_mut().range(stream, upper_exclusive, purpose)
801    }
802
803    #[allow(clippy::too_many_arguments)]
804    pub fn random_range_for_operation(
805        &self,
806        stream: &RandomStreamKey,
807        evidence: EvidenceRef,
808        operation_kind: &str,
809        application_operation_id: &str,
810        target: RandomOperationTarget,
811        draw_slot: u32,
812        upper_exclusive: u64,
813        purpose: &str,
814    ) -> Result<u64, CanwuError> {
815        self.random_sample_for_operation(
816            stream,
817            evidence,
818            operation_kind,
819            application_operation_id,
820            target,
821            draw_slot,
822            upper_exclusive,
823            purpose,
824        )
825        .map(|sample| sample.value)
826    }
827
828    #[allow(clippy::too_many_arguments)]
829    pub fn random_sample_for_operation(
830        &self,
831        stream: &RandomStreamKey,
832        evidence: EvidenceRef,
833        operation_kind: &str,
834        application_operation_id: &str,
835        target: RandomOperationTarget,
836        draw_slot: u32,
837        upper_exclusive: u64,
838        purpose: &str,
839    ) -> Result<super::RandomSample, CanwuError> {
840        let available = self
841            .proposal_evidence
842            .is_some_and(|values| values.contains(&evidence))
843            || validation::resolve_evidence_reference(
844                &validation::RuntimeValidationContext::new(self.state.runtime()),
845                &evidence,
846            ) == validation::EvidenceAvailability::Retained;
847        if !available {
848            return Err(CanwuError::new(
849                ErrorCode::InvalidRandomOperationEvidence,
850                "operation-keyed random draw references unavailable evidence",
851            ));
852        }
853        let Some(session) = &self.random_session else {
854            return Err(CanwuError::new(
855                ErrorCode::UndeclaredRandomStream,
856                format!(
857                    "system {} has no declared random streams",
858                    self.reader.unwrap_or("unscoped caller")
859                ),
860            ));
861        };
862        session.borrow_mut().sample_for_operation(
863            stream,
864            evidence,
865            operation_kind,
866            application_operation_id,
867            target,
868            draw_slot,
869            upper_exclusive,
870            purpose,
871        )
872    }
873
874    pub fn component(
875        &self,
876        state: &StateKey,
877        entity: &EntityRef,
878        component: &str,
879    ) -> Result<Option<&Value>, CanwuError> {
880        self.require_read(state)?;
881        let Some(owner) = self.state_owners.get(state) else {
882            return Err(CanwuError::new(
883                ErrorCode::UndeclaredStateRead,
884                format!(
885                    "state {}.{} has no registered owner",
886                    state.namespace, state.name
887                ),
888            ));
889        };
890        let key = component_key(owner, state, entity, component);
891        Ok(self
892            .component_overlay
893            .and_then(|overlay| overlay.get(&key))
894            .or_else(|| self.state.current().plugin_components.get(&key))
895            .map(|record| &record.value))
896    }
897
898    pub fn proposed_component(
899        &self,
900        state: &StateKey,
901        entity: &EntityRef,
902        component: &str,
903    ) -> Result<Option<&Value>, CanwuError> {
904        self.require_read(state)?;
905        let Some(owner) = self.state_owners.get(state) else {
906            return Err(CanwuError::new(
907                ErrorCode::UndeclaredStateRead,
908                format!(
909                    "state {}.{} has no registered owner",
910                    state.namespace, state.name
911                ),
912            ));
913        };
914        let key = component_key(owner, state, entity, component);
915        Ok(self
916            .proposed_components
917            .and_then(|proposals| proposals.get(&key))
918            .map(|record| &record.value))
919    }
920
921    fn require_read(&self, state: &StateKey) -> Result<(), CanwuError> {
922        if self
923            .allowed_reads
924            .is_some_and(|reads| !reads.contains(state))
925        {
926            return Err(CanwuError::new(
927                ErrorCode::UndeclaredStateRead,
928                format!(
929                    "{} did not declare read access to {}.{}",
930                    self.reader.unwrap_or("internal system"),
931                    state.namespace,
932                    state.name
933                ),
934            ));
935        }
936        Ok(())
937    }
938
939    fn require_domain_record_read(&self, reference: &DomainRecordRef) -> Result<(), CanwuError> {
940        let exact = records::record_state_key(&reference.kind);
941        if self.allowed_reads.is_some_and(|reads| {
942            !reads.contains(&exact) && !reads.contains(&StateKey::core_domain_records())
943        }) {
944            return Err(CanwuError::new(
945                ErrorCode::UndeclaredStateRead,
946                format!(
947                    "{} did not declare read access to {}.{}",
948                    self.reader.unwrap_or("internal system"),
949                    exact.namespace,
950                    exact.name
951                ),
952            ));
953        }
954        Ok(())
955    }
956
957    pub(super) fn finish_random_session(self) -> Option<random::RandomExecution> {
958        self.random_session
959            .map(RefCell::into_inner)
960            .map(random::RandomSession::finish)
961    }
962}
963
964impl super::PluginArchiveObjectProvider for SimulationView<'_> {
965    fn load_plugin_archive_object(
966        &self,
967        namespace: &str,
968        object_id: &str,
969    ) -> Result<Option<Vec<u8>>, CanwuError> {
970        self.plugin_archive_object(namespace, object_id)
971    }
972}