Skip to main content

mant_ast/
search.rs

1//! Stable request and response contracts for structure-aware document search.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::{DefinitionRole, DocumentMeta, DocumentSource, SourceSpan};
7
8pub const DEFAULT_SEARCH_LIMIT: u32 = 100;
9
10/// Pattern language used for one search.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
12#[serde(rename_all = "kebab-case")]
13pub enum SearchSyntax {
14    #[default]
15    Literal,
16    Regex,
17}
18
19/// Case-folding policy applied when compiling the matcher.
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
21#[serde(rename_all = "kebab-case")]
22pub enum SearchCase {
23    #[default]
24    Insensitive,
25    Sensitive,
26    Smart,
27}
28
29/// Text representation searched while Markdown remains the coordinate basis.
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
31#[serde(rename_all = "kebab-case")]
32pub enum SearchScope {
33    /// Search the text visible after parsing `ManT`'s generated `CommonMark`.
34    #[default]
35    Visible,
36    /// Search the generated `CommonMark` bytes, including markup.
37    Markdown,
38}
39
40/// Normalized search configuration echoed in a search response.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42#[serde(rename_all = "camelCase", deny_unknown_fields)]
43pub struct SearchQuery {
44    #[schemars(length(min = 1, max = 4096))]
45    pub pattern: String,
46    #[serde(default)]
47    pub syntax: SearchSyntax,
48    #[serde(default)]
49    pub case: SearchCase,
50    #[serde(default)]
51    pub scope: SearchScope,
52    #[serde(default)]
53    pub word: bool,
54    #[serde(default)]
55    #[schemars(range(max = 100))]
56    pub context_lines: u16,
57    #[serde(default = "default_search_limit")]
58    #[schemars(range(min = 1, max = 10000))]
59    pub limit: u32,
60    #[serde(default)]
61    pub offset: u32,
62}
63
64#[must_use]
65pub const fn default_search_limit() -> u32 {
66    DEFAULT_SEARCH_LIMIT
67}
68
69/// Exact schema marker for structure-aware search results.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
71pub enum SearchSchema {
72    #[serde(rename = "mant.search/v4")]
73    V4,
74}
75
76/// Markdown contract used as the coordinate space for every search format.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78pub enum MarkdownSchema {
79    #[serde(rename = "mant.markdown/v1")]
80    V1,
81}
82
83/// Canonical render format used for search coordinates.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85#[serde(rename_all = "kebab-case")]
86pub enum SearchRenderFormat {
87    Markdown,
88}
89
90/// Amount of the query included in the coordinate-bearing render.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
92#[serde(rename_all = "kebab-case")]
93pub enum SearchRenderScope {
94    Full,
95}
96
97/// Description of the deterministic document whose Markdown coordinates are reported.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
99#[serde(rename_all = "camelCase")]
100pub struct SearchRender {
101    pub schema: MarkdownSchema,
102    pub format: SearchRenderFormat,
103    pub scope: SearchRenderScope,
104    pub line_base: u8,
105    pub column_base: u8,
106    pub line_count: u32,
107}
108
109/// Complete, paginatable search result returned to agents and scripts.
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
111#[serde(rename_all = "camelCase")]
112#[schemars(extend("$id" = "urn:mant:search:v4"))]
113pub struct QuerySearch {
114    pub schema: SearchSchema,
115    pub label: String,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub source: Option<DocumentSource>,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub meta: Option<DocumentMeta>,
120    pub query: SearchQuery,
121    pub render: SearchRender,
122    pub total: u32,
123    pub returned: u32,
124    pub offset: u32,
125    pub truncated: bool,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub next_offset: Option<u32>,
128    pub matches: Vec<SearchMatch>,
129}
130
131/// One exact occurrence and both of its structural and rendered locations.
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
133#[serde(rename_all = "camelCase")]
134pub struct SearchMatch {
135    pub ordinal: u32,
136    pub node: SearchNode,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub section: Option<SearchSectionReference>,
139    pub matched_text: String,
140    pub markdown: SearchMarkdownRange,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub source: Option<SourceSpan>,
143    pub preview: String,
144    #[serde(default, skip_serializing_if = "Vec::is_empty")]
145    pub context: Vec<SearchContextLine>,
146}
147
148/// Nearest node accepted by `mant --node` for a matching occurrence.
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
150#[serde(
151    tag = "kind",
152    rename_all = "kebab-case",
153    rename_all_fields = "camelCase"
154)]
155pub enum SearchNode {
156    Tldr {
157        path: String,
158        id: String,
159        title: String,
160    },
161    DocumentRoot {
162        path: String,
163        id: String,
164        title: String,
165    },
166    DocumentSection {
167        path: String,
168        id: String,
169        title: String,
170    },
171    DocumentEntry {
172        path: String,
173        id: String,
174        title: String,
175        role: DefinitionRole,
176        names: Vec<String>,
177    },
178}
179
180impl SearchNode {
181    #[must_use]
182    pub fn path(&self) -> &str {
183        match self {
184            Self::Tldr { path, .. }
185            | Self::DocumentRoot { path, .. }
186            | Self::DocumentSection { path, .. }
187            | Self::DocumentEntry { path, .. } => path,
188        }
189    }
190
191    #[must_use]
192    pub fn title(&self) -> &str {
193        match self {
194            Self::Tldr { title, .. }
195            | Self::DocumentRoot { title, .. }
196            | Self::DocumentSection { title, .. }
197            | Self::DocumentEntry { title, .. } => title,
198        }
199    }
200}
201
202/// Addressable containing section for a non-tldr match.
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "camelCase")]
205pub struct SearchSectionReference {
206    pub path: String,
207    pub id: String,
208    pub title: String,
209}
210
211/// Half-open byte range plus one-based human coordinates in full Markdown.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
213#[serde(rename_all = "camelCase")]
214pub struct SearchMarkdownRange {
215    pub start_byte: u64,
216    pub end_byte: u64,
217    pub start_line: u32,
218    pub start_column: u32,
219    pub end_line: u32,
220    pub end_column: u32,
221}
222
223/// One rendered Markdown line surrounding a match.
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
225#[serde(rename_all = "camelCase")]
226pub struct SearchContextLine {
227    pub line: u32,
228    pub text: String,
229    pub matched: bool,
230}