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 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
376pub struct Runtime {
378 registry: CapabilityRegistry,
379 services: ServiceRegistry,
380}
381impl Runtime {
382 pub fn new(registry: CapabilityRegistry) -> Self {
384 Self {
385 registry,
386 services: ServiceRegistry::new(),
387 }
388 }
389 #[must_use]
391 pub fn with_services(mut self, services: ServiceRegistry) -> Self {
392 self.services = services;
393 self
394 }
395 pub fn run(&self, project: &Project, plan: ExecutionPlan) -> Result<Report, EngineError> {
400 self.run_with_services(project, &self.services, plan)
401 }
402
403 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}