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 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
356pub struct Runtime {
358 registry: CapabilityRegistry,
359 services: ServiceRegistry,
360}
361impl Runtime {
362 pub fn new(registry: CapabilityRegistry) -> Self {
364 Self {
365 registry,
366 services: ServiceRegistry::new(),
367 }
368 }
369 #[must_use]
371 pub fn with_services(mut self, services: ServiceRegistry) -> Self {
372 self.services = services;
373 self
374 }
375 pub fn run(&self, project: &Project, plan: ExecutionPlan) -> Result<Report, EngineError> {
380 self.run_with_services(project, &self.services, plan)
381 }
382
383 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}