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::{DocumentMeta, DocumentSource, SourceSpan};
7
8use crate::OutlineTrail;
9
10/// Default maximum number of matching line groups 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 matching line groups returned.
72    #[serde(default = "default_search_limit")]
73    #[schemars(range(min = 1, max = 10000))]
74    pub limit: u32,
75    /// Number of matching line groups 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 0.10 of the pre-stable search protocol.
90    #[serde(rename = "mant.search/v0.10")]
91    V0Dot10,
92}
93
94impl SearchSchema {
95    /// Serialized identifier of the current search contract.
96    pub const ID: &'static str = "mant.search/v0.10";
97}
98
99/// Markdown contract used as the coordinate space for every search format.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
101pub enum MarkdownSchema {
102    /// Version 1 of `ManT`'s deterministic Markdown rendering contract.
103    #[serde(rename = "mant.markdown/v1")]
104    V1,
105}
106
107/// Canonical render format used for search coordinates.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
109#[serde(rename_all = "kebab-case")]
110pub enum SearchRenderFormat {
111    /// Generated `CommonMark` text.
112    Markdown,
113}
114
115/// Amount of the query included in the coordinate-bearing render.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
117#[serde(rename_all = "kebab-case")]
118pub enum SearchRenderScope {
119    /// Complete query document, including optional tldr content.
120    Full,
121}
122
123/// Description of the deterministic document whose Markdown coordinates are reported.
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
125#[serde(rename_all = "camelCase")]
126pub struct SearchRender {
127    /// Coordinate-space schema discriminator.
128    pub schema: MarkdownSchema,
129    /// Rendered text format.
130    pub format: SearchRenderFormat,
131    /// Portion of the query represented by the render.
132    pub scope: SearchRenderScope,
133    /// First valid human-readable line number.
134    #[schemars(range(min = 1, max = 1))]
135    pub line_base: u8,
136    /// First valid human-readable column number.
137    #[schemars(range(min = 1, max = 1))]
138    pub column_base: u8,
139    /// Total rendered line count.
140    pub line_count: u32,
141}
142
143/// Complete, paginatable search result returned to agents and scripts.
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
145#[serde(rename_all = "camelCase")]
146#[schemars(extend("$id" = "urn:mant:search:v0.10"))]
147pub struct QuerySearch {
148    /// Exact response schema discriminator.
149    pub schema: SearchSchema,
150    /// Human-readable selected-document label.
151    pub label: String,
152    /// Authoritative document source, when one was loaded.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub source: Option<DocumentSource>,
155    /// Document metadata, when one was loaded.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub meta: Option<DocumentMeta>,
158    /// Normalized query applied by the engine.
159    pub query: SearchQuery,
160    /// Coordinate-space description shared by all matching line groups.
161    pub render: SearchRender,
162    /// Total matching line groups before pagination.
163    pub total: u32,
164    /// Number of matching line groups present in [`Self::matches`].
165    pub returned: u32,
166    /// Applied zero-based matching-line offset.
167    pub offset: u32,
168    /// Whether additional matching line groups remain.
169    pub truncated: bool,
170    /// Offset for the next page, when one exists.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub next_offset: Option<u32>,
173    /// Matching line groups in render order.
174    pub matches: Vec<SearchHit>,
175}
176
177/// One rendered line or line span containing one or more exact occurrences.
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
179#[serde(rename_all = "camelCase")]
180pub struct SearchHit {
181    /// One-based line-group number in the unpaginated result set.
182    #[schemars(range(min = 1))]
183    pub ordinal: u32,
184    /// Complete logical location of the nearest addressable node.
185    pub outline: OutlineTrail,
186    /// Exact matcher occurrences on this rendered line or line span.
187    #[schemars(length(min = 1, max = 256))]
188    pub occurrences: Vec<SearchOccurrence>,
189    /// Total exact matcher occurrences represented by this line group.
190    #[schemars(range(min = 1))]
191    pub occurrence_count: u32,
192    /// Whether [`Self::occurrences`] omits exact ranges to remain bounded.
193    pub occurrences_truncated: bool,
194    /// Original-source location of the owning outline node, when retained.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub node_source: Option<SourceSpan>,
197    /// Compact single-string presentation of the match.
198    pub preview: String,
199    /// Optional rendered lines surrounding the match.
200    #[serde(default, skip_serializing_if = "Vec::is_empty")]
201    pub context: Vec<SearchContextLine>,
202}
203
204/// One exact matcher occurrence in the canonical Markdown render.
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
206#[serde(rename_all = "camelCase")]
207pub struct SearchOccurrence {
208    /// Exact text consumed by the matcher.
209    pub matched_text: String,
210    /// Location in the deterministic full Markdown render.
211    pub markdown: SearchMarkdownRange,
212    /// Exact ranges within the anchor-free Markdown lines used for presentation.
213    #[schemars(length(min = 1))]
214    pub line_ranges: Vec<SearchLineRange>,
215}
216
217/// One exact occurrence fragment within an anchor-free rendered Markdown line.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
219#[serde(rename_all = "camelCase")]
220pub struct SearchLineRange {
221    /// One-based line number in the deterministic full Markdown render.
222    #[schemars(range(min = 1))]
223    pub line: u32,
224    /// Inclusive zero-based UTF-8 byte offset within the presented Markdown line.
225    pub start_byte: u32,
226    /// Exclusive zero-based UTF-8 byte offset within the presented Markdown line.
227    pub end_byte: u32,
228}
229
230/// Half-open byte range plus one-based human coordinates in full Markdown.
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
232#[serde(rename_all = "camelCase")]
233pub struct SearchMarkdownRange {
234    /// Inclusive zero-based UTF-8 byte offset.
235    pub start_byte: u64,
236    /// Exclusive zero-based UTF-8 byte offset.
237    pub end_byte: u64,
238    /// One-based starting line.
239    #[schemars(range(min = 1))]
240    pub start_line: u32,
241    /// One-based starting column.
242    #[schemars(range(min = 1))]
243    pub start_column: u32,
244    /// One-based ending line.
245    #[schemars(range(min = 1))]
246    pub end_line: u32,
247    /// One-based exclusive ending column.
248    #[schemars(range(min = 1))]
249    pub end_column: u32,
250}
251
252/// One rendered Markdown line surrounding a match.
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
254#[serde(rename_all = "camelCase")]
255pub struct SearchContextLine {
256    /// One-based line number in the deterministic Markdown render.
257    #[schemars(range(min = 1))]
258    pub line: u32,
259    /// Complete rendered line without its newline terminator.
260    pub text: String,
261    /// Whether this is one of the lines intersecting the match.
262    pub matched: bool,
263}