Skip to main content

strixonomy_reasoner/
runner.rs

1use crate::adapter::{ReasonerAdapter, ReasonerId};
2use crate::auto::AutoAdapter;
3use crate::dl::DlAdapter;
4use crate::el::ElAdapter;
5use crate::error::{ReasonerError, Result};
6use crate::input::ReasonerInput;
7use crate::rdfs::RdfsAdapter;
8use crate::result::{
9    ClassificationResult, ConsistencyResult, ExplanationRequest, ExplanationResult,
10    InferredAssertions, InstanceCheckResult, RealizationResult,
11};
12use crate::rl::RlAdapter;
13use ontologos_profile::scanner::scan_constructs;
14
15pub fn adapter_for(id: ReasonerId) -> Result<Box<dyn ReasonerAdapter>> {
16    let adapter: Box<dyn ReasonerAdapter> = match id {
17        ReasonerId::El => Box::new(ElAdapter),
18        ReasonerId::Rl => Box::new(RlAdapter),
19        ReasonerId::Rdfs => Box::new(RdfsAdapter),
20        ReasonerId::Dl => Box::new(DlAdapter),
21        ReasonerId::Auto => Box::new(AutoAdapter),
22    };
23    Ok(adapter)
24}
25
26pub fn classify(
27    profile: ReasonerId,
28    input: &ReasonerInput,
29    auto_detect: bool,
30) -> Result<ClassificationResult> {
31    let mut warnings = profile_warnings(&input.ontology, profile, auto_detect)?;
32    if auto_detect {
33        if let Some(suggested) = suggest_profile(&input.ontology) {
34            if suggested != profile.as_str() {
35                warnings.push(crate::result::ReasonerWarning {
36                    code: "profile_suggestion".to_string(),
37                    message: format!(
38                        "ontology may be better suited to profile '{suggested}' (selected: {})",
39                        profile.as_str()
40                    ),
41                    suggested_profile: Some(suggested),
42                });
43            }
44        }
45    }
46    let adapter = adapter_for(profile)?;
47    let swrl_present = crate::swrl_run::input_has_swrl_rules(input);
48    let mut used_swrl = false;
49    let mut result = if swrl_present && matches!(profile, ReasonerId::Dl | ReasonerId::Auto) {
50        match crate::swrl_run::classify_with_swrl(input) {
51            Ok(r) => {
52                used_swrl = true;
53                r
54            }
55            Err(e) => {
56                warnings.push(crate::result::ReasonerWarning {
57                    code: "swrl_classify_failed".to_string(),
58                    message: format!(
59                        "SWRL-aware classify failed ({e}); falling back to {profile:?} without SWRL materialization"
60                    ),
61                    suggested_profile: None,
62                });
63                adapter.classify(input)?
64            }
65        }
66    } else {
67        if swrl_present {
68            // #360 — non-DL/Auto profiles must not silently ignore SWRL.
69            warnings.push(crate::result::ReasonerWarning {
70                code: "swrl_skipped_for_profile".to_string(),
71                message: format!(
72                    "SWRL rules are present but profile '{}' does not run SWRL materialization; use profile 'dl' or 'auto'",
73                    profile.as_str()
74                ),
75                suggested_profile: Some("dl".to_string()),
76            });
77        }
78        adapter.classify(input)?
79    };
80    // When SWRL materialization ran, consistency was already checked on the
81    // post-materialization ontology inside classify_with_swrl (#358).
82    if !used_swrl {
83        if let Ok(detail) =
84            crate::abox::check_full_consistency(&input.ontology, profile, &result.unsatisfiable)
85        {
86            result.consistent = detail.consistent;
87            if !detail.abox_clashes.is_empty() {
88                for clash in &detail.abox_clashes {
89                    warnings.push(crate::result::ReasonerWarning {
90                        code: "abox_clash".to_string(),
91                        message: clash.clone(),
92                        suggested_profile: None,
93                    });
94                }
95            }
96            if !detail.complete {
97                warnings.push(crate::result::ReasonerWarning {
98                    code: "consistency_incomplete".to_string(),
99                    message: "consistency check did not complete (budget or cancel)".into(),
100                    suggested_profile: None,
101                });
102            }
103        }
104    }
105    result.warnings.extend(warnings);
106    Ok(result)
107}
108
109pub fn check_consistency(profile: ReasonerId, input: &ReasonerInput) -> Result<ConsistencyResult> {
110    let adapter = adapter_for(profile)?;
111    adapter.check_consistency(input)
112}
113
114pub fn realize(profile: ReasonerId, input: &ReasonerInput) -> Result<RealizationResult> {
115    let adapter = adapter_for(profile)?;
116    adapter.realize(input)
117}
118
119pub fn check_instance(
120    profile: ReasonerId,
121    input: &ReasonerInput,
122    individual_iri: &str,
123    class_iri: &str,
124) -> Result<InstanceCheckResult> {
125    let adapter = adapter_for(profile)?;
126    adapter.check_instance(input, individual_iri, class_iri)
127}
128
129pub fn inferred_assertions(
130    profile: ReasonerId,
131    input: &ReasonerInput,
132) -> Result<InferredAssertions> {
133    let adapter = adapter_for(profile)?;
134    adapter.inferred_assertions(input)
135}
136
137pub fn explain(
138    profile: ReasonerId,
139    input: &ReasonerInput,
140    request: &ExplanationRequest,
141) -> Result<ExplanationResult> {
142    let adapter = adapter_for(profile)?;
143    adapter.explain(input, request)
144}
145
146pub fn explain_alternatives(
147    profile: ReasonerId,
148    input: &ReasonerInput,
149    request: &ExplanationRequest,
150    max_justifications: usize,
151) -> Result<Vec<ExplanationResult>> {
152    crate::explain::explain_unsatisfiable_alternatives(
153        profile,
154        &input.ontology,
155        &request.class_iri,
156        max_justifications,
157    )
158}
159
160fn profile_warnings(
161    ontology: &ontologos_core::Ontology,
162    profile: ReasonerId,
163    auto_detect: bool,
164) -> Result<Vec<crate::result::ReasonerWarning>> {
165    if !auto_detect {
166        return Ok(Vec::new());
167    }
168    let report = ontologos_profile::detect_profile(ontology)
169        .map_err(|e| ReasonerError::Classify(e.to_string()))?;
170    let mut warnings = Vec::new();
171    for diag in report.diagnostics {
172        warnings.push(crate::result::ReasonerWarning {
173            code: "profile_construct".to_string(),
174            message: diag.message,
175            suggested_profile: report.detected.map(|p| format!("{p:?}").to_ascii_lowercase()),
176        });
177    }
178    if profile == ReasonerId::El {
179        let constructs = scan_constructs(ontology);
180        for diag in ontologos_profile::el_diagnostics(&constructs) {
181            warnings.push(crate::result::ReasonerWarning {
182                code: "el_construct".to_string(),
183                message: diag.message,
184                suggested_profile: Some("el".to_string()),
185            });
186        }
187    }
188    Ok(warnings)
189}
190
191fn suggest_profile(ontology: &ontologos_core::Ontology) -> Option<String> {
192    let report = ontologos_profile::detect_profile(ontology).ok()?;
193    report.detected.map(|p| match p {
194        ontologos_profile::OwlProfile::El => "el".to_string(),
195        ontologos_profile::OwlProfile::Rl => "rl".to_string(),
196        ontologos_profile::OwlProfile::Ql => "el".to_string(),
197        ontologos_profile::OwlProfile::Dl => "dl".to_string(),
198    })
199}