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