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.8 of the pre-stable search protocol.
90    #[serde(rename = "mant.search/v0.8")]
91    V0Dot8,
92}
93
94impl SearchSchema {
95    /// Serialized identifier of the current search contract.
96    pub const ID: &'static str = "mant.search/v0.8";
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.8"))]
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    pub line_ranges: Vec<SearchLineRange>,
214}
215
216/// One exact occurrence fragment within an anchor-free rendered Markdown line.
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
218#[serde(rename_all = "camelCase")]
219pub struct SearchLineRange {
220    /// One-based line number in the deterministic full Markdown render.
221    #[schemars(range(min = 1))]
222    pub line: u32,
223    /// Inclusive zero-based UTF-8 byte offset within the presented Markdown line.
224    pub start_byte: u32,
225    /// Exclusive zero-based UTF-8 byte offset within the presented Markdown line.
226    pub end_byte: u32,
227}
228
229/// Half-open byte range plus one-based human coordinates in full Markdown.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231#[serde(rename_all = "camelCase")]
232pub struct SearchMarkdownRange {
233    /// Inclusive zero-based UTF-8 byte offset.
234    pub start_byte: u64,
235    /// Exclusive zero-based UTF-8 byte offset.
236    pub end_byte: u64,
237    /// One-based starting line.
238    #[schemars(range(min = 1))]
239    pub start_line: u32,
240    /// One-based starting column.
241    #[schemars(range(min = 1))]
242    pub start_column: u32,
243    /// One-based ending line.
244    #[schemars(range(min = 1))]
245    pub end_line: u32,
246    /// One-based exclusive ending column.
247    #[schemars(range(min = 1))]
248    pub end_column: u32,
249}
250
251/// One rendered Markdown line surrounding a match.
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
253#[serde(rename_all = "camelCase")]
254pub struct SearchContextLine {
255    /// One-based line number in the deterministic Markdown render.
256    #[schemars(range(min = 1))]
257    pub line: u32,
258    /// Complete rendered line without its newline terminator.
259    pub text: String,
260    /// Whether this is one of the lines intersecting the match.
261    pub matched: bool,
262}