Skip to main content

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 linear_quantity;
312mod metric_routing;
313mod properties;
314mod relationships;
315mod services;
316mod topology;
317mod walkability;
318pub use compiler::{SUPPORTED_SCHEMA_VERSION, compile};
319pub use free_space::{
320    AreaInterval, BoxClearance, ClearanceOutcome, ClearancePlacementEvidence, ClearanceRequest,
321    ClearanceShape, CompleteClearanceEvidence, CompletePlacementEvidence, CompleteSupportEvidence,
322    CylinderClearance, FrameOffsetPlacement, FreeAreaEvidence, FreeAreaRequest, FreeSpaceError,
323    FreeSpaceService, FreeSpaceServiceHandle, MetricDirection, MetricFrame, ObstructionEvidence,
324    PlacementDomain, PlacementOutcome, PlacementRequest, SignedDistanceInterval,
325    SupportedPlacement,
326};
327pub use linear_quantity::{
328    LinearInterval, LinearQuantityError, LinearQuantityEvidence, LinearQuantityKind,
329    LinearQuantityRequest, LinearQuantityService, LinearQuantityServiceHandle, ShelfGeometry,
330};
331pub use metric_routing::{
332    BlockedMetricRouteEvidence, CompleteMetricEvidence, LengthInterval, MetricPoint,
333    MetricRouteEvidence, MetricRouteOutcome, MetricRouteRequest, MetricRoutingError,
334    MetricRoutingService, MetricRoutingServiceHandle, MobilityProfile, ThresholdVerdict,
335};
336pub use properties::{
337    CompletePropertyAbsenceEvidence, PropertyRequest, PropertyResolution, PropertyResolutionError,
338    PropertyResolutionService, PropertyResolutionServiceHandle, ResolvedProperty,
339};
340pub use relationships::{
341    CompleteRelationshipSelection, RelationshipQuery, RelationshipSelectionError,
342    RelationshipSelectionRequest, RelationshipSelectionService, RelationshipSelectionServiceHandle,
343    SemanticRelationship, TraversalDirection,
344};
345pub use services::{ServiceRegistry, ServiceRegistryError};
346pub use session::{EvidenceSession, EvidenceSessionError, SnapshotBoundService, SourceSnapshot};
347pub use topology::{
348    CompleteTopologyEvidence, ConnectivityGraph, RouteOutcome, TopologyError, VerifiedConnection,
349};
350pub use walkability::{
351    VerifiedWalkablePassage, WalkabilityError, WalkabilityRegion, WalkabilityRegionId,
352    WalkabilityRequest, WalkabilityRouteOutcome, WalkabilityService, WalkabilityServiceHandle,
353    WalkabilitySnapshot,
354};
355
356/// Deterministic runtime that invokes only registered trusted capabilities.
357pub struct Runtime {
358    registry: CapabilityRegistry,
359    services: ServiceRegistry,
360}
361impl Runtime {
362    /// Creates a runtime from a host-controlled registry.
363    pub fn new(registry: CapabilityRegistry) -> Self {
364        Self {
365            registry,
366            services: ServiceRegistry::new(),
367        }
368    }
369    /// Adds adapter-provided host services to subsequent evaluations.
370    #[must_use]
371    pub fn with_services(mut self, services: ServiceRegistry) -> Self {
372        self.services = services;
373        self
374    }
375    /// Executes a plan and returns deterministically sorted findings.
376    ///
377    /// Execution fails closed if the host registry no longer contains any capability
378    /// that was present when the plan was compiled.
379    pub fn run(&self, project: &Project, plan: ExecutionPlan) -> Result<Report, EngineError> {
380        self.run_with_services(project, &self.services, plan)
381    }
382
383    /// Executes a plan against one immutable source/evidence snapshot.
384    pub fn run_session(
385        &self,
386        session: &EvidenceSession,
387        plan: ExecutionPlan,
388    ) -> Result<Report, EngineError> {
389        self.run_with_services(session.project(), session.services(), plan)
390    }
391
392    fn run_with_services(
393        &self,
394        project: &Project,
395        services: &ServiceRegistry,
396        plan: ExecutionPlan,
397    ) -> Result<Report, EngineError> {
398        let context = RuleContext { project, services };
399        let mut findings = Vec::new();
400        let mut not_evaluated = Vec::new();
401        for rule in plan.rules {
402            let capability = self
403                .registry
404                .get(&rule.capability)
405                .ok_or_else(|| EngineError::UnknownCapability(rule.capability.clone()))?;
406            let rule_id = rule.id.clone();
407            let evaluation = capability.evaluate(&context, &rule);
408            findings.extend(evaluation.findings);
409            not_evaluated.extend(evaluation.not_evaluated.into_iter().map(|outcome| {
410                NotEvaluated {
411                    rule_id: rule_id.clone(),
412                    object_id: outcome.object_id,
413                    reason: outcome.reason,
414                    message: outcome.message,
415                }
416            }));
417        }
418        findings.sort_by(|a, b| {
419            a.rule_id
420                .cmp(&b.rule_id)
421                .then_with(|| a.object_id.cmp(&b.object_id))
422                .then_with(|| a.message.cmp(&b.message))
423        });
424        not_evaluated.sort();
425        Ok(Report {
426            findings,
427            not_evaluated,
428        })
429    }
430}