#![forbid(unsafe_code)]
#![allow(missing_docs, clippy::missing_errors_doc)]
use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
pub use axioval_ir::NotEvaluatedReason;
use axioval_ir::contract as schema;
use axioval_ir::{
Finding, Location, NotEvaluated, ObjectId, Project, Report, ReportTable, RuleId, RuleSummary,
Scope, SourceId,
};
use thiserror::Error;
mod session;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum EngineError {
#[error(
"unsupported schema version `{version}` for {package_kind} `{package_id}`; supported: {supported}"
)]
UnsupportedSchemaVersion {
package_kind: &'static str,
package_id: String,
version: String,
supported: &'static str,
},
#[error("duplicate definition package `{0}`")]
DuplicateDefinitionPackage(String),
#[error("unknown capability `{0}`")]
UnknownCapability(String),
#[error("duplicate capability `{0}`")]
DuplicateCapability(String),
#[error("capability `{capability}` does not declare parameter `{parameter}`")]
UnknownParameter {
capability: String,
parameter: String,
},
#[error("capability `{capability}` requires parameter `{parameter}`")]
MissingParameter {
capability: String,
parameter: String,
},
#[error("capability `{capability}` parameter `{parameter}` has invalid type")]
InvalidParameterType {
capability: String,
parameter: String,
},
#[error("capability `{capability}` parameter `{parameter}` row {row}: {detail}")]
InvalidTableRow {
capability: String,
parameter: String,
row: usize,
detail: String,
},
#[error("rule has duplicate parameter binding `{0}`")]
DuplicateBinding(String),
#[error("invalid rule id `{0}`")]
InvalidRuleId(String),
#[error("unknown rule definition `{0}`")]
UnknownDefinition(String),
#[error("missing definition package `{0}`")]
MissingDefinitionPackage(String),
#[error("definition `{definition}` conflicts with capability `{capability}`: {detail}")]
CapabilityContract {
definition: String,
capability: String,
detail: String,
},
#[error("duplicate rule id `{0}`")]
DuplicateRule(String),
#[error("duplicate ruleset package `{0}`")]
DuplicateRuleSet(String),
#[error("no ruleset to compile")]
NoRuleSet,
#[error("duplicate concept `{0}` across definition packages")]
DuplicateConcept(String),
#[error("rule `{rule}` references unknown {kind} concept `{concept}`")]
UnknownConcept {
rule: String,
kind: String,
concept: String,
},
#[error("rule `{rule}` reported table `{table}` twice")]
DuplicateReportTable { rule: String, table: String },
#[error("rule `{rule}`: {detail}")]
InvalidRefinement { rule: String, detail: String },
#[error("{0} needs an outcome refiner, and the host registered none")]
MissingRefiner(&'static str),
#[error("rule `{rule}`: {detail}")]
InvalidDependency { rule: String, detail: String },
#[error("classification `{classification}`: {detail}")]
InvalidClassification {
classification: String,
detail: String,
},
}
pub use schema::ColumnKind;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TableColumn {
pub id: &'static str,
pub kind: ColumnKind,
pub required: bool,
}
impl TableColumn {
#[must_use]
pub const fn required(id: &'static str, kind: ColumnKind) -> Self {
Self {
id,
kind,
required: true,
}
}
#[must_use]
pub const fn optional(id: &'static str, kind: ColumnKind) -> Self {
Self {
id,
kind,
required: false,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ParameterType {
Boolean,
Integer,
Number,
String,
Quantity,
Enum,
Date,
DateTime,
Reference,
ObjectTypeReference,
PropertyReference,
Selector,
StringList,
ReferenceList,
Table(&'static [TableColumn]),
}
impl ParameterType {
#[must_use]
pub fn package_kind(self) -> &'static str {
match self {
Self::Boolean => "boolean",
Self::Integer => "integer",
Self::Number => "number",
Self::String => "string",
Self::Quantity => "quantity",
Self::Enum => "enum",
Self::Date => "date",
Self::DateTime => "dateTime",
Self::Reference => "reference",
Self::ObjectTypeReference => "objectTypeReference",
Self::PropertyReference => "propertyReference",
Self::Selector => "selector",
Self::StringList => "stringList",
Self::ReferenceList => "referenceList",
Self::Table(_) => "table",
}
}
fn accepts(self, value: &schema::ParameterValue) -> bool {
matches!(
(self, value),
(Self::Boolean, schema::ParameterValue::Boolean { .. })
| (Self::Integer, schema::ParameterValue::Integer { .. })
| (Self::Number, schema::ParameterValue::Number { .. })
| (Self::String, schema::ParameterValue::String { .. })
| (Self::Quantity, schema::ParameterValue::Quantity { .. })
| (Self::Enum, schema::ParameterValue::Enum { .. })
| (Self::Date, schema::ParameterValue::Date { .. })
| (Self::DateTime, schema::ParameterValue::DateTime { .. })
| (Self::Reference, schema::ParameterValue::Reference { .. })
| (
Self::ObjectTypeReference,
schema::ParameterValue::ObjectTypeReference { .. }
)
| (
Self::PropertyReference,
schema::ParameterValue::PropertyReference { .. }
)
| (Self::Selector, schema::ParameterValue::Selector { .. })
| (Self::StringList, schema::ParameterValue::StringList { .. })
| (
Self::ReferenceList,
schema::ParameterValue::ReferenceList { .. }
)
| (Self::Table(_), schema::ParameterValue::Table { .. })
)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParameterDescriptor {
pub name: String,
pub parameter_type: ParameterType,
pub required: bool,
}
impl ParameterDescriptor {
pub fn required(name: impl Into<String>, parameter_type: ParameterType) -> Self {
Self {
name: name.into(),
parameter_type,
required: true,
}
}
pub fn optional(name: impl Into<String>, parameter_type: ParameterType) -> Self {
Self {
name: name.into(),
parameter_type,
required: false,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CompiledRule {
pub id: RuleId,
pub capability: String,
pub severity: schema::Severity,
pub selector: schema::Selector,
pub parameters: BTreeMap<String, schema::ParameterValue>,
}
pub struct RuleContext<'a> {
pub project: &'a Project,
pub services: &'a ServiceRegistry,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CapabilityEvaluation {
findings: Vec<Finding>,
not_evaluated: Vec<CapabilityNotEvaluated>,
tables: Vec<ReportTable>,
graded: Vec<(usize, Deviation)>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CapabilityNotEvaluated {
scope: Scope,
reason: NotEvaluatedReason,
message: String,
location: Option<Location>,
}
impl CapabilityNotEvaluated {
#[must_use]
pub fn object_id(&self) -> Option<&ObjectId> {
self.scope.object()
}
#[must_use]
pub fn scope(&self) -> &Scope {
&self.scope
}
#[must_use]
pub fn reason(&self) -> &NotEvaluatedReason {
&self.reason
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub fn location(&self) -> Option<&Location> {
self.location.as_ref()
}
pub fn set_location(&mut self, location: Location) {
self.location = Some(location);
}
}
impl CapabilityEvaluation {
#[must_use]
pub fn findings(&self) -> &[Finding] {
&self.findings
}
#[must_use]
pub fn not_evaluated_outcomes(&self) -> &[CapabilityNotEvaluated] {
&self.not_evaluated
}
#[must_use]
pub fn tables(&self) -> &[ReportTable] {
&self.tables
}
#[must_use]
pub fn evaluated(findings: Vec<Finding>) -> Self {
Self {
findings,
..Self::default()
}
}
pub fn push_table(&mut self, table: ReportTable) {
if !table.is_empty() {
self.tables.push(table);
}
}
#[must_use]
pub fn not_evaluated(reason: NotEvaluatedReason, message: impl Into<String>) -> Self {
let mut outcome = Self::default();
outcome.push_not_evaluated(reason, message);
outcome
}
pub fn push_finding(&mut self, finding: Finding) {
self.findings.push(finding);
}
pub fn push_graded_finding(&mut self, finding: Finding, deviation: Deviation) {
self.graded.push((self.findings.len(), deviation));
self.findings.push(finding);
}
pub fn push_finding_deviating(&mut self, finding: Finding, deviation: Option<Deviation>) {
match deviation {
Some(deviation) => self.push_graded_finding(finding, deviation),
None => self.push_finding(finding),
}
}
#[must_use]
pub fn deviation(&self, finding: usize) -> Option<Deviation> {
self.graded
.iter()
.find(|(index, _)| *index == finding)
.map(|(_, deviation)| *deviation)
}
fn grade(&mut self, bands: &[schema::SeverityBand]) {
for (index, deviation) in std::mem::take(&mut self.graded) {
let finding = &mut self.findings[index];
let (severity, mixed) = refinement::grade(bands, &finding.severity, deviation);
if mixed {
use std::fmt::Write as _;
let _ = write!(
finding.message,
"; deviation between {} and {}, graded {} by its most severe band",
percent(deviation.lower()),
percent(deviation.upper()),
refinement::label(&severity)
);
}
finding.severity = severity;
}
}
pub fn findings_mut(&mut self) -> &mut [Finding] {
&mut self.findings
}
pub fn not_evaluated_outcomes_mut(&mut self) -> &mut [CapabilityNotEvaluated] {
&mut self.not_evaluated
}
pub fn take_findings(&mut self) -> Vec<Finding> {
self.graded.clear();
std::mem::take(&mut self.findings)
}
pub fn push_not_evaluated_about(
&mut self,
scope: Scope,
reason: NotEvaluatedReason,
message: impl Into<String>,
) {
self.push_unavailable(scope, reason, message);
}
pub fn push_not_evaluated(&mut self, reason: NotEvaluatedReason, message: impl Into<String>) {
self.push_unavailable(Scope::Project, reason, message);
}
pub fn push_source_not_evaluated(
&mut self,
source: SourceId,
reason: NotEvaluatedReason,
message: impl Into<String>,
) {
self.push_unavailable(Scope::Source(source), reason, message);
}
pub fn push_object_not_evaluated(
&mut self,
object_id: ObjectId,
reason: NotEvaluatedReason,
message: impl Into<String>,
) {
self.push_unavailable(Scope::Object(object_id), reason, message);
}
fn push_unavailable(
&mut self,
scope: Scope,
reason: NotEvaluatedReason,
message: impl Into<String>,
) {
self.not_evaluated.push(CapabilityNotEvaluated {
scope,
reason,
message: message.into(),
location: None,
});
}
}
fn percent(value: f64) -> String {
if value.is_finite() {
format!("{} %", (value * 1e4).round() / 1e2)
} else {
"unbounded".to_owned()
}
}
pub trait RuleCapability: Send + Sync {
fn id(&self) -> &'static str;
fn parameters(&self) -> Vec<ParameterDescriptor>;
fn grades_deviation(&self) -> bool {
false
}
fn evaluate(&self, context: &RuleContext<'_>, rule: &CompiledRule) -> CapabilityEvaluation;
}
#[derive(Clone, Default)]
pub struct CapabilityRegistry {
capabilities: BTreeMap<String, Arc<dyn RuleCapability>>,
refiner: Option<Arc<dyn OutcomeRefiner>>,
}
impl CapabilityRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register<C: RuleCapability + 'static>(
mut self,
capability: C,
) -> Result<Self, EngineError> {
let id = capability.id().to_owned();
if self
.capabilities
.insert(id.clone(), Arc::new(capability))
.is_some()
{
return Err(EngineError::DuplicateCapability(id));
}
Ok(self)
}
pub fn get(&self, id: &str) -> Option<&Arc<dyn RuleCapability>> {
self.capabilities.get(id)
}
#[must_use]
pub fn with_refiner<R: OutcomeRefiner + 'static>(mut self, refiner: R) -> Self {
self.refiner = Some(Arc::new(refiner));
self
}
pub fn refiner(&self) -> Option<&Arc<dyn OutcomeRefiner>> {
self.refiner.as_ref()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeferredRule {
pub id: RuleId,
pub capability: String,
pub reason: String,
}
#[derive(Clone, Debug)]
pub struct ExecutionPlan {
rules: Vec<CompiledRule>,
deferred: Vec<DeferredRule>,
concepts: Arc<ConceptCatalog>,
refinements: BTreeMap<RuleId, RuleRefinement>,
gates: BTreeMap<RuleId, Vec<(RuleId, schema::GateCondition)>>,
recorded: BTreeSet<RuleId>,
auxiliary: BTreeSet<RuleId>,
classifications: Vec<schema::ClassificationDefinition>,
}
impl ExecutionPlan {
pub fn rules(&self) -> &[CompiledRule] {
&self.rules
}
pub fn deferred(&self) -> &[DeferredRule] {
&self.deferred
}
pub fn concepts(&self) -> &ConceptCatalog {
&self.concepts
}
pub fn refinement(&self, rule: &RuleId) -> Option<&RuleRefinement> {
self.refinements.get(rule)
}
pub fn classifications(&self) -> &[schema::ClassificationDefinition] {
&self.classifications
}
pub fn gates(&self, rule: &RuleId) -> &[(RuleId, schema::GateCondition)] {
self.gates.get(rule).map_or(&[], Vec::as_slice)
}
pub fn is_auxiliary(&self, rule: &RuleId) -> bool {
self.auxiliary.contains(rule)
}
}
mod boundary_coverage;
mod circulation;
mod classifications;
mod compiler;
mod concepts;
mod contact;
mod coordinate_system;
mod corridor_end;
mod coverage;
mod derived;
mod derived_relationships;
mod discipline_map;
mod door_leaves;
mod envelope_membership;
mod facade_area;
mod federation;
mod free_space;
mod guard;
mod integrity;
mod linear_quantity;
mod measured;
mod metric_routing;
mod object_frame;
mod pairwise;
mod path;
mod plan_area;
mod plan_region;
mod plan_span;
mod properties;
mod proximity;
mod refinement;
mod relationships;
mod resources;
mod rule_outcomes;
mod services;
mod side_distance;
mod sight;
mod source_metadata;
mod space;
mod topology;
mod triangle_count;
mod vertical_extent;
mod walkability;
mod walking_surface;
pub use boundary_coverage::{
BoundaryCoverage, BoundaryCoverageError, BoundaryCoverageRequest, BoundaryCoverageService,
BoundaryCoverageServiceHandle, BoundaryOverlap, BoundaryPlacement, CoverageAreas,
MeasuredBoundary, ShareInterval, SurfaceAreaInterval,
};
pub use circulation::{
CirculationContact, CirculationMap, CirculationNode, CirculationNodeKind, CirculationRequest,
};
pub use classifications::{
ClassificationAssignment, ClassificationError, ClassificationService,
ClassificationServiceHandle,
};
pub use compiler::{QUALIFIED_RULE_SEPARATOR, SUPPORTED_SCHEMA_VERSION, compile, compile_rulesets};
pub use concepts::{
BindingError, ConceptBindings, ConceptCatalog, ConceptKind, TypeHierarchyError,
TypeHierarchyService, TypeHierarchyServiceHandle,
};
pub use contact::{
ContactError, ContactEvidence, ContactRequest, ContactService, ContactServiceHandle,
ContactSide, ContactTolerance,
};
pub use coordinate_system::{
CoordinateFrame, CoordinateSystemError, CoordinateSystemService, CoordinateSystemServiceHandle,
MapConversion, SourceCoordinateSystem,
};
pub use corridor_end::{CorridorEnd, CorridorEndRequest, CorridorEnds, EndWall, WallContact};
pub use coverage::{CoverageEvidence, CoverageRequest, EffectMeets, EffectReach, Participant};
pub use derived::{ClassOutcome, Classifications};
pub use derived_relationships::{
AdjacentSide, DERIVED_RELATIONSHIP_PREFIX, Derivation, DerivedRelationshipService,
DerivedRelationshipServiceHandle, LevelFacts, LevelMatch, adjacent_side,
};
pub use discipline_map::{
DisciplineMap, DisciplineMapError, DisciplineOrigin, DisciplineRule, UnmappedReason,
wildcard_regex,
};
pub use door_leaves::{
DoorLeaf, DoorLeaves, DoorLeavesError, HingeSide, LeafMotion, LeafPosition, PlanRing,
SWEPT_FLOOR_REACH_METRES, SweptDoor, SwingSector,
};
pub use envelope_membership::{
EnvelopeDerivation, EnvelopeMembershipError, EnvelopeMembershipEvidence,
EnvelopeMembershipRequest, EnvelopeMembershipService, EnvelopeMembershipServiceHandle,
};
pub use facade_area::{FacadeArea, FacadeAreaError, FacadeAreaService, FacadeAreaServiceHandle};
pub use free_space::{
AreaInterval, BoxClearance, ClearanceOutcome, ClearancePlacementEvidence, ClearanceRequest,
ClearanceShape, CompleteClearanceEvidence, CompletePlacementEvidence, CompleteSupportEvidence,
ContainmentEvidence, ContainmentOutcome, ContainmentRequest, CylinderClearance, ElevationBand,
EntranceReach, FrameOffsetPlacement, FreeAreaEvidence, FreeAreaRequest, FreeSpaceError,
FreeSpaceService, FreeSpaceServiceHandle, MetricDirection, MetricFrame, ObstructionEvidence,
PlacementDomain, PlacementOrientation, PlacementOutcome, PlacementRequest, PlacementShape,
SignedDistanceInterval, SupportCoverageEvidence, SupportCoverageOutcome,
SupportCoverageRequest, SupportedPlacement,
};
pub use guard::{
ClimbableCandidate, GuardCandidate, GuardEdge, GuardError, GuardEvidence, GuardSearch,
GuardService, GuardServiceHandle,
};
pub use integrity::{
IntegrityError, IntegrityIssue, IntegritySeverity, SourceIntegrityService,
SourceIntegrityServiceHandle,
};
pub use linear_quantity::{
LinearInterval, LinearQuantityError, LinearQuantityEvidence, LinearQuantityKind,
LinearQuantityRequest, LinearQuantityService, LinearQuantityServiceHandle, ShelfGeometry,
};
pub use metric_routing::{
BlockedMetricRouteEvidence, ClimbLength, CompleteMetricEvidence, ConnectorRouting,
FarthestPointEvidence, FarthestPointOutcome, FarthestPointRequest, ForcedWalkEvidence,
ForcedWalkOutcome, ForcedWalkRequest, LengthInterval, MetricPoint, MetricRouteEvidence,
MetricRouteOutcome, MetricRouteRequest, MetricRoutingError, MetricRoutingService,
MetricRoutingServiceHandle, MobilityProfile, NearestTargetEvidence, NearestTargetOutcome,
NearestTargetRequest, NeverEnteredEvidence, PathTrace, PathTraceRequest, StairLength,
ThresholdVerdict, TravelCost, UnreachableRegionEvidence, UnreachableTargetsEvidence,
};
pub use object_frame::{
ObjectFrame, ObjectFrameError, ObjectFrameService, ObjectFrameServiceHandle, ObjectFront,
};
pub use pairwise::{
CandidatePair, CandidateSearchError, candidate_pairs, projected_candidate_pairs,
};
pub use path::PathSegment;
pub use plan_area::{
ElevationCover, ElevationRequest, PlanArea, PlanAreaError, PlanAreaService,
PlanAreaServiceHandle, PlanBand,
};
pub use plan_region::ConvexPlanRegion;
pub use plan_span::{
CentrePlacement, PlanCentre, PlanLength, PlanRecess, PlanRecesses, PlanRectangle, PlanSection,
PlanSpan, PlanSpanError, PlanSpanService, PlanSpanServiceHandle, RectangleOrientation,
};
pub use properties::{
CompletePropertyAbsenceEvidence, NameMatch, NamePattern, PropertyEnumeration,
PropertyEnumerationRequest, PropertyRequest, PropertyResolution, PropertyResolutionError,
PropertyResolutionService, PropertyResolutionServiceHandle, ResolvedProperty, UnreadableValue,
};
pub use proximity::{
BodyContainment, BodyVolume, Bounds3, CounterpartSurface, FaceClass, FaceDistanceError,
FaceDistanceEvidence, FaceDistanceRequest, GeometryFidelity, IntersectionVolume, ObjectBounds,
OverlapAlongEvidence, OverlapAlongRequest, OverlapExtents, ProjectedDistanceEvidence,
ProximityError, ProximityEvidence, ProximityProjection, ProximityRequest, ProximityService,
ProximityServiceHandle, RegionDistanceEvidence, RegionDistanceRequest, SubjectSurface,
VerticalDirection, VerticalSurfaces, VolumeInterval,
};
pub use refinement::{
Deviation, LocationMethod, LocationPolicy, OutcomeRefiner, Refining, RuleRefinement,
report_severity,
};
pub use relationships::{
AbsentEndPolicy, CompleteRelationshipSelection, RelationshipQuery, RelationshipSelectionError,
RelationshipSelectionRequest, RelationshipSelectionService, RelationshipSelectionServiceHandle,
SemanticRelationship, TraversalDirection,
};
pub use resources::{
Reached, ResourceError, ResourceObjects, ResourceRequest, ResourceService,
ResourceServiceHandle,
};
pub use rule_outcomes::{ObjectVerdict, RuleOutcomes, RuleRecord, RuleVerdict, SelectorVerdict};
pub use services::{ServiceRegistry, ServiceRegistryError};
pub use session::{
EvidenceSession, EvidenceSessionError, SessionSources, SnapshotBoundService, SourceDisciplines,
SourceSnapshot,
};
pub use side_distance::{
RectangleSide, SideDistance, SideDistanceRequest, SideDistances, SidePresence,
};
pub use sight::{
SightError, SightEvidence, SightOutcome, SightRequest, SightService, SightServiceHandle,
};
pub use source_metadata::{SourceMetadata, SourceMetadataIndex};
pub use space::{
BoundaryGap, BoundaryRequest, Cap, CapCoverage, CapRequest, ClearHeightEvidence, Containment,
OverlapRequest, SpaceError, SpaceOverlap, SpaceService, SpaceServiceHandle, SupportCounts,
UnallocatedRegion,
};
pub use topology::{
CompleteTopologyEvidence, ConnectivityGraph, RouteOutcome, TopologyError, VerifiedConnection,
};
pub use triangle_count::{
TriangleCount, TriangleCountError, TriangleCountService, TriangleCountServiceHandle,
};
pub use vertical_extent::{
DirectionalExtent, ElevationInterval, VerticalExtent, VerticalExtentError,
VerticalExtentService, VerticalExtentServiceHandle,
};
pub use walkability::{
PassageAdmission, StretchLimit, VerifiedWalkablePassage, VerticalConnector,
VerticalConnectorKind, WalkabilityError, WalkabilityRegion, WalkabilityRegionId,
WalkabilityRequest, WalkabilityRouteOutcome, WalkabilityService, WalkabilityServiceHandle,
WalkabilitySnapshot, WalkableStretch,
};
pub use walking_surface::{
ClearWidthEvidence, ClearWidthRequest, ClearanceBelow, ClearanceBelowRequest, HandrailEvidence,
HandrailRequest, Headroom, HeadroomRequest, Landing, LandingEvidence, LandingExtent,
LandingRequest, MeasuredInterval, PlanSegment, RailMeasurement, RailSide, RiserClosure,
SlopedRun, SlopedSurface, StretchPart, Tread, TreadFlight, TreadFlightRequest, WalkingEnd,
WalkingLine, WalkingLinePlacement, WalkingStretch, WalkingSurfaceError, WalkingSurfaceService,
WalkingSurfaceServiceHandle, across,
};
fn collapse_source_wide(
rule_id: &RuleId,
outcomes: Vec<CapabilityNotEvaluated>,
) -> Vec<NotEvaluated> {
const EXAMPLES: usize = 3;
let mut merged: BTreeMap<(axioval_ir::SourceId, NotEvaluatedReason, String), Vec<ObjectId>> =
BTreeMap::new();
let mut kept = Vec::new();
for outcome in outcomes {
match (outcome.reason, outcome.scope) {
(
reason @ (NotEvaluatedReason::UnboundConcept
| NotEvaluatedReason::NotRecorded
| NotEvaluatedReason::MissingService),
Scope::Object(object),
) => merged
.entry((object.source.clone(), reason, outcome.message))
.or_default()
.push(object),
(reason, scope) => kept.push(NotEvaluated {
rule_id: rule_id.clone(),
scope,
reason,
message: outcome.message,
location: outcome.location,
}),
}
}
kept.extend(merged.into_iter().map(|((source, reason, message), mut objects)| {
objects.sort();
let examples: Vec<&str> = objects
.iter()
.take(EXAMPLES)
.map(|object| object.local_id.as_str())
.collect();
let more = objects.len().saturating_sub(EXAMPLES);
let tail = if more > 0 {
format!(", +{more} more")
} else {
String::new()
};
NotEvaluated {
rule_id: rule_id.clone(),
scope: Scope::Source(source.clone()),
reason,
message: format!(
"{message}; {} object(s) of source `{source}` not evaluated (e.g. {}{tail})",
objects.len(),
examples.join(", ")
),
location: None,
}
}));
kept
}
fn summarize(rule: &RuleId, checked: usize, evaluation: &CapabilityEvaluation) -> RuleSummary {
let objects = |scopes: &mut dyn Iterator<Item = &Scope>| {
scopes
.filter_map(Scope::object)
.collect::<std::collections::BTreeSet<_>>()
.len()
};
let failed = objects(&mut evaluation.findings.iter().map(|finding| &finding.scope));
let open = objects(
&mut evaluation
.not_evaluated
.iter()
.map(|outcome| &outcome.scope),
);
RuleSummary::new(
rule.clone(),
checked,
(failed, !evaluation.findings.is_empty()),
(open, !evaluation.not_evaluated.is_empty()),
)
}
pub struct Runtime {
registry: CapabilityRegistry,
services: ServiceRegistry,
locations: Option<LocationPolicy>,
summaries: bool,
}
impl Runtime {
pub fn new(registry: CapabilityRegistry) -> Self {
Self {
registry,
services: ServiceRegistry::new(),
locations: None,
summaries: false,
}
}
#[must_use]
pub fn with_rule_summaries(mut self) -> Self {
self.summaries = true;
self
}
#[must_use]
pub fn with_locations(mut self, policy: LocationPolicy) -> Self {
self.locations = Some(policy);
self
}
#[must_use]
pub fn with_services(mut self, services: ServiceRegistry) -> Self {
self.services = services;
self
}
pub fn run(&self, project: &Project, plan: ExecutionPlan) -> Result<Report, EngineError> {
self.run_with_services(
project,
&self.services,
BTreeMap::new(),
(
SessionSources::new(project.objects().map(|object| object.id.source.clone())),
SourceDisciplines::default(),
SourceMetadataIndex::default(),
),
plan,
)
}
pub fn run_session(
&self,
session: &EvidenceSession,
plan: ExecutionPlan,
) -> Result<Report, EngineError> {
let type_systems = session
.snapshots()
.map(|snapshot| (snapshot.source().clone(), snapshot.type_systems().to_vec()))
.collect();
self.run_with_services(
session.project(),
session.services(),
type_systems,
(
SessionSources::new(
session
.snapshots()
.map(|snapshot| snapshot.source().clone()),
),
session.source_disciplines(),
session.metadata_index(),
),
plan,
)
}
fn refiner_for(
&self,
plan: &ExecutionPlan,
) -> Result<Option<&Arc<dyn OutcomeRefiner>>, EngineError> {
let refiner = self.registry.refiner();
if refiner.is_some() {
return Ok(refiner);
}
if !plan.recorded.is_empty() {
return Err(EngineError::MissingRefiner(
"reading another rule's outcomes per object",
));
}
if !plan.classifications.is_empty() {
return Err(EngineError::MissingRefiner("deriving classifications"));
}
if self.locations.is_some() {
return Err(EngineError::MissingRefiner("locating outcomes"));
}
if self.summaries {
return Err(EngineError::MissingRefiner(
"counting each rule's selection",
));
}
Ok(None)
}
fn evaluate(
&self,
context: &RuleContext<'_>,
capability: &dyn RuleCapability,
rule: &CompiledRule,
refinement: Option<&RuleRefinement>,
) -> CapabilityEvaluation {
let mut evaluation = capability.evaluate(context, rule);
if let Some(refinement) = refinement {
evaluation.grade(&refinement.severity_bands);
}
if let Some(refiner) = self.registry.refiner()
&& (refinement.is_some() || self.locations.is_some())
{
let unrefined = RuleRefinement::default();
let refining = Refining {
refinement: refinement.unwrap_or(&unrefined),
locations: self.locations.as_ref(),
};
refiner.refine(context, rule, &refining, &mut evaluation);
}
evaluation
}
fn run_with_services(
&self,
project: &Project,
services: &ServiceRegistry,
type_systems: BTreeMap<axioval_ir::SourceId, Vec<Arc<str>>>,
(sources, disciplines, metadata): (SessionSources, SourceDisciplines, SourceMetadataIndex),
plan: ExecutionPlan,
) -> Result<Report, EngineError> {
let mut services = services.clone();
services.replace(ConceptBindings::new(plan.concepts.clone(), type_systems));
services.replace(disciplines);
services.replace(metadata);
services.replace(sources);
resources::install(&mut services, project, &plan.rules);
let refiner = self.refiner_for(&plan)?;
let mut summaries: Vec<RuleSummary> = Vec::new();
let mut findings = Vec::new();
let mut tables: Vec<ReportTable> = Vec::new();
let mut not_evaluated: Vec<NotEvaluated> = plan
.deferred
.into_iter()
.inspect(|rule| {
if self.summaries {
summaries.push(RuleSummary::new(rule.id.clone(), 0, (0, false), (0, true)));
}
})
.map(|rule| NotEvaluated {
rule_id: rule.id,
scope: Scope::Project,
reason: NotEvaluatedReason::InvalidDeclaration,
message: rule.reason,
location: None,
})
.collect();
if plan.classifications.is_empty()
&& let Some(host) = services.get::<PropertyResolutionServiceHandle>().cloned()
{
derived::install(&mut services, Some(&host), Arc::default(), project);
}
if let Some(refiner) = refiner.filter(|_| !plan.classifications.is_empty()) {
derive_classifications(
refiner.as_ref(),
project,
&mut services,
&plan.classifications,
);
}
let mut outcomes = RuleOutcomes::default();
for rule in plan.rules {
let capability = self
.registry
.get(&rule.capability)
.ok_or_else(|| EngineError::UnknownCapability(rule.capability.clone()))?;
let rule_id = rule.id.clone();
let reported = !plan.auxiliary.contains(&rule_id);
services.replace(outcomes.clone());
let context = RuleContext {
project,
services: &services,
};
let gates = plan.gates.get(&rule_id).map_or(&[][..], Vec::as_slice);
let mut evaluation = match outcomes.gate(gates) {
rule_outcomes::Gate::Closed => {
outcomes.insert(rule_id.clone(), RuleRecord::skipped());
if self.summaries && reported {
summaries.push(RuleSummary::skipped(rule_id));
}
continue;
}
rule_outcomes::Gate::Undecided(why) => {
outcomes.insert(rule_id.clone(), RuleRecord::undecided(&why));
CapabilityEvaluation::not_evaluated(NotEvaluatedReason::IncompleteEvidence, why)
}
rule_outcomes::Gate::Open => {
let evaluation = self.evaluate(
&context,
capability.as_ref(),
&rule,
plan.refinements.get(&rule_id),
);
let selection = refiner
.filter(|_| plan.recorded.contains(&rule_id))
.map(|refiner| rule_outcomes::selection(refiner.as_ref(), &context, &rule));
outcomes.insert(rule_id.clone(), RuleRecord::of(&evaluation, selection));
evaluation
}
};
if !reported {
continue;
}
if let Some(refiner) = refiner
&& self.summaries
{
summaries.push(summarize(
&rule_id,
refiner.selected(&context, &rule),
&evaluation,
));
}
findings.extend(std::mem::take(&mut evaluation.findings));
tables.extend(
std::mem::take(&mut evaluation.tables)
.into_iter()
.map(|table| table.with_rule_id(rule_id.clone())),
);
not_evaluated.extend(collapse_source_wide(&rule_id, evaluation.not_evaluated));
}
assemble(findings, not_evaluated, tables, summaries, &services)
}
}
fn named_resources(
findings: &[Finding],
not_evaluated: &[NotEvaluated],
tables: &[ReportTable],
services: &ServiceRegistry,
) -> Vec<axioval_ir::Object> {
let Some(resources) = services
.get::<ResourceObjects>()
.filter(|resources| !resources.is_empty())
else {
return Vec::new();
};
let mut named: BTreeSet<&ObjectId> = BTreeSet::new();
for finding in findings {
named.extend(finding.object_id());
named.extend(&finding.related);
}
named.extend(not_evaluated.iter().filter_map(NotEvaluated::object_id));
named.extend(
tables
.iter()
.flat_map(|table| table.rows().iter().filter_map(|row| row.scope().object())),
);
named
.into_iter()
.filter_map(|id| resources.object(id).cloned())
.collect()
}
fn derive_classifications(
refiner: &dyn OutcomeRefiner,
project: &Project,
services: &mut ServiceRegistry,
definitions: &[schema::ClassificationDefinition],
) {
let host = services.get::<PropertyResolutionServiceHandle>().cloned();
let mut derived = Classifications::default();
for definition in definitions {
derived::install(services, host.as_ref(), Arc::new(derived.clone()), project);
let context = RuleContext { project, services };
derived.classify(refiner, &context, definition);
}
derived::install(services, host.as_ref(), Arc::new(derived), project);
}
fn assemble(
mut findings: Vec<Finding>,
mut not_evaluated: Vec<NotEvaluated>,
mut tables: Vec<ReportTable>,
mut summaries: Vec<RuleSummary>,
services: &ServiceRegistry,
) -> Result<Report, EngineError> {
findings.sort_by(|a, b| {
a.rule_id
.cmp(&b.rule_id)
.then_with(|| a.scope.cmp(&b.scope))
.then_with(|| a.message.cmp(&b.message))
});
not_evaluated.sort();
tables.sort_by(|a, b| (a.rule_id(), a.name()).cmp(&(b.rule_id(), b.name())));
if let Some(pair) = tables
.windows(2)
.find(|pair| (pair[0].rule_id(), pair[0].name()) == (pair[1].rule_id(), pair[1].name()))
{
return Err(EngineError::DuplicateReportTable {
rule: pair[0].rule_id().to_string(),
table: pair[0].name().to_owned(),
});
}
summaries.sort();
Ok(Report {
resources: named_resources(&findings, ¬_evaluated, &tables, services),
stale_decisions: Vec::new(),
findings,
not_evaluated,
tables,
rules: summaries,
})
}