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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}