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::{DefinitionCase, 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/v6")]
73    V6,
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:v6"))]
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        case: DefinitionCase,
177        names: Vec<String>,
178    },
179}
180
181impl SearchNode {
182    #[must_use]
183    pub fn path(&self) -> &str {
184        match self {
185            Self::Tldr { path, .. }
186            | Self::DocumentRoot { path, .. }
187            | Self::DocumentSection { path, .. }
188            | Self::DocumentEntry { path, .. } => path,
189        }
190    }
191
192    #[must_use]
193    pub fn title(&self) -> &str {
194        match self {
195            Self::Tldr { title, .. }
196            | Self::DocumentRoot { title, .. }
197            | Self::DocumentSection { title, .. }
198            | Self::DocumentEntry { title, .. } => title,
199        }
200    }
201}
202
203/// Addressable containing section for a non-tldr match.
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
205#[serde(rename_all = "camelCase")]
206pub struct SearchSectionReference {
207    pub path: String,
208    pub id: String,
209    pub title: String,
210}
211
212/// Half-open byte range plus one-based human coordinates in full Markdown.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
214#[serde(rename_all = "camelCase")]
215pub struct SearchMarkdownRange {
216    pub start_byte: u64,
217    pub end_byte: u64,
218    pub start_line: u32,
219    pub start_column: u32,
220    pub end_line: u32,
221    pub end_column: u32,
222}
223
224/// One rendered Markdown line surrounding a match.
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
226#[serde(rename_all = "camelCase")]
227pub struct SearchContextLine {
228    pub line: u32,
229    pub text: String,
230    pub matched: bool,
231}