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