1#![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#[derive(Debug, Error, PartialEq, Eq)]
16pub enum EngineError {
17 #[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 #[error("duplicate definition package `{0}`")]
29 DuplicateDefinitionPackage(String),
30 #[error("unknown capability `{0}`")]
32 UnknownCapability(String),
33 #[error("duplicate capability `{0}`")]
35 DuplicateCapability(String),
36 #[error("capability `{capability}` does not declare parameter `{parameter}`")]
38 UnknownParameter {
39 capability: String,
40 parameter: String,
41 },
42 #[error("capability `{capability}` requires parameter `{parameter}`")]
44 MissingParameter {
45 capability: String,
46 parameter: String,
47 },
48 #[error("capability `{capability}` parameter `{parameter}` has invalid type")]
50 InvalidParameterType {
51 capability: String,
52 parameter: String,
53 },
54 #[error("rule has duplicate parameter binding `{0}`")]
56 DuplicateBinding(String),
57 #[error("invalid rule id `{0}`")]
59 InvalidRuleId(String),
60 #[error("unknown rule definition `{0}`")]
62 UnknownDefinition(String),
63 #[error("missing definition package `{0}`")]
65 MissingDefinitionPackage(String),
66 #[error("definition `{definition}` conflicts with capability `{capability}`: {detail}")]
68 CapabilityContract {
69 definition: String,
70 capability: String,
71 detail: String,
72 },
73 #[error("duplicate rule id `{0}`")]
75 DuplicateRule(String),
76}
77
78#[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#[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 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 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#[derive(Clone, Debug, PartialEq)]
150pub struct CompiledRule {
151 pub id: RuleId,
153 pub capability: String,
155 pub severity: schema::Severity,
157 pub selector: schema::Selector,
159 pub parameters: BTreeMap<String, schema::ParameterValue>,
161}
162
163pub struct RuleContext<'a> {
165 pub project: &'a Project,
167 pub services: &'a ServiceRegistry,
169}
170
171#[derive(Clone, Debug, Default, PartialEq)]
173pub struct CapabilityEvaluation {
174 findings: Vec<Finding>,
175 not_evaluated: Vec<CapabilityNotEvaluated>,
176}
177#[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 #[must_use]
202 pub fn findings(&self) -> &[Finding] {
203 &self.findings
204 }
205 #[must_use]
207 pub fn not_evaluated_outcomes(&self) -> &[CapabilityNotEvaluated] {
208 &self.not_evaluated
209 }
210 #[must_use]
212 pub fn evaluated(findings: Vec<Finding>) -> Self {
213 Self {
214 findings,
215 not_evaluated: Vec::new(),
216 }
217 }
218 #[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 pub fn push_finding(&mut self, finding: Finding) {
227 self.findings.push(finding);
228 }
229 pub fn push_not_evaluated(&mut self, reason: NotEvaluatedReason, message: impl Into<String>) {
231 self.push_unavailable(None, reason, message);
232 }
233 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
256pub trait RuleCapability: Send + Sync {
258 fn id(&self) -> &'static str;
260 fn parameters(&self) -> Vec<ParameterDescriptor>;
262 fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation;
264}
265
266#[derive(Clone, Default)]
268pub struct CapabilityRegistry {
269 capabilities: BTreeMap<String, Arc<dyn RuleCapability>>,
270}
271impl CapabilityRegistry {
272 pub fn new() -> Self {
274 Self::default()
275 }
276 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 pub fn get(&self, id: &str) -> Option<&Arc<dyn RuleCapability>> {
293 self.capabilities.get(id)
294 }
295}
296
297#[derive(Clone, Debug)]
299pub struct ExecutionPlan {
300 rules: Vec<CompiledRule>,
301}
302impl ExecutionPlan {
303 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
351pub struct Runtime {
353 registry: CapabilityRegistry,
354 services: ServiceRegistry,
355}
356impl Runtime {
357 pub fn new(registry: CapabilityRegistry) -> Self {
359 Self {
360 registry,
361 services: ServiceRegistry::new(),
362 }
363 }
364 #[must_use]
366 pub fn with_services(mut self, services: ServiceRegistry) -> Self {
367 self.services = services;
368 self
369 }
370 pub fn run(&self, project: &Project, plan: ExecutionPlan) -> Result<Report, EngineError> {
375 self.run_with_services(project, &self.services, plan)
376 }
377
378 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}