Skip to main content

mant_protocol/
search.rs

1//! Stable request and response contracts for structure-aware document search.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use mant_ir::{DefinitionCase, DefinitionRole, DocumentMeta, DocumentSource, NodeId, SourceSpan};
7
8use crate::NodePath;
9
10/// Default maximum number of search matches returned in one page.
11pub const DEFAULT_SEARCH_LIMIT: u32 = 100;
12
13/// Pattern language used for one search.
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
15#[serde(rename_all = "kebab-case")]
16pub enum SearchSyntax {
17    /// Match the pattern as ordinary text.
18    #[default]
19    Literal,
20    /// Interpret the pattern as a Rust regular expression.
21    Regex,
22}
23
24/// Case-folding policy applied when compiling the matcher.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
26#[serde(rename_all = "kebab-case")]
27pub enum SearchCase {
28    /// Ignore case distinctions.
29    #[default]
30    Insensitive,
31    /// Preserve case distinctions.
32    Sensitive,
33    /// Match case-sensitively only when the pattern contains uppercase text.
34    Smart,
35}
36
37/// Text representation searched while Markdown remains the coordinate basis.
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
39#[serde(rename_all = "kebab-case")]
40pub enum SearchScope {
41    /// Search the text visible after parsing `ManT`'s generated `CommonMark`.
42    #[default]
43    Visible,
44    /// Search the generated `CommonMark` bytes, including markup.
45    Markdown,
46}
47
48/// Normalized search configuration echoed in a search response.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51pub struct SearchQuery {
52    /// Literal or regular-expression search pattern.
53    #[schemars(length(min = 1, max = 4096))]
54    pub pattern: String,
55    /// Pattern language.
56    #[serde(default)]
57    pub syntax: SearchSyntax,
58    /// Case-matching policy.
59    #[serde(default)]
60    pub case: SearchCase,
61    /// Text representation searched.
62    #[serde(default)]
63    pub scope: SearchScope,
64    /// Require matches to be bounded by word boundaries.
65    #[serde(default)]
66    pub word: bool,
67    /// Neighboring rendered lines included around each match.
68    #[serde(default)]
69    #[schemars(range(max = 100))]
70    pub context_lines: u16,
71    /// Maximum number of matches returned.
72    #[serde(default = "default_search_limit")]
73    #[schemars(range(min = 1, max = 10000))]
74    pub limit: u32,
75    /// Number of matching results skipped before collection.
76    #[serde(default)]
77    pub offset: u32,
78}
79
80#[must_use]
81/// Return [`DEFAULT_SEARCH_LIMIT`].
82pub const fn default_search_limit() -> u32 {
83    DEFAULT_SEARCH_LIMIT
84}
85
86/// Exact schema marker for structure-aware search results.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
88pub enum SearchSchema {
89    /// Version 7 of the search protocol.
90    #[serde(rename = "mant.search/v7")]
91    V7,
92}
93
94/// Markdown contract used as the coordinate space for every search format.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
96pub enum MarkdownSchema {
97    /// Version 1 of `ManT`'s deterministic Markdown rendering contract.
98    #[serde(rename = "mant.markdown/v1")]
99    V1,
100}
101
102/// Canonical render format used for search coordinates.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
104#[serde(rename_all = "kebab-case")]
105pub enum SearchRenderFormat {
106    /// Generated `CommonMark` text.
107    Markdown,
108}
109
110/// Amount of the query included in the coordinate-bearing render.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
112#[serde(rename_all = "kebab-case")]
113pub enum SearchRenderScope {
114    /// Complete query document, including optional tldr content.
115    Full,
116}
117
118/// Description of the deterministic document whose Markdown coordinates are reported.
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
120#[serde(rename_all = "camelCase")]
121pub struct SearchRender {
122    /// Coordinate-space schema discriminator.
123    pub schema: MarkdownSchema,
124    /// Rendered text format.
125    pub format: SearchRenderFormat,
126    /// Portion of the query represented by the render.
127    pub scope: SearchRenderScope,
128    /// First valid human-readable line number.
129    pub line_base: u8,
130    /// First valid human-readable column number.
131    pub column_base: u8,
132    /// Total rendered line count.
133    pub line_count: u32,
134}
135
136/// Complete, paginatable search result returned to agents and scripts.
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
138#[serde(rename_all = "camelCase")]
139#[schemars(extend("$id" = "urn:mant:search:v7"))]
140pub struct QuerySearch {
141    /// Exact response schema discriminator.
142    pub schema: SearchSchema,
143    /// Human-readable selected-document label.
144    pub label: String,
145    /// Authoritative document source, when one was loaded.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub source: Option<DocumentSource>,
148    /// Document metadata, when one was loaded.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub meta: Option<DocumentMeta>,
151    /// Normalized query applied by the engine.
152    pub query: SearchQuery,
153    /// Coordinate-space description shared by all matches.
154    pub render: SearchRender,
155    /// Total matches before pagination.
156    pub total: u32,
157    /// Number of matches present in [`Self::matches`].
158    pub returned: u32,
159    /// Applied zero-based match offset.
160    pub offset: u32,
161    /// Whether additional matches remain.
162    pub truncated: bool,
163    /// Offset for the next page, when one exists.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub next_offset: Option<u32>,
166    /// Matching occurrences in render order.
167    pub matches: Vec<SearchMatch>,
168}
169
170/// One exact occurrence and both of its structural and rendered locations.
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
172#[serde(rename_all = "camelCase")]
173pub struct SearchMatch {
174    /// One-based occurrence number in the unpaginated result set.
175    pub ordinal: u32,
176    /// Nearest structurally addressable node.
177    pub node: SearchNode,
178    /// Containing document section, when applicable.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub section: Option<SearchSectionReference>,
181    /// Exact text consumed by the matcher.
182    pub matched_text: String,
183    /// Location in the deterministic full Markdown render.
184    pub markdown: SearchMarkdownRange,
185    /// Original-source location, when the parser retained one.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub source: Option<SourceSpan>,
188    /// Compact single-string presentation of the match.
189    pub preview: String,
190    /// Optional rendered lines surrounding the match.
191    #[serde(default, skip_serializing_if = "Vec::is_empty")]
192    pub context: Vec<SearchContextLine>,
193}
194
195/// Nearest node accepted by `mant --node` for a matching occurrence.
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
197#[serde(
198    tag = "kind",
199    rename_all = "kebab-case",
200    rename_all_fields = "camelCase"
201)]
202pub enum SearchNode {
203    /// Match in optional quick-reference content.
204    Tldr {
205        /// Canonical structural outline path.
206        path: NodePath,
207        /// Stable document-local identity.
208        id: NodeId,
209        /// Display title.
210        title: String,
211    },
212    /// Match in content preceding the first heading.
213    DocumentRoot {
214        /// Canonical structural outline path.
215        path: NodePath,
216        /// Virtual document-root identity.
217        id: NodeId,
218        /// Display title.
219        title: String,
220    },
221    /// Match in an ordinary semantic section.
222    DocumentSection {
223        /// Canonical structural outline path.
224        path: NodePath,
225        /// Stable document-local section identity.
226        id: NodeId,
227        /// Section heading text.
228        title: String,
229    },
230    /// Match within a semantic definition.
231    DocumentEntry {
232        /// Canonical structural outline path.
233        path: NodePath,
234        /// Stable document-local entry identity.
235        id: NodeId,
236        /// Primary display term.
237        title: String,
238        /// Semantic category of the definition.
239        role: DefinitionRole,
240        /// Alias case-matching policy.
241        case: DefinitionCase,
242        /// Normalized selectable aliases.
243        names: Vec<String>,
244    },
245}
246
247impl SearchNode {
248    /// Return the canonical structural outline path.
249    #[must_use]
250    pub fn path(&self) -> &str {
251        match self {
252            Self::Tldr { path, .. }
253            | Self::DocumentRoot { path, .. }
254            | Self::DocumentSection { path, .. }
255            | Self::DocumentEntry { path, .. } => path,
256        }
257    }
258
259    /// Return the node's display title.
260    #[must_use]
261    pub fn title(&self) -> &str {
262        match self {
263            Self::Tldr { title, .. }
264            | Self::DocumentRoot { title, .. }
265            | Self::DocumentSection { title, .. }
266            | Self::DocumentEntry { title, .. } => title,
267        }
268    }
269}
270
271/// Addressable containing section for a non-tldr match.
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
273#[serde(rename_all = "camelCase")]
274pub struct SearchSectionReference {
275    /// Canonical structural outline path.
276    pub path: NodePath,
277    /// Stable document-local section identity.
278    pub id: NodeId,
279    /// Section heading text.
280    pub title: String,
281}
282
283/// Half-open byte range plus one-based human coordinates in full Markdown.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
285#[serde(rename_all = "camelCase")]
286pub struct SearchMarkdownRange {
287    /// Inclusive zero-based UTF-8 byte offset.
288    pub start_byte: u64,
289    /// Exclusive zero-based UTF-8 byte offset.
290    pub end_byte: u64,
291    /// One-based starting line.
292    pub start_line: u32,
293    /// One-based starting column.
294    pub start_column: u32,
295    /// One-based ending line.
296    pub end_line: u32,
297    /// One-based exclusive ending column.
298    pub end_column: u32,
299}
300
301/// One rendered Markdown line surrounding a match.
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
303#[serde(rename_all = "camelCase")]
304pub struct SearchContextLine {
305    /// One-based line number in the deterministic Markdown render.
306    pub line: u32,
307    /// Complete rendered line without its newline terminator.
308    pub text: String,
309    /// Whether this is one of the lines intersecting the match.
310    pub matched: bool,
311}