1mod collect;
3mod literal;
4mod location;
5pub use location::resolve_explanation_block;
6mod details;
7mod matches;
8mod materialize;
9mod page;
10mod plan;
11mod positions;
12mod preview;
13mod relations;
14mod scoped;
15mod support;
16pub(crate) use scoped::explain as explain_scope;
17
18use crate::selectors::{LocatedNode, collect_root_entries, collect_sections};
19use mant_ir::{Block, EntryOwner, NameCase, OutlinePath, ResolvedContent, SourceSpan};
20use mant_protocol::{EvidenceBasis, ExplanationQuery, QueryExplanation};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum ExplanationError {
25 Entry(mant_protocol::ScopeTextError),
27 ResultLimit,
29 ContentLimit,
31 MissingContent,
33}
34impl std::fmt::Display for ExplanationError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 Self::Entry(mant_protocol::ScopeTextError::Empty) => {
38 f.write_str("explanation entry must not be empty")
39 }
40 Self::Entry(mant_protocol::ScopeTextError::ControlCharacter) => {
41 f.write_str("explanation entry must not contain control characters")
42 }
43 Self::Entry(mant_protocol::ScopeTextError::TooLong { maximum }) => write!(
44 f,
45 "explanation entry must not exceed {maximum} Unicode scalar values"
46 ),
47 Self::ResultLimit => f.write_str("explanation limit must be between 1 and 256"),
48 Self::ContentLimit => {
49 f.write_str("explanation content budget must be between 1 and 4194304 bytes")
50 }
51 Self::MissingContent => f.write_str("explanation requires readable content"),
52 }
53 }
54}
55impl std::error::Error for ExplanationError {}
56
57pub fn validate_explanation_query(query: &ExplanationQuery) -> Result<(), ExplanationError> {
62 mant_protocol::validate_scope_text(&query.entry, mant_protocol::MAX_SEMANTIC_ENTRY_CHARS)
63 .map_err(ExplanationError::Entry)?;
64 if !(1..=mant_protocol::MAX_EXPLANATION_RESULTS).contains(&query.options.limit) {
65 return Err(ExplanationError::ResultLimit);
66 }
67 if !(1..=mant_protocol::MAX_EXPLANATION_CONTENT_BYTES).contains(&query.options.content_bytes) {
68 return Err(ExplanationError::ContentLimit);
69 }
70 Ok(())
71}
72
73pub fn explain_query(
83 content: &ResolvedContent,
84 query: &ExplanationQuery,
85) -> Result<QueryExplanation, ExplanationError> {
86 explain_with_usage(content, query).map(|(response, _)| response)
87}
88
89pub(crate) fn explain_with_usage(
90 content: &ResolvedContent,
91 query: &ExplanationQuery,
92) -> Result<(QueryExplanation, u32), ExplanationError> {
93 validate_explanation_query(query)?;
94 let plan = collection_plan(content, query.entry.trim())?;
95 Ok(materialize::response(plan, query))
96}
97
98fn collection_plan<'a>(
99 content: &'a ResolvedContent,
100 entry: &str,
101) -> Result<plan::CollectionPlan<'a>, ExplanationError> {
102 if content.document.is_none() && content.tldr.is_none() {
103 return Err(ExplanationError::MissingContent);
104 }
105 let mut located = Vec::new();
106 if let Some(document) = &content.document {
107 collect_root_entries(&document.blocks, &mut located);
108 collect_sections(&document.sections, &[], &[], &mut located);
109 }
110 let validation = content
111 .document
112 .as_ref()
113 .map(mant_ir::DocumentValidation::new);
114 let (mut candidates, orders, supports) =
115 collect::collect(content, entry, &located, validation.as_ref());
116 let relations = relations::expand(
117 validation.as_ref(),
118 entry,
119 &located,
120 &orders,
121 &mut candidates,
122 );
123 let (candidates, truncated) = candidates.finish();
124 let mut diagnostics = content
125 .document
126 .as_ref()
127 .map(|d| d.diagnostics.clone())
128 .unwrap_or_default();
129 let rejected_aliases = validation
130 .as_ref()
131 .into_iter()
132 .flat_map(mant_ir::DocumentValidation::relation_issues)
133 .filter(|issue| {
134 matches!(
135 issue.kind,
136 mant_ir::EntryRelationIssueKind::AliasOf | mant_ir::EntryRelationIssueKind::Cycle
137 )
138 })
139 .map(|issue| issue.owner.clone())
140 .collect();
141 if let Some(validation) = validation {
142 for diagnostic in validation.into_diagnostics() {
143 if !diagnostics.contains(&diagnostic) {
144 diagnostics.push(diagnostic);
145 }
146 }
147 }
148 Ok(plan::CollectionPlan {
149 content,
150 located,
151 candidates,
152 supports,
153 diagnostics,
154 rejected_aliases,
155 truncation: mant_protocol::ExplanationTruncation {
156 candidates: truncated,
157 relations,
158 content: false,
159 },
160 })
161}
162
163pub fn select_explanation(
170 content: &ResolvedContent,
171 entry: &str,
172) -> Result<QueryExplanation, ExplanationError> {
173 explain_query(
174 content,
175 &ExplanationQuery {
176 entry: entry.to_owned(),
177 options: mant_protocol::ExplanationOptions::default(),
178 },
179 )
180}
181
182struct Candidate<'a> {
183 order: usize,
184 located: Option<usize>,
185 ordinary: Option<&'a Block>,
186 section: Option<usize>,
187 block_path: Option<String>,
188 source: Option<SourceSpan>,
189 bases: Vec<EvidenceBasis>,
190 matched: matches::MatchPlan,
191 hits: Vec<preview::LiteralHit<'a>>,
192}
193
194fn same(left: &str, right: &str, case: NameCase) -> bool {
195 match case {
196 NameCase::Sensitive => left == right,
197 NameCase::Insensitive => left.eq_ignore_ascii_case(right),
198 }
199}
200
201fn owner<'a>(node: &LocatedNode<'a>) -> Option<EntryOwner<'a>> {
202 match node {
203 LocatedNode::Entry { entry, .. } => Some(entry.owner()),
204 LocatedNode::Section { .. } => None,
205 }
206}
207
208fn is_identity(node: &LocatedNode<'_>, entry: &str) -> bool {
209 node.id() == entry
210 || entry
211 .parse::<OutlinePath>()
212 .is_ok_and(|path| &path == node.path())
213}