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::{
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/// Errors while compiling untrusted declarations into a trusted execution plan.
21#[derive(Debug, Error, PartialEq, Eq)]
22pub enum EngineError {
23    /// A package declares a schema version this compiler does not implement.
24    #[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    /// Multiple supplied definition packages declared the same package identity.
34    #[error("duplicate definition package `{0}`")]
35    DuplicateDefinitionPackage(String),
36    /// A capability was not registered by the host.
37    #[error("unknown capability `{0}`")]
38    UnknownCapability(String),
39    /// Two trusted implementations claimed an ID.
40    #[error("duplicate capability `{0}`")]
41    DuplicateCapability(String),
42    /// A package supplied a non-declared parameter.
43    #[error("capability `{capability}` does not declare parameter `{parameter}`")]
44    UnknownParameter {
45        capability: String,
46        parameter: String,
47    },
48    /// A required parameter was absent.
49    #[error("capability `{capability}` requires parameter `{parameter}`")]
50    MissingParameter {
51        capability: String,
52        parameter: String,
53    },
54    /// A binding type did not conform to its descriptor.
55    #[error("capability `{capability}` parameter `{parameter}` has invalid type")]
56    InvalidParameterType {
57        capability: String,
58        parameter: String,
59    },
60    /// A row of a table-valued binding does not fit the declared columns.
61    #[error("capability `{capability}` parameter `{parameter}` row {row}: {detail}")]
62    InvalidTableRow {
63        capability: String,
64        parameter: String,
65        /// Zero-based row index.
66        row: usize,
67        detail: String,
68    },
69    /// A rule binds a parameter more than once.
70    #[error("rule has duplicate parameter binding `{0}`")]
71    DuplicateBinding(String),
72    /// A rule ID violates the engine identity contract.
73    #[error("invalid rule id `{0}`")]
74    InvalidRuleId(String),
75    /// A rule references no loaded definition.
76    #[error("unknown rule definition `{0}`")]
77    UnknownDefinition(String),
78    /// A ruleset references a definition package that was not supplied.
79    #[error("missing definition package `{0}`")]
80    MissingDefinitionPackage(String),
81    /// A trusted capability descriptor conflicts with its portable definition.
82    #[error("definition `{definition}` conflicts with capability `{capability}`: {detail}")]
83    CapabilityContract {
84        definition: String,
85        capability: String,
86        detail: String,
87    },
88    /// Rule IDs must be unique throughout the recursive folder tree.
89    #[error("duplicate rule id `{0}`")]
90    DuplicateRule(String),
91    /// Two rulesets compiled together share a package ID.
92    #[error("duplicate ruleset package `{0}`")]
93    DuplicateRuleSet(String),
94    /// No ruleset was given to compile.
95    #[error("no ruleset to compile")]
96    NoRuleSet,
97    /// Two loaded definition packages declare the same concept identity.
98    #[error("duplicate concept `{0}` across definition packages")]
99    DuplicateConcept(String),
100    /// A rule names a concept no loaded definition package declares.
101    #[error("rule `{rule}` references unknown {kind} concept `{concept}`")]
102    UnknownConcept {
103        rule: String,
104        kind: String,
105        concept: String,
106    },
107    /// One rule reported two tables of one name.
108    #[error("rule `{rule}` reported table `{table}` twice")]
109    DuplicateReportTable { rule: String, table: String },
110    /// A rule refines its outcomes in a way the package or the capability
111    /// cannot support, such as severity bands on a capability that reports
112    /// no deviation.
113    #[error("rule `{rule}`: {detail}")]
114    InvalidRefinement { rule: String, detail: String },
115    /// The host asked the runtime to refine every outcome (locate it) and
116    /// registered no outcome refiner to do so.
117    #[error("{0} needs an outcome refiner, and the host registered none")]
118    MissingRefiner(&'static str),
119    /// A rule's gate or `ruleOutcome` selector names no rule of its
120    /// ruleset, a cycle of rules depends on itself, or the host registered
121    /// no outcome refiner to read another rule's selection.
122    #[error("rule `{rule}`: {detail}")]
123    InvalidDependency { rule: String, detail: String },
124    /// A classification the ruleset derives is malformed, reads a rule's
125    /// outcome, depends on itself through other classifications, is declared
126    /// twice with different rows, or the host registered no outcome refiner
127    /// to evaluate its rows.
128    #[error("classification `{classification}`: {detail}")]
129    InvalidClassification {
130        classification: String,
131        detail: String,
132    },
133}
134
135pub use schema::ColumnKind;
136
137/// One trusted column of a table parameter.
138///
139/// A definition's columns must match the descriptor's by ID, kind and
140/// requirement, in any order; their names and descriptions are presentation.
141#[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    /// A column every row must fill.
149    #[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    /// A column a row may leave empty.
158    #[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/// Supported declarative parameter types.
169#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170pub enum ParameterType {
171    Boolean,
172    Integer,
173    Number,
174    String,
175    Quantity,
176    Enum,
177    /// An ISO 8601 calendar date, validated when the package is read.
178    Date,
179    /// An ISO 8601 date-time with a UTC offset, validated when the package
180    /// is read.
181    DateTime,
182    Reference,
183    ObjectTypeReference,
184    PropertyReference,
185    Selector,
186    StringList,
187    ReferenceList,
188    /// Rows of typed cells in the given columns.
189    Table(&'static [TableColumn]),
190}
191impl ParameterType {
192    /// The kind's spelling in a definition package.
193    #[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/// Trusted capability parameter descriptor.
244#[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    /// Required parameter descriptor.
252    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    /// Optional parameter descriptor.
260    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/// One validated portable rule bound to trusted executable capability code.
270#[derive(Clone, Debug, PartialEq)]
271pub struct CompiledRule {
272    /// Package-local stable rule ID.
273    pub id: RuleId,
274    /// Registered capability ID.
275    pub capability: String,
276    /// Rule severity.
277    pub severity: schema::Severity,
278    /// Source-neutral applicability selector.
279    pub selector: schema::Selector,
280    /// Strictly validated parameter bindings.
281    pub parameters: BTreeMap<String, schema::ParameterValue>,
282}
283
284/// Source-neutral data and typed host services visible during one rule evaluation.
285pub struct RuleContext<'a> {
286    /// Immutable composed project view.
287    pub project: &'a Project,
288    /// Adapter-provided semantic and computational capabilities.
289    pub services: &'a ServiceRegistry,
290}
291
292/// Fail-closed output from one trusted capability evaluation.
293#[derive(Clone, Debug, Default, PartialEq)]
294pub struct CapabilityEvaluation {
295    findings: Vec<Finding>,
296    not_evaluated: Vec<CapabilityNotEvaluated>,
297    tables: Vec<ReportTable>,
298    /// The deviation of each graded finding, by its index in `findings`.
299    graded: Vec<(usize, Deviation)>,
300}
301/// A not-evaluated outcome before the runtime binds its compiled rule ID.
302#[derive(Clone, Debug, PartialEq)]
303pub struct CapabilityNotEvaluated {
304    scope: Scope,
305    reason: NotEvaluatedReason,
306    message: String,
307    location: Option<Location>,
308}
309impl CapabilityNotEvaluated {
310    /// The object that could not be evaluated, if the outcome is about one.
311    #[must_use]
312    pub fn object_id(&self) -> Option<&ObjectId> {
313        self.scope.object()
314    }
315    /// What could not be evaluated: an object, a source, or the whole rule.
316    #[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    /// Where the outcome's object lies, once an [`OutcomeRefiner`] located
329    /// it.
330    #[must_use]
331    pub fn location(&self) -> Option<&Location> {
332        self.location.as_ref()
333    }
334    /// Locates the outcome; for an [`OutcomeRefiner`].
335    pub fn set_location(&mut self, location: Location) {
336        self.location = Some(location);
337    }
338}
339
340impl CapabilityEvaluation {
341    /// Conclusive findings emitted by this capability.
342    #[must_use]
343    pub fn findings(&self) -> &[Finding] {
344        &self.findings
345    }
346    /// Explicit fail-closed outcomes emitted by this capability.
347    #[must_use]
348    pub fn not_evaluated_outcomes(&self) -> &[CapabilityNotEvaluated] {
349        &self.not_evaluated
350    }
351    /// Tables of measured values emitted by this capability.
352    #[must_use]
353    pub fn tables(&self) -> &[ReportTable] {
354        &self.tables
355    }
356    /// Creates a conclusive evaluation from zero or more findings.
357    #[must_use]
358    pub fn evaluated(findings: Vec<Finding>) -> Self {
359        Self {
360            findings,
361            ..Self::default()
362        }
363    }
364    /// Adds a table of measured values, reported beside the findings.
365    ///
366    /// A table without rows is dropped: it measured nothing, and the
367    /// not-evaluated outcomes already say why. Tables are informative only;
368    /// they never stand in for a finding or a not-evaluated outcome.
369    pub fn push_table(&mut self, table: ReportTable) {
370        if !table.is_empty() {
371            self.tables.push(table);
372        }
373    }
374    /// Creates a rule-level not-evaluated outcome.
375    #[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    /// Adds a conclusive finding, about an object, a source or the project
382    /// as its [`Scope`] says.
383    pub fn push_finding(&mut self, finding: Finding) {
384        self.findings.push(finding);
385    }
386    /// Adds a finding of a value missing its bound by `deviation`, so a rule
387    /// declaring severity bands grades it. The finding keeps its own
388    /// severity otherwise; the deviation is never reported.
389    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    /// Adds a finding, graded when `deviation` is known.
394    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    /// The deviation a graded finding was pushed with, by its index in
401    /// [`Self::findings`]; `None` for an ungraded one.
402    #[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    /// Grades every graded finding's severity by `bands`, the finding's own
410    /// severity standing beyond the last band.
411    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    /// Findings, for an [`OutcomeRefiner`] to refine in place.
429    pub fn findings_mut(&mut self) -> &mut [Finding] {
430        &mut self.findings
431    }
432    /// Not-evaluated outcomes, for an [`OutcomeRefiner`] to locate.
433    pub fn not_evaluated_outcomes_mut(&mut self) -> &mut [CapabilityNotEvaluated] {
434        &mut self.not_evaluated
435    }
436    /// Removes and returns every finding, for an [`OutcomeRefiner`] to put
437    /// back refined; deviations are dropped with them.
438    pub fn take_findings(&mut self) -> Vec<Finding> {
439        self.graded.clear();
440        std::mem::take(&mut self.findings)
441    }
442    /// Adds a not-evaluated outcome about `scope`: an object, a source, or
443    /// the rule as a whole.
444    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    /// Adds a rule-level not-evaluated outcome.
453    pub fn push_not_evaluated(&mut self, reason: NotEvaluatedReason, message: impl Into<String>) {
454        self.push_unavailable(Scope::Project, reason, message);
455    }
456    /// Adds a not-evaluated outcome about one source as a whole, such as a
457    /// count over that source that undecided objects could still change.
458    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    /// Adds an object-specific not-evaluated outcome.
467    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
490/// A relative deviation as a reviewer reads it, `12.5 %`.
491fn percent(value: f64) -> String {
492    if value.is_finite() {
493        format!("{} %", (value * 1e4).round() / 1e2)
494    } else {
495        "unbounded".to_owned()
496    }
497}
498
499/// Trusted code selected by a package capability ID; packages never supply executable code.
500pub trait RuleCapability: Send + Sync {
501    /// Stable trusted capability ID.
502    fn id(&self) -> &'static str;
503    /// Strict accepted parameters.
504    fn parameters(&self) -> Vec<ParameterDescriptor>;
505    /// Whether findings report how far a value misses its bound
506    /// ([`CapabilityEvaluation::push_graded_finding`]), so a rule may grade
507    /// them with severity bands. A rule declaring bands on a capability that
508    /// answers `false` fails compilation.
509    fn grades_deviation(&self) -> bool {
510        false
511    }
512    /// Evaluates an already-validated rule request.
513    fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation;
514}
515
516/// Host-controlled registry of trusted capabilities.
517#[derive(Clone, Default)]
518pub struct CapabilityRegistry {
519    capabilities: BTreeMap<String, Arc<dyn RuleCapability>>,
520    refiner: Option<Arc<dyn OutcomeRefiner>>,
521}
522impl CapabilityRegistry {
523    /// Creates an empty registry.
524    pub fn new() -> Self {
525        Self::default()
526    }
527    /// Registers a capability; duplicate IDs are rejected.
528    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    /// Gets trusted code by exact ID.
543    pub fn get(&self, id: &str) -> Option<&Arc<dyn RuleCapability>> {
544        self.capabilities.get(id)
545    }
546    /// Installs the trusted code that applies rule refinements reading the
547    /// model, replacing any installed before.
548    #[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    /// The installed outcome refiner, if any.
554    pub fn refiner(&self) -> Option<&Arc<dyn OutcomeRefiner>> {
555        self.refiner.as_ref()
556    }
557}
558
559/// A compiled rule the engine cannot execute as authored.
560///
561/// Carried in the plan so every run reports it as not evaluated; dropping it
562/// would make an unexecuted requirement indistinguishable from a satisfied one.
563#[derive(Clone, Debug, PartialEq, Eq)]
564pub struct DeferredRule {
565    /// Package-local stable rule ID.
566    pub id: RuleId,
567    /// Registered capability ID.
568    pub capability: String,
569    /// Why the rule cannot run as authored.
570    pub reason: String,
571}
572
573/// Validated, deterministic request plan.
574#[derive(Clone, Debug)]
575pub struct ExecutionPlan {
576    rules: Vec<CompiledRule>,
577    deferred: Vec<DeferredRule>,
578    concepts: Arc<ConceptCatalog>,
579    refinements: BTreeMap<RuleId, RuleRefinement>,
580    /// Whole-rule gates: each gated rule's parents and conditions.
581    gates: BTreeMap<RuleId, Vec<(RuleId, schema::GateCondition)>>,
582    /// Rules whose selection a dependent reads per object.
583    recorded: BTreeSet<RuleId>,
584    /// Rules reported only through the rules that read them.
585    auxiliary: BTreeSet<RuleId>,
586    /// Classifications to derive before any rule runs, each after those
587    /// its rows read.
588    classifications: Vec<schema::ClassificationDefinition>,
589}
590impl ExecutionPlan {
591    /// Rules in execution order: every rule after the rules its gates and
592    /// `ruleOutcome` selectors read, and otherwise by stable rule ID.
593    pub fn rules(&self) -> &[CompiledRule] {
594        &self.rules
595    }
596    /// Rules compiled but not executable as authored, ordered by rule ID.
597    pub fn deferred(&self) -> &[DeferredRule] {
598        &self.deferred
599    }
600    /// Canonical concepts declared by the ruleset's definition packages.
601    pub fn concepts(&self) -> &ConceptCatalog {
602        &self.concepts
603    }
604    /// How `rule` refines its outcomes; `None` when it declares nothing.
605    pub fn refinement(&self, rule: &RuleId) -> Option<&RuleRefinement> {
606        self.refinements.get(rule)
607    }
608    /// The classifications the plan derives, in the order they are derived.
609    pub fn classifications(&self) -> &[schema::ClassificationDefinition] {
610        &self.classifications
611    }
612    /// The whole-rule gates of `rule`: each parent rule and the condition
613    /// on its outcome. Empty for an ungated rule.
614    pub fn gates(&self, rule: &RuleId) -> &[(RuleId, schema::GateCondition)] {
615        self.gates.get(rule).map_or(&[], Vec::as_slice)
616    }
617    /// Whether `rule` is auxiliary: run for the rules that read its
618    /// outcome, and never reported itself.
619    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
829/// Binds a rule's outcomes to it, reporting each source-wide cause once.
830///
831/// An unbound concept depends on the package and the source, an unrecorded
832/// fact on the source alone, and a missing service on the run, never on the
833/// object, so every object of that source fails identically. Listing each
834/// one buries the single cause under thousands of copies. Object-level
835/// outcomes with any of these reasons
836/// are merged per source, reason and message into one rule-level outcome
837/// scoped to that source, naming the count and a few examples. Every other
838/// outcome keeps its scope.
839fn 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
895/// One rule's counts from its refined outcomes.
896fn 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
918/// Deterministic runtime that invokes only registered trusted capabilities.
919pub struct Runtime {
920    registry: CapabilityRegistry,
921    services: ServiceRegistry,
922    locations: Option<LocationPolicy>,
923    summaries: bool,
924}
925impl Runtime {
926    /// Creates a runtime from a host-controlled registry.
927    pub fn new(registry: CapabilityRegistry) -> Self {
928        Self {
929            registry,
930            services: ServiceRegistry::new(),
931            locations: None,
932            summaries: false,
933        }
934    }
935    /// Reports per-rule counts and status ([`Report::rules`]): how many
936    /// objects each rule surely selected, how many it found or left not
937    /// evaluated, and whether it passed, failed, was not evaluated or
938    /// selected nothing. Off by default, and then reports are unchanged.
939    /// Needs the registry's outcome refiner to count selections.
940    #[must_use]
941    pub fn with_rule_summaries(mut self) -> Self {
942        self.summaries = true;
943        self
944    }
945    /// Locates every finding and not-evaluated outcome by storey and space
946    /// as `policy` says. Off by default, and then reports carry no
947    /// location. Needs the registry's outcome refiner.
948    #[must_use]
949    pub fn with_locations(mut self, policy: LocationPolicy) -> Self {
950        self.locations = Some(policy);
951        self
952    }
953    /// Adds adapter-provided host services to subsequent evaluations.
954    #[must_use]
955    pub fn with_services(mut self, services: ServiceRegistry) -> Self {
956        self.services = services;
957        self
958    }
959    /// Executes a plan and returns deterministically sorted findings.
960    ///
961    /// Execution fails closed if the host registry no longer contains any capability
962    /// that was present when the plan was compiled.
963    ///
964    /// A bare project carries no source type-system declarations, so package
965    /// concepts bind to nothing and concept-based selection is reported as not
966    /// evaluated. Hosts that want concept binding run an [`EvidenceSession`].
967    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    /// Executes a plan against one immutable source/evidence snapshot.
982    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    /// The registry's outcome refiner, refused missing when the plan or
1009    /// the host's options need one.
1010    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    /// Evaluates `rule`, then grades and refines its outcomes.
1038    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        // Bindings are per run: they join this plan's package concepts to this
1071        // project's declared type systems, so they cannot be registered once by
1072        // a host. A host-registered `ConceptBindings` is overridden rather than
1073        // trusted, because it could bind concepts the packages never declared.
1074        let mut services = services.clone();
1075        services.replace(ConceptBindings::new(plan.concepts.clone(), type_systems));
1076        // Disciplines are the session's declarations; a host-registered copy
1077        // could claim roles the session never declared.
1078        services.replace(disciplines);
1079        // So is what the session knows about each source as a whole.
1080        services.replace(metadata);
1081        // So are the sources: a host copy could hide an empty source.
1082        services.replace(sources);
1083        // And the resource objects the rules name: a host copy could claim
1084        // resource objects the session never listed.
1085        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        // Measured values are answered through the host's resolver in
1107        // every run; classifications are derived first when the plan has
1108        // any.
1109        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        // What every completed rule reported, for the rules that read it.
1123        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            // An auxiliary rule's outcome is recorded for the rules that
1131            // read it, and reported only through them.
1132            let reported = !plan.auxiliary.contains(&rule_id);
1133            // A rule reads the outcomes of the rules the plan ran before it.
1134            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            // The compiled rule is the table's identity, whatever the capability named.
1180            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
1191/// The resource objects the outcomes name, sorted by identity.
1192fn 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
1221/// Classifies every object by each of `definitions`, in order, and installs
1222/// the run's property resolver answering the classification set: each
1223/// classification's rows read the ones derived before it.
1224fn 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
1240/// The report of every rule's outcomes, each part in its deterministic
1241/// order, with the resource objects of `services` they name; a rule
1242/// reporting one table name twice fails the run.
1243fn 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            // Project, then sources, then objects, each by identity.
1254            .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, &not_evaluated, &tables, services),
1271        stale_decisions: Vec::new(),
1272        findings,
1273        not_evaluated,
1274        tables,
1275        rules: summaries,
1276    })
1277}