1#![forbid(unsafe_code)]
3#![allow(missing_docs, clippy::missing_errors_doc)]
4
5use std::{
6 collections::{BTreeMap, BTreeSet},
7 sync::Arc,
8};
9
10pub use axioval_ir::NotEvaluatedReason;
11use axioval_ir::contract as schema;
12use axioval_ir::{
13 Finding, Location, NotEvaluated, ObjectId, Project, Report, ReportTable, RuleId, RuleSummary,
14 Scope, SourceId,
15};
16use thiserror::Error;
17
18mod session;
19
20#[derive(Debug, Error, PartialEq, Eq)]
22pub enum EngineError {
23 #[error(
25 "unsupported schema version `{version}` for {package_kind} `{package_id}`; supported: {supported}"
26 )]
27 UnsupportedSchemaVersion {
28 package_kind: &'static str,
29 package_id: String,
30 version: String,
31 supported: &'static str,
32 },
33 #[error("duplicate definition package `{0}`")]
35 DuplicateDefinitionPackage(String),
36 #[error("unknown capability `{0}`")]
38 UnknownCapability(String),
39 #[error("duplicate capability `{0}`")]
41 DuplicateCapability(String),
42 #[error("capability `{capability}` does not declare parameter `{parameter}`")]
44 UnknownParameter {
45 capability: String,
46 parameter: String,
47 },
48 #[error("capability `{capability}` requires parameter `{parameter}`")]
50 MissingParameter {
51 capability: String,
52 parameter: String,
53 },
54 #[error("capability `{capability}` parameter `{parameter}` has invalid type")]
56 InvalidParameterType {
57 capability: String,
58 parameter: String,
59 },
60 #[error("capability `{capability}` parameter `{parameter}` row {row}: {detail}")]
62 InvalidTableRow {
63 capability: String,
64 parameter: String,
65 row: usize,
67 detail: String,
68 },
69 #[error("rule has duplicate parameter binding `{0}`")]
71 DuplicateBinding(String),
72 #[error("invalid rule id `{0}`")]
74 InvalidRuleId(String),
75 #[error("unknown rule definition `{0}`")]
77 UnknownDefinition(String),
78 #[error("missing definition package `{0}`")]
80 MissingDefinitionPackage(String),
81 #[error("definition `{definition}` conflicts with capability `{capability}`: {detail}")]
83 CapabilityContract {
84 definition: String,
85 capability: String,
86 detail: String,
87 },
88 #[error("duplicate rule id `{0}`")]
90 DuplicateRule(String),
91 #[error("duplicate ruleset package `{0}`")]
93 DuplicateRuleSet(String),
94 #[error("no ruleset to compile")]
96 NoRuleSet,
97 #[error("duplicate concept `{0}` across definition packages")]
99 DuplicateConcept(String),
100 #[error("rule `{rule}` references unknown {kind} concept `{concept}`")]
102 UnknownConcept {
103 rule: String,
104 kind: String,
105 concept: String,
106 },
107 #[error("rule `{rule}` reported table `{table}` twice")]
109 DuplicateReportTable { rule: String, table: String },
110 #[error("rule `{rule}`: {detail}")]
114 InvalidRefinement { rule: String, detail: String },
115 #[error("{0} needs an outcome refiner, and the host registered none")]
118 MissingRefiner(&'static str),
119 #[error("rule `{rule}`: {detail}")]
123 InvalidDependency { rule: String, detail: String },
124 #[error("classification `{classification}`: {detail}")]
129 InvalidClassification {
130 classification: String,
131 detail: String,
132 },
133}
134
135pub use schema::ColumnKind;
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
142pub struct TableColumn {
143 pub id: &'static str,
144 pub kind: ColumnKind,
145 pub required: bool,
146}
147impl TableColumn {
148 #[must_use]
150 pub const fn required(id: &'static str, kind: ColumnKind) -> Self {
151 Self {
152 id,
153 kind,
154 required: true,
155 }
156 }
157 #[must_use]
159 pub const fn optional(id: &'static str, kind: ColumnKind) -> Self {
160 Self {
161 id,
162 kind,
163 required: false,
164 }
165 }
166}
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170pub enum ParameterType {
171 Boolean,
172 Integer,
173 Number,
174 String,
175 Quantity,
176 Enum,
177 Date,
179 DateTime,
182 Reference,
183 ObjectTypeReference,
184 PropertyReference,
185 Selector,
186 StringList,
187 ReferenceList,
188 Table(&'static [TableColumn]),
190}
191impl ParameterType {
192 #[must_use]
194 pub fn package_kind(self) -> &'static str {
195 match self {
196 Self::Boolean => "boolean",
197 Self::Integer => "integer",
198 Self::Number => "number",
199 Self::String => "string",
200 Self::Quantity => "quantity",
201 Self::Enum => "enum",
202 Self::Date => "date",
203 Self::DateTime => "dateTime",
204 Self::Reference => "reference",
205 Self::ObjectTypeReference => "objectTypeReference",
206 Self::PropertyReference => "propertyReference",
207 Self::Selector => "selector",
208 Self::StringList => "stringList",
209 Self::ReferenceList => "referenceList",
210 Self::Table(_) => "table",
211 }
212 }
213 fn accepts(self, value: &schema::ParameterValue) -> bool {
214 matches!(
215 (self, value),
216 (Self::Boolean, schema::ParameterValue::Boolean { .. })
217 | (Self::Integer, schema::ParameterValue::Integer { .. })
218 | (Self::Number, schema::ParameterValue::Number { .. })
219 | (Self::String, schema::ParameterValue::String { .. })
220 | (Self::Quantity, schema::ParameterValue::Quantity { .. })
221 | (Self::Enum, schema::ParameterValue::Enum { .. })
222 | (Self::Date, schema::ParameterValue::Date { .. })
223 | (Self::DateTime, schema::ParameterValue::DateTime { .. })
224 | (Self::Reference, schema::ParameterValue::Reference { .. })
225 | (
226 Self::ObjectTypeReference,
227 schema::ParameterValue::ObjectTypeReference { .. }
228 )
229 | (
230 Self::PropertyReference,
231 schema::ParameterValue::PropertyReference { .. }
232 )
233 | (Self::Selector, schema::ParameterValue::Selector { .. })
234 | (Self::StringList, schema::ParameterValue::StringList { .. })
235 | (
236 Self::ReferenceList,
237 schema::ParameterValue::ReferenceList { .. }
238 )
239 | (Self::Table(_), schema::ParameterValue::Table { .. })
240 )
241 }
242}
243#[derive(Clone, Debug, Eq, PartialEq)]
245pub struct ParameterDescriptor {
246 pub name: String,
247 pub parameter_type: ParameterType,
248 pub required: bool,
249}
250impl ParameterDescriptor {
251 pub fn required(name: impl Into<String>, parameter_type: ParameterType) -> Self {
253 Self {
254 name: name.into(),
255 parameter_type,
256 required: true,
257 }
258 }
259 pub fn optional(name: impl Into<String>, parameter_type: ParameterType) -> Self {
261 Self {
262 name: name.into(),
263 parameter_type,
264 required: false,
265 }
266 }
267}
268
269#[derive(Clone, Debug, PartialEq)]
271pub struct CompiledRule {
272 pub id: RuleId,
274 pub capability: String,
276 pub severity: schema::Severity,
278 pub selector: schema::Selector,
280 pub parameters: BTreeMap<String, schema::ParameterValue>,
282}
283
284pub struct RuleContext<'a> {
286 pub project: &'a Project,
288 pub services: &'a ServiceRegistry,
290}
291
292#[derive(Clone, Debug, Default, PartialEq)]
294pub struct CapabilityEvaluation {
295 findings: Vec<Finding>,
296 not_evaluated: Vec<CapabilityNotEvaluated>,
297 tables: Vec<ReportTable>,
298 graded: Vec<(usize, Deviation)>,
300}
301#[derive(Clone, Debug, PartialEq)]
303pub struct CapabilityNotEvaluated {
304 scope: Scope,
305 reason: NotEvaluatedReason,
306 message: String,
307 location: Option<Location>,
308}
309impl CapabilityNotEvaluated {
310 #[must_use]
312 pub fn object_id(&self) -> Option<&ObjectId> {
313 self.scope.object()
314 }
315 #[must_use]
317 pub fn scope(&self) -> &Scope {
318 &self.scope
319 }
320 #[must_use]
321 pub fn reason(&self) -> &NotEvaluatedReason {
322 &self.reason
323 }
324 #[must_use]
325 pub fn message(&self) -> &str {
326 &self.message
327 }
328 #[must_use]
331 pub fn location(&self) -> Option<&Location> {
332 self.location.as_ref()
333 }
334 pub fn set_location(&mut self, location: Location) {
336 self.location = Some(location);
337 }
338}
339
340impl CapabilityEvaluation {
341 #[must_use]
343 pub fn findings(&self) -> &[Finding] {
344 &self.findings
345 }
346 #[must_use]
348 pub fn not_evaluated_outcomes(&self) -> &[CapabilityNotEvaluated] {
349 &self.not_evaluated
350 }
351 #[must_use]
353 pub fn tables(&self) -> &[ReportTable] {
354 &self.tables
355 }
356 #[must_use]
358 pub fn evaluated(findings: Vec<Finding>) -> Self {
359 Self {
360 findings,
361 ..Self::default()
362 }
363 }
364 pub fn push_table(&mut self, table: ReportTable) {
370 if !table.is_empty() {
371 self.tables.push(table);
372 }
373 }
374 #[must_use]
376 pub fn not_evaluated(reason: NotEvaluatedReason, message: impl Into<String>) -> Self {
377 let mut outcome = Self::default();
378 outcome.push_not_evaluated(reason, message);
379 outcome
380 }
381 pub fn push_finding(&mut self, finding: Finding) {
384 self.findings.push(finding);
385 }
386 pub fn push_graded_finding(&mut self, finding: Finding, deviation: Deviation) {
390 self.graded.push((self.findings.len(), deviation));
391 self.findings.push(finding);
392 }
393 pub fn push_finding_deviating(&mut self, finding: Finding, deviation: Option<Deviation>) {
395 match deviation {
396 Some(deviation) => self.push_graded_finding(finding, deviation),
397 None => self.push_finding(finding),
398 }
399 }
400 #[must_use]
403 pub fn deviation(&self, finding: usize) -> Option<Deviation> {
404 self.graded
405 .iter()
406 .find(|(index, _)| *index == finding)
407 .map(|(_, deviation)| *deviation)
408 }
409 fn grade(&mut self, bands: &[schema::SeverityBand]) {
412 for (index, deviation) in std::mem::take(&mut self.graded) {
413 let finding = &mut self.findings[index];
414 let (severity, mixed) = refinement::grade(bands, &finding.severity, deviation);
415 if mixed {
416 use std::fmt::Write as _;
417 let _ = write!(
418 finding.message,
419 "; deviation between {} and {}, graded {} by its most severe band",
420 percent(deviation.lower()),
421 percent(deviation.upper()),
422 refinement::label(&severity)
423 );
424 }
425 finding.severity = severity;
426 }
427 }
428 pub fn findings_mut(&mut self) -> &mut [Finding] {
430 &mut self.findings
431 }
432 pub fn not_evaluated_outcomes_mut(&mut self) -> &mut [CapabilityNotEvaluated] {
434 &mut self.not_evaluated
435 }
436 pub fn take_findings(&mut self) -> Vec<Finding> {
439 self.graded.clear();
440 std::mem::take(&mut self.findings)
441 }
442 pub fn push_not_evaluated_about(
445 &mut self,
446 scope: Scope,
447 reason: NotEvaluatedReason,
448 message: impl Into<String>,
449 ) {
450 self.push_unavailable(scope, reason, message);
451 }
452 pub fn push_not_evaluated(&mut self, reason: NotEvaluatedReason, message: impl Into<String>) {
454 self.push_unavailable(Scope::Project, reason, message);
455 }
456 pub fn push_source_not_evaluated(
459 &mut self,
460 source: SourceId,
461 reason: NotEvaluatedReason,
462 message: impl Into<String>,
463 ) {
464 self.push_unavailable(Scope::Source(source), reason, message);
465 }
466 pub fn push_object_not_evaluated(
468 &mut self,
469 object_id: ObjectId,
470 reason: NotEvaluatedReason,
471 message: impl Into<String>,
472 ) {
473 self.push_unavailable(Scope::Object(object_id), reason, message);
474 }
475 fn push_unavailable(
476 &mut self,
477 scope: Scope,
478 reason: NotEvaluatedReason,
479 message: impl Into<String>,
480 ) {
481 self.not_evaluated.push(CapabilityNotEvaluated {
482 scope,
483 reason,
484 message: message.into(),
485 location: None,
486 });
487 }
488}
489
490fn percent(value: f64) -> String {
492 if value.is_finite() {
493 format!("{} %", (value * 1e4).round() / 1e2)
494 } else {
495 "unbounded".to_owned()
496 }
497}
498
499pub trait RuleCapability: Send + Sync {
501 fn id(&self) -> &'static str;
503 fn parameters(&self) -> Vec<ParameterDescriptor>;
505 fn grades_deviation(&self) -> bool {
510 false
511 }
512 fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation;
514}
515
516#[derive(Clone, Default)]
518pub struct CapabilityRegistry {
519 capabilities: BTreeMap<String, Arc<dyn RuleCapability>>,
520 refiner: Option<Arc<dyn OutcomeRefiner>>,
521}
522impl CapabilityRegistry {
523 pub fn new() -> Self {
525 Self::default()
526 }
527 pub fn register<C: RuleCapability + 'static>(
529 mut self,
530 capability: C,
531 ) -> Result<Self, EngineError> {
532 let id = capability.id().to_owned();
533 if self
534 .capabilities
535 .insert(id.clone(), Arc::new(capability))
536 .is_some()
537 {
538 return Err(EngineError::DuplicateCapability(id));
539 }
540 Ok(self)
541 }
542 pub fn get(&self, id: &str) -> Option<&Arc<dyn RuleCapability>> {
544 self.capabilities.get(id)
545 }
546 #[must_use]
549 pub fn with_refiner<R: OutcomeRefiner + 'static>(mut self, refiner: R) -> Self {
550 self.refiner = Some(Arc::new(refiner));
551 self
552 }
553 pub fn refiner(&self) -> Option<&Arc<dyn OutcomeRefiner>> {
555 self.refiner.as_ref()
556 }
557}
558
559#[derive(Clone, Debug, PartialEq, Eq)]
564pub struct DeferredRule {
565 pub id: RuleId,
567 pub capability: String,
569 pub reason: String,
571}
572
573#[derive(Clone, Debug)]
575pub struct ExecutionPlan {
576 rules: Vec<CompiledRule>,
577 deferred: Vec<DeferredRule>,
578 concepts: Arc<ConceptCatalog>,
579 refinements: BTreeMap<RuleId, RuleRefinement>,
580 gates: BTreeMap<RuleId, Vec<(RuleId, schema::GateCondition)>>,
582 recorded: BTreeSet<RuleId>,
584 auxiliary: BTreeSet<RuleId>,
586 classifications: Vec<schema::ClassificationDefinition>,
589}
590impl ExecutionPlan {
591 pub fn rules(&self) -> &[CompiledRule] {
594 &self.rules
595 }
596 pub fn deferred(&self) -> &[DeferredRule] {
598 &self.deferred
599 }
600 pub fn concepts(&self) -> &ConceptCatalog {
602 &self.concepts
603 }
604 pub fn refinement(&self, rule: &RuleId) -> Option<&RuleRefinement> {
606 self.refinements.get(rule)
607 }
608 pub fn classifications(&self) -> &[schema::ClassificationDefinition] {
610 &self.classifications
611 }
612 pub fn gates(&self, rule: &RuleId) -> &[(RuleId, schema::GateCondition)] {
615 self.gates.get(rule).map_or(&[], Vec::as_slice)
616 }
617 pub fn is_auxiliary(&self, rule: &RuleId) -> bool {
620 self.auxiliary.contains(rule)
621 }
622}
623
624mod boundary_coverage;
625mod circulation;
626mod classifications;
627mod compiler;
628mod concepts;
629mod contact;
630mod coordinate_system;
631mod corridor_end;
632mod coverage;
633mod derived;
634mod derived_relationships;
635mod discipline_map;
636mod door_leaves;
637mod envelope_membership;
638mod facade_area;
639mod federation;
640mod free_space;
641mod guard;
642mod integrity;
643mod linear_quantity;
644mod measured;
645mod metric_routing;
646mod object_frame;
647mod pairwise;
648mod path;
649mod plan_area;
650mod plan_region;
651mod plan_span;
652mod properties;
653mod proximity;
654mod refinement;
655mod relationships;
656mod resources;
657mod rule_outcomes;
658mod services;
659mod side_distance;
660mod sight;
661mod source_metadata;
662mod space;
663mod topology;
664mod triangle_count;
665mod vertical_extent;
666mod walkability;
667mod walking_surface;
668pub use boundary_coverage::{
669 BoundaryCoverage, BoundaryCoverageError, BoundaryCoverageRequest, BoundaryCoverageService,
670 BoundaryCoverageServiceHandle, BoundaryOverlap, BoundaryPlacement, CoverageAreas,
671 MeasuredBoundary, ShareInterval, SurfaceAreaInterval,
672};
673pub use circulation::{
674 CirculationContact, CirculationMap, CirculationNode, CirculationNodeKind, CirculationRequest,
675};
676pub use classifications::{
677 ClassificationAssignment, ClassificationError, ClassificationService,
678 ClassificationServiceHandle,
679};
680pub use compiler::{QUALIFIED_RULE_SEPARATOR, SUPPORTED_SCHEMA_VERSION, compile, compile_rulesets};
681pub use concepts::{
682 BindingError, ConceptBindings, ConceptCatalog, ConceptKind, TypeHierarchyError,
683 TypeHierarchyService, TypeHierarchyServiceHandle,
684};
685pub use contact::{
686 ContactError, ContactEvidence, ContactRequest, ContactService, ContactServiceHandle,
687 ContactSide, ContactTolerance,
688};
689pub use coordinate_system::{
690 CoordinateFrame, CoordinateSystemError, CoordinateSystemService, CoordinateSystemServiceHandle,
691 MapConversion, SourceCoordinateSystem,
692};
693pub use corridor_end::{CorridorEnd, CorridorEndRequest, CorridorEnds, EndWall, WallContact};
694pub use coverage::{CoverageEvidence, CoverageRequest, EffectMeets, EffectReach, Participant};
695pub use derived::{ClassOutcome, Classifications};
696pub use derived_relationships::{
697 AdjacentSide, DERIVED_RELATIONSHIP_PREFIX, Derivation, DerivedRelationshipService,
698 DerivedRelationshipServiceHandle, LevelFacts, LevelMatch, adjacent_side,
699};
700pub use discipline_map::{
701 DisciplineMap, DisciplineMapError, DisciplineOrigin, DisciplineRule, UnmappedReason,
702 wildcard_regex,
703};
704pub use door_leaves::{
705 DoorLeaf, DoorLeaves, DoorLeavesError, HingeSide, LeafMotion, LeafPosition, PlanRing,
706 SWEPT_FLOOR_REACH_METRES, SweptDoor, SwingSector,
707};
708pub use envelope_membership::{
709 EnvelopeDerivation, EnvelopeMembershipError, EnvelopeMembershipEvidence,
710 EnvelopeMembershipRequest, EnvelopeMembershipService, EnvelopeMembershipServiceHandle,
711};
712pub use facade_area::{FacadeArea, FacadeAreaError, FacadeAreaService, FacadeAreaServiceHandle};
713pub use free_space::{
714 AreaInterval, BoxClearance, ClearanceOutcome, ClearancePlacementEvidence, ClearanceRequest,
715 ClearanceShape, CompleteClearanceEvidence, CompletePlacementEvidence, CompleteSupportEvidence,
716 ContainmentEvidence, ContainmentOutcome, ContainmentRequest, CylinderClearance, ElevationBand,
717 EntranceReach, FrameOffsetPlacement, FreeAreaEvidence, FreeAreaRequest, FreeSpaceError,
718 FreeSpaceService, FreeSpaceServiceHandle, MetricDirection, MetricFrame, ObstructionEvidence,
719 PlacementDomain, PlacementOrientation, PlacementOutcome, PlacementRequest, PlacementShape,
720 SignedDistanceInterval, SupportCoverageEvidence, SupportCoverageOutcome,
721 SupportCoverageRequest, SupportedPlacement,
722};
723pub use guard::{
724 ClimbableCandidate, GuardCandidate, GuardEdge, GuardError, GuardEvidence, GuardSearch,
725 GuardService, GuardServiceHandle,
726};
727pub use integrity::{
728 IntegrityError, IntegrityIssue, IntegritySeverity, SourceIntegrityService,
729 SourceIntegrityServiceHandle,
730};
731pub use linear_quantity::{
732 LinearInterval, LinearQuantityError, LinearQuantityEvidence, LinearQuantityKind,
733 LinearQuantityRequest, LinearQuantityService, LinearQuantityServiceHandle, ShelfGeometry,
734};
735pub use metric_routing::{
736 BlockedMetricRouteEvidence, ClimbLength, CompleteMetricEvidence, ConnectorRouting,
737 FarthestPointEvidence, FarthestPointOutcome, FarthestPointRequest, ForcedWalkEvidence,
738 ForcedWalkOutcome, ForcedWalkRequest, LengthInterval, MetricPoint, MetricRouteEvidence,
739 MetricRouteOutcome, MetricRouteRequest, MetricRoutingError, MetricRoutingService,
740 MetricRoutingServiceHandle, MobilityProfile, NearestTargetEvidence, NearestTargetOutcome,
741 NearestTargetRequest, NeverEnteredEvidence, PathTrace, PathTraceRequest, StairLength,
742 ThresholdVerdict, TravelCost, UnreachableRegionEvidence, UnreachableTargetsEvidence,
743};
744pub use object_frame::{
745 ObjectFrame, ObjectFrameError, ObjectFrameService, ObjectFrameServiceHandle, ObjectFront,
746};
747pub use pairwise::{
748 CandidatePair, CandidateSearchError, candidate_pairs, projected_candidate_pairs,
749};
750pub use path::PathSegment;
751pub use plan_area::{
752 ElevationCover, ElevationRequest, PlanArea, PlanAreaError, PlanAreaService,
753 PlanAreaServiceHandle, PlanBand,
754};
755pub use plan_region::ConvexPlanRegion;
756pub use plan_span::{
757 CentrePlacement, PlanCentre, PlanLength, PlanRecess, PlanRecesses, PlanRectangle, PlanSection,
758 PlanSpan, PlanSpanError, PlanSpanService, PlanSpanServiceHandle, RectangleOrientation,
759};
760pub use properties::{
761 CompletePropertyAbsenceEvidence, NameMatch, NamePattern, PropertyEnumeration,
762 PropertyEnumerationRequest, PropertyRequest, PropertyResolution, PropertyResolutionError,
763 PropertyResolutionService, PropertyResolutionServiceHandle, ResolvedProperty, UnreadableValue,
764};
765pub use proximity::{
766 BodyContainment, BodyVolume, Bounds3, CounterpartSurface, FaceClass, FaceDistanceError,
767 FaceDistanceEvidence, FaceDistanceRequest, GeometryFidelity, IntersectionVolume, ObjectBounds,
768 OverlapAlongEvidence, OverlapAlongRequest, OverlapExtents, ProjectedDistanceEvidence,
769 ProximityError, ProximityEvidence, ProximityProjection, ProximityRequest, ProximityService,
770 ProximityServiceHandle, RegionDistanceEvidence, RegionDistanceRequest, SubjectSurface,
771 VerticalDirection, VerticalSurfaces, VolumeInterval,
772};
773pub use refinement::{
774 Deviation, LocationMethod, LocationPolicy, OutcomeRefiner, Refining, RuleRefinement,
775 report_severity,
776};
777pub use relationships::{
778 AbsentEndPolicy, CompleteRelationshipSelection, RelationshipQuery, RelationshipSelectionError,
779 RelationshipSelectionRequest, RelationshipSelectionService, RelationshipSelectionServiceHandle,
780 SemanticRelationship, TraversalDirection,
781};
782pub use resources::{
783 Reached, ResourceError, ResourceObjects, ResourceRequest, ResourceService,
784 ResourceServiceHandle,
785};
786pub use rule_outcomes::{ObjectVerdict, RuleOutcomes, RuleRecord, RuleVerdict, SelectorVerdict};
787pub use services::{ServiceRegistry, ServiceRegistryError};
788pub use session::{
789 EvidenceSession, EvidenceSessionError, SessionSources, SnapshotBoundService, SourceDisciplines,
790 SourceSnapshot,
791};
792pub use side_distance::{
793 RectangleSide, SideDistance, SideDistanceRequest, SideDistances, SidePresence,
794};
795pub use sight::{
796 SightError, SightEvidence, SightOutcome, SightRequest, SightService, SightServiceHandle,
797};
798pub use source_metadata::{SourceMetadata, SourceMetadataIndex};
799pub use space::{
800 BoundaryGap, BoundaryRequest, Cap, CapCoverage, CapRequest, ClearHeightEvidence, Containment,
801 OverlapRequest, SpaceError, SpaceOverlap, SpaceService, SpaceServiceHandle, SupportCounts,
802 UnallocatedRegion,
803};
804pub use topology::{
805 CompleteTopologyEvidence, ConnectivityGraph, RouteOutcome, TopologyError, VerifiedConnection,
806};
807pub use triangle_count::{
808 TriangleCount, TriangleCountError, TriangleCountService, TriangleCountServiceHandle,
809};
810pub use vertical_extent::{
811 DirectionalExtent, ElevationInterval, VerticalExtent, VerticalExtentError,
812 VerticalExtentService, VerticalExtentServiceHandle,
813};
814pub use walkability::{
815 PassageAdmission, StretchLimit, VerifiedWalkablePassage, VerticalConnector,
816 VerticalConnectorKind, WalkabilityError, WalkabilityRegion, WalkabilityRegionId,
817 WalkabilityRequest, WalkabilityRouteOutcome, WalkabilityService, WalkabilityServiceHandle,
818 WalkabilitySnapshot, WalkableStretch,
819};
820pub use walking_surface::{
821 ClearWidthEvidence, ClearWidthRequest, ClearanceBelow, ClearanceBelowRequest, HandrailEvidence,
822 HandrailRequest, Headroom, HeadroomRequest, Landing, LandingEvidence, LandingExtent,
823 LandingRequest, MeasuredInterval, PlanSegment, RailMeasurement, RailSide, RiserClosure,
824 SlopedRun, SlopedSurface, StretchPart, Tread, TreadFlight, TreadFlightRequest, WalkingEnd,
825 WalkingLine, WalkingLinePlacement, WalkingStretch, WalkingSurfaceError, WalkingSurfaceService,
826 WalkingSurfaceServiceHandle, across,
827};
828
829fn collapse_source_wide(
840 rule_id: &RuleId,
841 outcomes: Vec<CapabilityNotEvaluated>,
842) -> Vec<NotEvaluated> {
843 const EXAMPLES: usize = 3;
844 let mut merged: BTreeMap<(axioval_ir::SourceId, NotEvaluatedReason, String), Vec<ObjectId>> =
845 BTreeMap::new();
846 let mut kept = Vec::new();
847 for outcome in outcomes {
848 match (outcome.reason, outcome.scope) {
849 (
850 reason @ (NotEvaluatedReason::UnboundConcept
851 | NotEvaluatedReason::NotRecorded
852 | NotEvaluatedReason::MissingService),
853 Scope::Object(object),
854 ) => merged
855 .entry((object.source.clone(), reason, outcome.message))
856 .or_default()
857 .push(object),
858 (reason, scope) => kept.push(NotEvaluated {
859 rule_id: rule_id.clone(),
860 scope,
861 reason,
862 message: outcome.message,
863 location: outcome.location,
864 }),
865 }
866 }
867 kept.extend(merged.into_iter().map(|((source, reason, message), mut objects)| {
868 objects.sort();
869 let examples: Vec<&str> = objects
870 .iter()
871 .take(EXAMPLES)
872 .map(|object| object.local_id.as_str())
873 .collect();
874 let more = objects.len().saturating_sub(EXAMPLES);
875 let tail = if more > 0 {
876 format!(", +{more} more")
877 } else {
878 String::new()
879 };
880 NotEvaluated {
881 rule_id: rule_id.clone(),
882 scope: Scope::Source(source.clone()),
883 reason,
884 message: format!(
885 "{message}; {} object(s) of source `{source}` not evaluated (e.g. {}{tail})",
886 objects.len(),
887 examples.join(", ")
888 ),
889 location: None,
890 }
891 }));
892 kept
893}
894
895fn summarize(rule: &RuleId, checked: usize, evaluation: &CapabilityEvaluation) -> RuleSummary {
897 let objects = |scopes: &mut dyn Iterator<Item = &Scope>| {
898 scopes
899 .filter_map(Scope::object)
900 .collect::<std::collections::BTreeSet<_>>()
901 .len()
902 };
903 let failed = objects(&mut evaluation.findings.iter().map(|finding| &finding.scope));
904 let open = objects(
905 &mut evaluation
906 .not_evaluated
907 .iter()
908 .map(|outcome| &outcome.scope),
909 );
910 RuleSummary::new(
911 rule.clone(),
912 checked,
913 (failed, !evaluation.findings.is_empty()),
914 (open, !evaluation.not_evaluated.is_empty()),
915 )
916}
917
918pub struct Runtime {
920 registry: CapabilityRegistry,
921 services: ServiceRegistry,
922 locations: Option<LocationPolicy>,
923 summaries: bool,
924}
925impl Runtime {
926 pub fn new(registry: CapabilityRegistry) -> Self {
928 Self {
929 registry,
930 services: ServiceRegistry::new(),
931 locations: None,
932 summaries: false,
933 }
934 }
935 #[must_use]
941 pub fn with_rule_summaries(mut self) -> Self {
942 self.summaries = true;
943 self
944 }
945 #[must_use]
949 pub fn with_locations(mut self, policy: LocationPolicy) -> Self {
950 self.locations = Some(policy);
951 self
952 }
953 #[must_use]
955 pub fn with_services(mut self, services: ServiceRegistry) -> Self {
956 self.services = services;
957 self
958 }
959 pub fn run(&self, project: &Project, plan: ExecutionPlan) -> Result<Report, EngineError> {
968 self.run_with_services(
969 project,
970 &self.services,
971 BTreeMap::new(),
972 (
973 SessionSources::new(project.objects().map(|object| object.id.source.clone())),
974 SourceDisciplines::default(),
975 SourceMetadataIndex::default(),
976 ),
977 plan,
978 )
979 }
980
981 pub fn run_session(
983 &self,
984 session: &EvidenceSession,
985 plan: ExecutionPlan,
986 ) -> Result<Report, EngineError> {
987 let type_systems = session
988 .snapshots()
989 .map(|snapshot| (snapshot.source().clone(), snapshot.type_systems().to_vec()))
990 .collect();
991 self.run_with_services(
992 session.project(),
993 session.services(),
994 type_systems,
995 (
996 SessionSources::new(
997 session
998 .snapshots()
999 .map(|snapshot| snapshot.source().clone()),
1000 ),
1001 session.source_disciplines(),
1002 session.metadata_index(),
1003 ),
1004 plan,
1005 )
1006 }
1007
1008 fn refiner_for(
1011 &self,
1012 plan: &ExecutionPlan,
1013 ) -> Result<Option<&Arc<dyn OutcomeRefiner>>, EngineError> {
1014 let refiner = self.registry.refiner();
1015 if refiner.is_some() {
1016 return Ok(refiner);
1017 }
1018 if !plan.recorded.is_empty() {
1019 return Err(EngineError::MissingRefiner(
1020 "reading another rule's outcomes per object",
1021 ));
1022 }
1023 if !plan.classifications.is_empty() {
1024 return Err(EngineError::MissingRefiner("deriving classifications"));
1025 }
1026 if self.locations.is_some() {
1027 return Err(EngineError::MissingRefiner("locating outcomes"));
1028 }
1029 if self.summaries {
1030 return Err(EngineError::MissingRefiner(
1031 "counting each rule's selection",
1032 ));
1033 }
1034 Ok(None)
1035 }
1036
1037 fn evaluate(
1039 &self,
1040 context: &RuleContext<'_>,
1041 capability: &dyn RuleCapability,
1042 rule: &CompiledRule,
1043 refinement: Option<&RuleRefinement>,
1044 ) -> CapabilityEvaluation {
1045 let mut evaluation = capability.evaluate(context, rule);
1046 if let Some(refinement) = refinement {
1047 evaluation.grade(&refinement.severity_bands);
1048 }
1049 if let Some(refiner) = self.registry.refiner()
1050 && (refinement.is_some() || self.locations.is_some())
1051 {
1052 let unrefined = RuleRefinement::default();
1053 let refining = Refining {
1054 refinement: refinement.unwrap_or(&unrefined),
1055 locations: self.locations.as_ref(),
1056 };
1057 refiner.refine(context, rule, &refining, &mut evaluation);
1058 }
1059 evaluation
1060 }
1061
1062 fn run_with_services(
1063 &self,
1064 project: &Project,
1065 services: &ServiceRegistry,
1066 type_systems: BTreeMap<axioval_ir::SourceId, Vec<Arc<str>>>,
1067 (sources, disciplines, metadata): (SessionSources, SourceDisciplines, SourceMetadataIndex),
1068 plan: ExecutionPlan,
1069 ) -> Result<Report, EngineError> {
1070 let mut services = services.clone();
1075 services.replace(ConceptBindings::new(plan.concepts.clone(), type_systems));
1076 services.replace(disciplines);
1079 services.replace(metadata);
1081 services.replace(sources);
1083 resources::install(&mut services, project, &plan.rules);
1086 let refiner = self.refiner_for(&plan)?;
1087 let mut summaries: Vec<RuleSummary> = Vec::new();
1088 let mut findings = Vec::new();
1089 let mut tables: Vec<ReportTable> = Vec::new();
1090 let mut not_evaluated: Vec<NotEvaluated> = plan
1091 .deferred
1092 .into_iter()
1093 .inspect(|rule| {
1094 if self.summaries {
1095 summaries.push(RuleSummary::new(rule.id.clone(), 0, (0, false), (0, true)));
1096 }
1097 })
1098 .map(|rule| NotEvaluated {
1099 rule_id: rule.id,
1100 scope: Scope::Project,
1101 reason: NotEvaluatedReason::InvalidDeclaration,
1102 message: rule.reason,
1103 location: None,
1104 })
1105 .collect();
1106 if plan.classifications.is_empty()
1110 && let Some(host) = services.get::<PropertyResolutionServiceHandle>().cloned()
1111 {
1112 derived::install(&mut services, Some(&host), Arc::default(), project);
1113 }
1114 if let Some(refiner) = refiner.filter(|_| !plan.classifications.is_empty()) {
1115 derive_classifications(
1116 refiner.as_ref(),
1117 project,
1118 &mut services,
1119 &plan.classifications,
1120 );
1121 }
1122 let mut outcomes = RuleOutcomes::default();
1124 for rule in plan.rules {
1125 let capability = self
1126 .registry
1127 .get(&rule.capability)
1128 .ok_or_else(|| EngineError::UnknownCapability(rule.capability.clone()))?;
1129 let rule_id = rule.id.clone();
1130 let reported = !plan.auxiliary.contains(&rule_id);
1133 services.replace(outcomes.clone());
1135 let context = RuleContext {
1136 project,
1137 services: &services,
1138 };
1139 let gates = plan.gates.get(&rule_id).map_or(&[][..], Vec::as_slice);
1140 let mut evaluation = match outcomes.gate(gates) {
1141 rule_outcomes::Gate::Closed => {
1142 outcomes.insert(rule_id.clone(), RuleRecord::skipped());
1143 if self.summaries && reported {
1144 summaries.push(RuleSummary::skipped(rule_id));
1145 }
1146 continue;
1147 }
1148 rule_outcomes::Gate::Undecided(why) => {
1149 outcomes.insert(rule_id.clone(), RuleRecord::undecided(&why));
1150 CapabilityEvaluation::not_evaluated(NotEvaluatedReason::IncompleteEvidence, why)
1151 }
1152 rule_outcomes::Gate::Open => {
1153 let evaluation = self.evaluate(
1154 &context,
1155 capability.as_ref(),
1156 &rule,
1157 plan.refinements.get(&rule_id),
1158 );
1159 let selection = refiner
1160 .filter(|_| plan.recorded.contains(&rule_id))
1161 .map(|refiner| rule_outcomes::selection(refiner.as_ref(), &context, &rule));
1162 outcomes.insert(rule_id.clone(), RuleRecord::of(&evaluation, selection));
1163 evaluation
1164 }
1165 };
1166 if !reported {
1167 continue;
1168 }
1169 if let Some(refiner) = refiner
1170 && self.summaries
1171 {
1172 summaries.push(summarize(
1173 &rule_id,
1174 refiner.selected(&context, &rule),
1175 &evaluation,
1176 ));
1177 }
1178 findings.extend(std::mem::take(&mut evaluation.findings));
1179 tables.extend(
1181 std::mem::take(&mut evaluation.tables)
1182 .into_iter()
1183 .map(|table| table.with_rule_id(rule_id.clone())),
1184 );
1185 not_evaluated.extend(collapse_source_wide(&rule_id, evaluation.not_evaluated));
1186 }
1187 assemble(findings, not_evaluated, tables, summaries, &services)
1188 }
1189}
1190
1191fn named_resources(
1193 findings: &[Finding],
1194 not_evaluated: &[NotEvaluated],
1195 tables: &[ReportTable],
1196 services: &ServiceRegistry,
1197) -> Vec<axioval_ir::Object> {
1198 let Some(resources) = services
1199 .get::<ResourceObjects>()
1200 .filter(|resources| !resources.is_empty())
1201 else {
1202 return Vec::new();
1203 };
1204 let mut named: BTreeSet<&ObjectId> = BTreeSet::new();
1205 for finding in findings {
1206 named.extend(finding.object_id());
1207 named.extend(&finding.related);
1208 }
1209 named.extend(not_evaluated.iter().filter_map(NotEvaluated::object_id));
1210 named.extend(
1211 tables
1212 .iter()
1213 .flat_map(|table| table.rows().iter().filter_map(|row| row.scope().object())),
1214 );
1215 named
1216 .into_iter()
1217 .filter_map(|id| resources.object(id).cloned())
1218 .collect()
1219}
1220
1221fn derive_classifications(
1225 refiner: &dyn OutcomeRefiner,
1226 project: &Project,
1227 services: &mut ServiceRegistry,
1228 definitions: &[schema::ClassificationDefinition],
1229) {
1230 let host = services.get::<PropertyResolutionServiceHandle>().cloned();
1231 let mut derived = Classifications::default();
1232 for definition in definitions {
1233 derived::install(services, host.as_ref(), Arc::new(derived.clone()), project);
1234 let context = RuleContext { project, services };
1235 derived.classify(refiner, &context, definition);
1236 }
1237 derived::install(services, host.as_ref(), Arc::new(derived), project);
1238}
1239
1240fn assemble(
1244 mut findings: Vec<Finding>,
1245 mut not_evaluated: Vec<NotEvaluated>,
1246 mut tables: Vec<ReportTable>,
1247 mut summaries: Vec<RuleSummary>,
1248 services: &ServiceRegistry,
1249) -> Result<Report, EngineError> {
1250 findings.sort_by(|a, b| {
1251 a.rule_id
1252 .cmp(&b.rule_id)
1253 .then_with(|| a.scope.cmp(&b.scope))
1255 .then_with(|| a.message.cmp(&b.message))
1256 });
1257 not_evaluated.sort();
1258 tables.sort_by(|a, b| (a.rule_id(), a.name()).cmp(&(b.rule_id(), b.name())));
1259 if let Some(pair) = tables
1260 .windows(2)
1261 .find(|pair| (pair[0].rule_id(), pair[0].name()) == (pair[1].rule_id(), pair[1].name()))
1262 {
1263 return Err(EngineError::DuplicateReportTable {
1264 rule: pair[0].rule_id().to_string(),
1265 table: pair[0].name().to_owned(),
1266 });
1267 }
1268 summaries.sort();
1269 Ok(Report {
1270 resources: named_resources(&findings, ¬_evaluated, &tables, services),
1271 stale_decisions: Vec::new(),
1272 findings,
1273 not_evaluated,
1274 tables,
1275 rules: summaries,
1276 })
1277}