axioval_engine/
lib.rs

1//! Trusted capability compilation and deterministic runtime.
2#![forbid(unsafe_code)]
3#![allow(missing_docs, clippy::missing_errors_doc)]
4
5use std::{collections::BTreeMap, sync::Arc};
6
7pub use axioval_ir::NotEvaluatedReason;
8use axioval_ir::contract as schema;
9use axioval_ir::{Finding, NotEvaluated, ObjectId, Project, Report, RuleId};
10use thiserror::Error;
11
12mod session;
13
14/// Errors while compiling untrusted declarations into a trusted execution plan.
15#[derive(Debug, Error, PartialEq, Eq)]
16pub enum EngineError {
17    /// A package declares a schema version this compiler does not implement.
18    #[error(
19        "unsupported schema version `{version}` for {package_kind} `{package_id}`; supported: {supported}"
20    )]
21    UnsupportedSchemaVersion {
22        package_kind: &'static str,
23        package_id: String,
24        version: String,
25        supported: &'static str,
26    },
27    /// Multiple supplied definition packages declared the same package identity.
28    #[error("duplicate definition package `{0}`")]
29    DuplicateDefinitionPackage(String),
30    /// A capability was not registered by the host.
31    #[error("unknown capability `{0}`")]
32    UnknownCapability(String),
33    /// Two trusted implementations claimed an ID.
34    #[error("duplicate capability `{0}`")]
35    DuplicateCapability(String),
36    /// A package supplied a non-declared parameter.
37    #[error("capability `{capability}` does not declare parameter `{parameter}`")]
38    UnknownParameter {
39        capability: String,
40        parameter: String,
41    },
42    /// A required parameter was absent.
43    #[error("capability `{capability}` requires parameter `{parameter}`")]
44    MissingParameter {
45        capability: String,
46        parameter: String,
47    },
48    /// A binding type did not conform to its descriptor.
49    #[error("capability `{capability}` parameter `{parameter}` has invalid type")]
50    InvalidParameterType {
51        capability: String,
52        parameter: String,
53    },
54    /// A rule binds a parameter more than once.
55    #[error("rule has duplicate parameter binding `{0}`")]
56    DuplicateBinding(String),
57    /// A rule ID violates the engine identity contract.
58    #[error("invalid rule id `{0}`")]
59    InvalidRuleId(String),
60    /// A rule references no loaded definition.
61    #[error("unknown rule definition `{0}`")]
62    UnknownDefinition(String),
63    /// A ruleset references a definition package that was not supplied.
64    #[error("missing definition package `{0}`")]
65    MissingDefinitionPackage(String),
66    /// A trusted capability descriptor conflicts with its portable definition.
67    #[error("definition `{definition}` conflicts with capability `{capability}`: {detail}")]
68    CapabilityContract {
69        definition: String,
70        capability: String,
71        detail: String,
72    },
73    /// Rule IDs must be unique throughout the recursive folder tree.
74    #[error("duplicate rule id `{0}`")]
75    DuplicateRule(String),
76}
77
78/// Supported declarative parameter types.
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum ParameterType {
81    Boolean,
82    Integer,
83    Number,
84    String,
85    Quantity,
86    Enum,
87    Reference,
88    ObjectTypeReference,
89    PropertyReference,
90    Selector,
91    StringList,
92    ReferenceList,
93}
94impl ParameterType {
95    fn accepts(self, value: &schema::ParameterValue) -> bool {
96        matches!(
97            (self, value),
98            (Self::Boolean, schema::ParameterValue::Boolean { .. })
99                | (Self::Integer, schema::ParameterValue::Integer { .. })
100                | (Self::Number, schema::ParameterValue::Number { .. })
101                | (Self::String, schema::ParameterValue::String { .. })
102                | (Self::Quantity, schema::ParameterValue::Quantity { .. })
103                | (Self::Enum, schema::ParameterValue::Enum { .. })
104                | (Self::Reference, schema::ParameterValue::Reference { .. })
105                | (
106                    Self::ObjectTypeReference,
107                    schema::ParameterValue::ObjectTypeReference { .. }
108                )
109                | (
110                    Self::PropertyReference,
111                    schema::ParameterValue::PropertyReference { .. }
112                )
113                | (Self::Selector, schema::ParameterValue::Selector { .. })
114                | (Self::StringList, schema::ParameterValue::StringList { .. })
115                | (
116                    Self::ReferenceList,
117                    schema::ParameterValue::ReferenceList { .. }
118                )
119        )
120    }
121}
122/// Trusted capability parameter descriptor.
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub struct ParameterDescriptor {
125    pub name: String,
126    pub parameter_type: ParameterType,
127    pub required: bool,
128}
129impl ParameterDescriptor {
130    /// Required parameter descriptor.
131    pub fn required(name: impl Into<String>, parameter_type: ParameterType) -> Self {
132        Self {
133            name: name.into(),
134            parameter_type,
135            required: true,
136        }
137    }
138    /// Optional parameter descriptor.
139    pub fn optional(name: impl Into<String>, parameter_type: ParameterType) -> Self {
140        Self {
141            name: name.into(),
142            parameter_type,
143            required: false,
144        }
145    }
146}
147
148/// One validated portable rule bound to trusted executable capability code.
149#[derive(Clone, Debug, PartialEq)]
150pub struct CompiledRule {
151    /// Package-local stable rule ID.
152    pub id: RuleId,
153    /// Registered capability ID.
154    pub capability: String,
155    /// Rule severity.
156    pub severity: schema::Severity,
157    /// Source-neutral applicability selector.
158    pub selector: schema::Selector,
159    /// Strictly validated parameter bindings.
160    pub parameters: BTreeMap<String, schema::ParameterValue>,
161}
162
163/// Source-neutral data and typed host services visible during one rule evaluation.
164pub struct RuleContext<'a> {
165    /// Immutable composed project view.
166    pub project: &'a Project,
167    /// Adapter-provided semantic and computational capabilities.
168    pub services: &'a ServiceRegistry,
169}
170
171/// Fail-closed output from one trusted capability evaluation.
172#[derive(Clone, Debug, Default, PartialEq)]
173pub struct CapabilityEvaluation {
174    findings: Vec<Finding>,
175    not_evaluated: Vec<CapabilityNotEvaluated>,
176}
177/// A not-evaluated outcome before the runtime binds its compiled rule ID.
178#[derive(Clone, Debug, PartialEq)]
179pub struct CapabilityNotEvaluated {
180    object_id: Option<ObjectId>,
181    reason: NotEvaluatedReason,
182    message: String,
183}
184impl CapabilityNotEvaluated {
185    #[must_use]
186    pub fn object_id(&self) -> Option<&ObjectId> {
187        self.object_id.as_ref()
188    }
189    #[must_use]
190    pub fn reason(&self) -> &NotEvaluatedReason {
191        &self.reason
192    }
193    #[must_use]
194    pub fn message(&self) -> &str {
195        &self.message
196    }
197}
198
199impl CapabilityEvaluation {
200    /// Conclusive findings emitted by this capability.
201    #[must_use]
202    pub fn findings(&self) -> &[Finding] {
203        &self.findings
204    }
205    /// Explicit fail-closed outcomes emitted by this capability.
206    #[must_use]
207    pub fn not_evaluated_outcomes(&self) -> &[CapabilityNotEvaluated] {
208        &self.not_evaluated
209    }
210    /// Creates a conclusive evaluation from zero or more findings.
211    #[must_use]
212    pub fn evaluated(findings: Vec<Finding>) -> Self {
213        Self {
214            findings,
215            not_evaluated: Vec::new(),
216        }
217    }
218    /// Creates a rule-level not-evaluated outcome.
219    #[must_use]
220    pub fn not_evaluated(reason: NotEvaluatedReason, message: impl Into<String>) -> Self {
221        let mut outcome = Self::default();
222        outcome.push_not_evaluated(reason, message);
223        outcome
224    }
225    /// Adds a conclusive finding.
226    pub fn push_finding(&mut self, finding: Finding) {
227        self.findings.push(finding);
228    }
229    /// Adds a rule-level not-evaluated outcome.
230    pub fn push_not_evaluated(&mut self, reason: NotEvaluatedReason, message: impl Into<String>) {
231        self.push_unavailable(None, reason, message);
232    }
233    /// Adds an object-specific not-evaluated outcome.
234    pub fn push_object_not_evaluated(
235        &mut self,
236        object_id: ObjectId,
237        reason: NotEvaluatedReason,
238        message: impl Into<String>,
239    ) {
240        self.push_unavailable(Some(object_id), reason, message);
241    }
242    fn push_unavailable(
243        &mut self,
244        object_id: Option<ObjectId>,
245        reason: NotEvaluatedReason,
246        message: impl Into<String>,
247    ) {
248        self.not_evaluated.push(CapabilityNotEvaluated {
249            object_id,
250            reason,
251            message: message.into(),
252        });
253    }
254}
255
256/// Trusted code selected by a package capability ID; packages never supply executable code.
257pub trait RuleCapability: Send + Sync {
258    /// Stable trusted capability ID.
259    fn id(&self) -> &'static str;
260    /// Strict accepted parameters.
261    fn parameters(&self) -> Vec<ParameterDescriptor>;
262    /// Evaluates an already-validated rule request.
263    fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation;
264}
265
266/// Host-controlled registry of trusted capabilities.
267#[derive(Clone, Default)]
268pub struct CapabilityRegistry {
269    capabilities: BTreeMap<String, Arc<dyn RuleCapability>>,
270}
271impl CapabilityRegistry {
272    /// Creates an empty registry.
273    pub fn new() -> Self {
274        Self::default()
275    }
276    /// Registers a capability; duplicate IDs are rejected.
277    pub fn register<C: RuleCapability + 'static>(
278        mut self,
279        capability: C,
280    ) -> Result<Self, EngineError> {
281        let id = capability.id().to_owned();
282        if self
283            .capabilities
284            .insert(id.clone(), Arc::new(capability))
285            .is_some()
286        {
287            return Err(EngineError::DuplicateCapability(id));
288        }
289        Ok(self)
290    }
291    /// Gets trusted code by exact ID.
292    pub fn get(&self, id: &str) -> Option<&Arc<dyn RuleCapability>> {
293        self.capabilities.get(id)
294    }
295}
296
297/// Validated, deterministic request plan.
298#[derive(Clone, Debug)]
299pub struct ExecutionPlan {
300    rules: Vec<CompiledRule>,
301}
302impl ExecutionPlan {
303    /// Rules ordered by stable rule ID.
304    pub fn rules(&self) -> &[CompiledRule] {
305        &self.rules
306    }
307}
308
309mod compiler;
310mod free_space;
311mod metric_routing;
312mod properties;
313mod relationships;
314mod services;
315mod topology;
316mod walkability;
317pub use compiler::{SUPPORTED_SCHEMA_VERSION, compile};
318pub use free_space::{
319    AreaInterval, BoxClearance, ClearanceOutcome, ClearancePlacementEvidence, ClearanceRequest,
320    ClearanceShape, CompleteClearanceEvidence, CompletePlacementEvidence, CompleteSupportEvidence,
321    CylinderClearance, FrameOffsetPlacement, FreeAreaEvidence, FreeAreaRequest, FreeSpaceError,
322    FreeSpaceService, FreeSpaceServiceHandle, MetricDirection, MetricFrame, ObstructionEvidence,
323    PlacementDomain, PlacementOutcome, PlacementRequest, SignedDistanceInterval,
324    SupportedPlacement,
325};
326pub use metric_routing::{
327    BlockedMetricRouteEvidence, CompleteMetricEvidence, LengthInterval, MetricPoint,
328    MetricRouteEvidence, MetricRouteOutcome, MetricRouteRequest, MetricRoutingError,
329    MetricRoutingService, MetricRoutingServiceHandle, MobilityProfile, ThresholdVerdict,
330};
331pub use properties::{
332    CompletePropertyAbsenceEvidence, PropertyRequest, PropertyResolution, PropertyResolutionError,
333    PropertyResolutionService, PropertyResolutionServiceHandle, ResolvedProperty,
334};
335pub use relationships::{
336    CompleteRelationshipSelection, RelationshipQuery, RelationshipSelectionError,
337    RelationshipSelectionRequest, RelationshipSelectionService, RelationshipSelectionServiceHandle,
338    SemanticRelationship, TraversalDirection,
339};
340pub use services::{ServiceRegistry, ServiceRegistryError};
341pub use session::{EvidenceSession, EvidenceSessionError, SnapshotBoundService, SourceSnapshot};
342pub use topology::{
343    CompleteTopologyEvidence, ConnectivityGraph, RouteOutcome, TopologyError, VerifiedConnection,
344};
345pub use walkability::{
346    VerifiedWalkablePassage, WalkabilityError, WalkabilityRegion, WalkabilityRegionId,
347    WalkabilityRequest, WalkabilityRouteOutcome, WalkabilityService, WalkabilityServiceHandle,
348    WalkabilitySnapshot,
349};
350
351/// Deterministic runtime that invokes only registered trusted capabilities.
352pub struct Runtime {
353    registry: CapabilityRegistry,
354    services: ServiceRegistry,
355}
356impl Runtime {
357    /// Creates a runtime from a host-controlled registry.
358    pub fn new(registry: CapabilityRegistry) -> Self {
359        Self {
360            registry,
361            services: ServiceRegistry::new(),
362        }
363    }
364    /// Adds adapter-provided host services to subsequent evaluations.
365    #[must_use]
366    pub fn with_services(mut self, services: ServiceRegistry) -> Self {
367        self.services = services;
368        self
369    }
370    /// Executes a plan and returns deterministically sorted findings.
371    ///
372    /// Execution fails closed if the host registry no longer contains any capability
373    /// that was present when the plan was compiled.
374    pub fn run(&self, project: &Project, plan: ExecutionPlan) -> Result<Report, EngineError> {
375        self.run_with_services(project, &self.services, plan)
376    }
377
378    /// Executes a plan against one immutable source/evidence snapshot.
379    pub fn run_session(
380        &self,
381        session: &EvidenceSession,
382        plan: ExecutionPlan,
383    ) -> Result<Report, EngineError> {
384        self.run_with_services(session.project(), session.services(), plan)
385    }
386
387    fn run_with_services(
388        &self,
389        project: &Project,
390        services: &ServiceRegistry,
391        plan: ExecutionPlan,
392    ) -> Result<Report, EngineError> {
393        let context = RuleContext { project, services };
394        let mut findings = Vec::new();
395        let mut not_evaluated = Vec::new();
396        for rule in plan.rules {
397            let capability = self
398                .registry
399                .get(&rule.capability)
400                .ok_or_else(|| EngineError::UnknownCapability(rule.capability.clone()))?;
401            let rule_id = rule.id.clone();
402            let evaluation = capability.evaluate(&context, &rule);
403            findings.extend(evaluation.findings);
404            not_evaluated.extend(evaluation.not_evaluated.into_iter().map(|outcome| {
405                NotEvaluated {
406                    rule_id: rule_id.clone(),
407                    object_id: outcome.object_id,
408                    reason: outcome.reason,
409                    message: outcome.message,
410                }
411            }));
412        }
413        findings.sort_by(|a, b| {
414            a.rule_id
415                .cmp(&b.rule_id)
416                .then_with(|| a.object_id.cmp(&b.object_id))
417                .then_with(|| a.message.cmp(&b.message))
418        });
419        not_evaluated.sort();
420        Ok(Report {
421            findings,
422            not_evaluated,
423        })
424    }
425}