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