Skip to main content

sim_incremental_core/projection/
model.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    error::Error,
4    fmt,
5    sync::Mutex,
6};
7
8use sim_kernel::{ContentId, Datum};
9
10macro_rules! string_id {
11    ($name:ident, $doc:literal) => {
12        #[doc = $doc]
13        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14        pub struct $name(String);
15
16        impl $name {
17            /// Constructs an identifier after rejecting an empty spelling.
18            pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
19                let value = value.into();
20                if value.trim().is_empty() {
21                    return Err(ProjectionError::InvalidIdentifier(stringify!($name)));
22                }
23                Ok(Self(value))
24            }
25
26            /// Returns the stable identifier spelling.
27            #[must_use]
28            pub fn as_str(&self) -> &str {
29                &self.0
30            }
31        }
32
33        impl fmt::Display for $name {
34            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35                self.0.fmt(formatter)
36            }
37        }
38    };
39}
40
41string_id!(FactId, "Stable identity of one semantic observed fact.");
42string_id!(
43    ConclusionId,
44    "Stable identity of a conclusion consuming facts."
45);
46string_id!(
47    ProjectionKindRef,
48    "Open identifier of a loaded projection kind."
49);
50
51/// Exact package and implementation identity of a projection provider.
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct PackageIdentity {
54    /// Registry package name.
55    pub name: String,
56    /// Semantic version used by the provider.
57    pub version: String,
58    /// Content identity of the exact loaded implementation.
59    pub code: ContentId,
60}
61
62/// One fact with a semantic value and a diagnostic envelope.
63///
64/// Only `semantic` enters projection identity. The envelope may carry timing,
65/// retries, path aliases, broker location, or logs without invalidating reuse.
66#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct ObservedFact {
68    /// Canonical value available to a projector.
69    pub semantic: Datum,
70    /// Diagnostic data retained outside semantic identity.
71    pub envelope: Option<Datum>,
72}
73
74/// Immutable observed facts from which a selector creates a bounded view.
75#[derive(Clone, Debug, Default, Eq, PartialEq)]
76pub struct ObservedWorld {
77    facts: BTreeMap<FactId, ObservedFact>,
78}
79
80impl ObservedWorld {
81    /// Builds a world and refuses duplicate fact identities.
82    pub fn new(
83        facts: impl IntoIterator<Item = (FactId, ObservedFact)>,
84    ) -> Result<Self, ProjectionError> {
85        let mut world = Self::default();
86        for (id, fact) in facts {
87            if world.facts.insert(id.clone(), fact).is_some() {
88                return Err(ProjectionError::DuplicateFact(id));
89            }
90        }
91        Ok(world)
92    }
93
94    pub(crate) fn select(
95        &self,
96        selector: &DeclaredInputSelector,
97    ) -> Result<ProjectionInputs, ProjectionError> {
98        let mut selected = BTreeMap::new();
99        for id in &selector.facts {
100            let fact = self
101                .facts
102                .get(id)
103                .ok_or_else(|| ProjectionError::MissingFact(id.clone()))?;
104            selected.insert(id.clone(), fact.semantic.clone());
105        }
106        Ok(ProjectionInputs {
107            facts: selected,
108            accessed: Mutex::new(BTreeSet::new()),
109        })
110    }
111}
112
113/// Closed set of facts admitted as one provider invocation's entire input.
114#[derive(Clone, Debug, Default, Eq, PartialEq)]
115pub struct DeclaredInputSelector {
116    facts: BTreeSet<FactId>,
117}
118
119impl DeclaredInputSelector {
120    /// Builds a canonical selector.
121    #[must_use]
122    pub fn new(facts: impl IntoIterator<Item = FactId>) -> Self {
123        Self {
124            facts: facts.into_iter().collect(),
125        }
126    }
127
128    /// Returns selected fact identities in canonical order.
129    pub fn facts(&self) -> impl ExactSizeIterator<Item = &FactId> {
130        self.facts.iter()
131    }
132}
133
134/// Bounded immutable input view supplied to a projector.
135///
136/// Its fields are private so a provider cannot reach the rest of the world or
137/// any diagnostic envelope. Every successful read is recorded.
138#[derive(Debug)]
139pub struct ProjectionInputs {
140    facts: BTreeMap<FactId, Datum>,
141    accessed: Mutex<BTreeSet<FactId>>,
142}
143
144impl ProjectionInputs {
145    /// Reads a selected fact and records the access.
146    pub fn get(&self, id: &FactId) -> Option<&Datum> {
147        let value = self.facts.get(id)?;
148        self.accessed
149            .lock()
150            .expect("projection access mutex poisoned")
151            .insert(id.clone());
152        Some(value)
153    }
154
155    /// Iterates over every selected fact and records each access.
156    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&FactId, &Datum)> {
157        self.accessed
158            .lock()
159            .expect("projection access mutex poisoned")
160            .extend(self.facts.keys().cloned());
161        self.facts.iter()
162    }
163
164    pub(crate) fn accessed(&self) -> BTreeSet<FactId> {
165        self.accessed
166            .lock()
167            .expect("projection access mutex poisoned")
168            .clone()
169    }
170}
171
172/// Canonical provider output and its declared fact dependencies.
173#[derive(Clone, Debug, Eq, PartialEq)]
174pub struct ProjectionOutput {
175    /// Canonical semantic projection value.
176    pub value: Datum,
177    /// Exact facts on which the value depends.
178    pub dependencies: BTreeSet<FactId>,
179}
180
181/// Loaded implementation of an open projection kind.
182pub trait ProjectionProvider: Send + Sync {
183    /// Open kind implemented by this provider.
184    fn kind(&self) -> &ProjectionKindRef;
185    /// Stable Shape identity used to check configuration before invocation.
186    fn config_shape(&self) -> &ContentId;
187    /// Projects solely over the bounded immutable input view.
188    fn project(
189        &self,
190        inputs: &ProjectionInputs,
191        config: &Datum,
192    ) -> Result<ProjectionOutput, ProjectionError>;
193}
194
195/// Checked request for one loaded projection.
196#[derive(Clone, Debug, Eq, PartialEq)]
197pub struct ProjectionSpec {
198    /// Stable request identity.
199    pub id: ContentId,
200    /// Open loaded provider kind.
201    pub kind: ProjectionKindRef,
202    /// Provider-specific configuration value.
203    pub config: Datum,
204    /// Shape that must match the loaded provider's declared config Shape.
205    pub config_shape: ContentId,
206    /// Exact provider package and code identity.
207    pub provider: PackageIdentity,
208}
209
210/// Fuel, memory, output, and selected-input ceilings for projection.
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212pub struct ProjectionBudget {
213    /// Maximum selected facts.
214    pub max_inputs: usize,
215    /// Maximum canonical output bytes.
216    pub max_output_bytes: usize,
217    /// Maximum wasm fuel, when the closed wasm route is used.
218    pub max_fuel: u64,
219    /// Maximum wasm linear-memory bytes.
220    pub max_memory_bytes: usize,
221}
222
223/// Closed deterministic imports made available to a wasm projector.
224#[derive(Clone, Debug, Default, Eq, PartialEq)]
225pub struct DeterministicImportManifest {
226    /// Fully qualified `module/name` imports in canonical order.
227    pub imports: BTreeSet<String>,
228}
229
230/// Runtime semantics that affect deterministic projection.
231#[derive(Clone, Debug, Eq, PartialEq)]
232pub struct ExecutionSemantics {
233    /// Stable semantics family and version.
234    pub id: String,
235    /// Whether canonical NaN behavior is fixed.
236    pub canonical_nan: bool,
237    /// Whether collection traversal order is fixed.
238    pub canonical_collections: bool,
239    /// Whether every invocation starts with fresh mutable instance state.
240    pub fresh_instance: bool,
241}
242
243/// Admission policy bound before projector qualification.
244#[derive(Clone, Debug, Eq, PartialEq)]
245pub struct ProjectorPolicy {
246    /// Semantic identity of the input Shape.
247    pub input_shape: ContentId,
248    /// Complete selected input universe.
249    pub reads: DeclaredInputSelector,
250    /// Closed deterministic import universe.
251    pub imports: DeterministicImportManifest,
252    /// Deterministic runtime semantics.
253    pub execution: ExecutionSemantics,
254    /// Bounded work and output policy.
255    pub budgets: ProjectionBudget,
256    /// Whether effect confinement is independently required on this host.
257    pub requires_confinement: bool,
258}
259
260/// Source closure admitted for a trusted native projector.
261#[derive(Clone, Debug, Eq, PartialEq)]
262pub struct QualifiedSourceClosure {
263    /// Exact implementation content identity.
264    pub code: ContentId,
265    /// Exact transitive runtime dependency closure identity.
266    pub dependencies: ContentId,
267    /// Independent source/dependency review evidence identity.
268    pub review: ContentId,
269}
270
271/// Runtime admitted for the closed wasm route.
272#[derive(Clone, Debug, Eq, PartialEq)]
273pub struct QualifiedRuntime {
274    /// Exact runtime implementation identity.
275    pub code: ContentId,
276    /// Qualified execution semantics.
277    pub semantics: ExecutionSemantics,
278}
279
280/// One of the two projector admission routes allowed by the roadmap.
281///
282/// This is a wrapper around a crate-private variant enum, not a public enum
283/// itself: `ProjectorQualificationKind` cannot be named from outside
284/// `sim-incremental-core`, so no external caller can write a
285/// `ProjectorQualification` struct literal or match into its variants.
286/// The only way to obtain one is through this crate's own admission logic
287/// (`admission::trusted_native`/`closed_wasm`, both `pub(crate)`, reached
288/// only from inside `ProjectionEngine::project` itself, at the moment of
289/// dispatch). A caller can hold this value (it appears in
290/// [`ProjectionResult::projector_qualification`]) and clone it, but never
291/// assemble one from its own claimed evidence, and never pass one back in:
292/// `project` does not accept a qualification argument.
293#[derive(Clone, Debug, Eq, PartialEq)]
294pub struct ProjectorQualification(pub(crate) ProjectorQualificationKind);
295
296#[derive(Clone, Debug, Eq, PartialEq)]
297pub(crate) enum ProjectorQualificationKind {
298    /// Exact reviewed native code and dependency closure.
299    TrustedNative {
300        /// Qualified source closure.
301        source: QualifiedSourceClosure,
302        /// Policy identity reviewed with that source.
303        policy: ContentId,
304    },
305    /// Closed wasm module with verified imports and runtime behavior.
306    ///
307    /// Unconstructed by any real caller yet; see `admission::closed_wasm`'s
308    /// doc comment for why this is a tracked deferral, not dead code.
309    #[allow(dead_code)]
310    ClosedWasm {
311        /// Exact semantic module identity.
312        module: ContentId,
313        /// Policy identity used for admission.
314        policy: ContentId,
315        /// Qualified runtime and semantics.
316        runtime: QualifiedRuntime,
317        /// Verified complete import manifest.
318        imports: DeterministicImportManifest,
319        /// Admission evidence identity.
320        admission: ContentId,
321    },
322}
323
324impl ProjectorQualification {
325    pub(crate) fn implementation(&self) -> &ContentId {
326        match &self.0 {
327            ProjectorQualificationKind::TrustedNative { source, .. } => &source.code,
328            ProjectorQualificationKind::ClosedWasm { module, .. } => module,
329        }
330    }
331
332    pub(crate) fn policy(&self) -> &ContentId {
333        match &self.0 {
334            ProjectorQualificationKind::TrustedNative { policy, .. }
335            | ProjectorQualificationKind::ClosedWasm { policy, .. } => policy,
336        }
337    }
338
339    #[cfg(test)]
340    pub(crate) const fn is_trusted_native(&self) -> bool {
341        matches!(self.0, ProjectorQualificationKind::TrustedNative { .. })
342    }
343}
344
345/// Independent bounded-effect confinement evidence.
346#[derive(Clone, Debug, Eq, PartialEq)]
347pub struct ConfinementEvidence {
348    /// Membrane implementation identity.
349    pub membrane: String,
350    /// Exact bounded policy identity.
351    pub policy: ContentId,
352    /// Live host readiness was probed at dispatch.
353    pub live: bool,
354}
355
356/// Exact facts read through the bounded projection input view.
357#[derive(Clone, Debug, Eq, PartialEq)]
358pub struct MediatedAccessWitness {
359    /// Facts selected by policy.
360    pub selected: BTreeSet<FactId>,
361    /// Facts actually read by the provider.
362    pub accessed: BTreeSet<FactId>,
363}
364
365/// One causal path from requested conclusion to changed leaf fact.
366#[derive(Clone, Debug, Eq, PartialEq)]
367pub struct Explanation {
368    /// Requested conclusion.
369    pub conclusion: ConclusionId,
370    /// Changed fact on which it depends.
371    pub fact: FactId,
372    /// Ordered owner-local path including conclusion and fact endpoints.
373    pub path: Vec<String>,
374}
375
376/// Durable semantic digest of one qualified projection.
377#[derive(Clone, Debug, Eq, PartialEq)]
378pub struct ProjectionDigest(pub ContentId);
379
380/// Complete checked result of one provider invocation.
381#[derive(Clone, Debug, Eq, PartialEq)]
382pub struct ProjectionResult {
383    /// Canonical semantic projection.
384    pub projection: Datum,
385    /// Exact selected and accessed facts.
386    pub mediated_access: MediatedAccessWitness,
387    /// Qualification used for this invocation.
388    pub projector_qualification: ProjectorQualification,
389    /// Independent confinement evidence, when required.
390    pub confinement: Option<ConfinementEvidence>,
391    /// Durable semantic identity excluding diagnostic envelopes.
392    pub digest: ProjectionDigest,
393    /// Conclusions affected by the consumed facts.
394    pub affected: Vec<ConclusionId>,
395    /// Causal explanations for affected conclusion/fact pairs.
396    pub explanations: Vec<Explanation>,
397}
398
399/// Fail-closed projection refusal with distinct policy boundaries.
400#[derive(Clone, Debug, Eq, PartialEq)]
401pub enum ProjectionError {
402    /// An identifier was empty.
403    InvalidIdentifier(&'static str),
404    /// A world declared the same fact twice.
405    DuplicateFact(FactId),
406    /// A selected fact was absent.
407    MissingFact(FactId),
408    /// No loaded provider owns the requested open kind.
409    UnknownProvider(ProjectionKindRef),
410    /// More than one loaded provider claimed the same kind.
411    DuplicateProvider(ProjectionKindRef),
412    /// Loaded provider and requested config Shape disagree.
413    ConfigShapeMismatch,
414    /// Configuration did not satisfy its declared Shape.
415    InvalidConfig(String),
416    /// A path selector or logical path was not canonical or valid.
417    InvalidPathSelection(String),
418    /// Provider code identity differs from qualified loaded code.
419    CodeIdentityMismatch,
420    /// Projector qualification is missing or invalid.
421    UnqualifiedProjector(String),
422    /// Required confinement is absent or unavailable.
423    UnavailableConfinement(String),
424    /// Provider read and dependency declarations disagree.
425    UndeclaredAccess {
426        /// Fact read without a matching dependency claim.
427        accessed: FactId,
428    },
429    /// Provider claimed a dependency it never read.
430    UnreadDependency(FactId),
431    /// A configured budget was exceeded.
432    BudgetExceeded(&'static str),
433    /// Canonical projection identity could not be constructed.
434    Canonical(String),
435    /// A requested explanation path does not exist.
436    MissingExplanation {
437        /// Requested conclusion.
438        conclusion: ConclusionId,
439        /// Requested leaf fact.
440        fact: FactId,
441    },
442}
443
444impl fmt::Display for ProjectionError {
445    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
446        write!(formatter, "{self:?}")
447    }
448}
449
450impl Error for ProjectionError {}