Skip to main content

mant_protocol/
query.rs

1//! Query envelope combining one structured input with optional tldr content.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use mant_ir::{ResolvedContent, TldrDocument};
7
8use crate::{
9    ContentSelector, DocumentAddress, DocumentResponse, EntryProjection,
10    MAX_DOCUMENT_SELECTOR_CHARS, MAX_MANUAL_SECTION_CHARS, MAX_SEMANTIC_ENTRY_CHARS,
11    MAX_SOURCE_SELECTOR_CHARS, SearchCase, SearchScope, SearchSyntax, default_search_limit,
12};
13
14/// Maximum outline selectors accepted by one focused read request.
15pub const MAX_NODE_SELECTORS: usize = 16;
16
17/// Exact schema marker for a complete `ManT` query result.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
19pub enum QuerySchema {
20    /// Query envelope built around `mant.document/v0.11`.
21    #[serde(rename = "mant.query/v0.11")]
22    V0Dot11,
23}
24
25impl QuerySchema {
26    /// Serialized identifier of the current query response contract.
27    pub const ID: &'static str = "mant.query/v0.11";
28}
29
30/// Exact schema marker for a native query request.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
32pub enum RequestSchema {
33    /// Query and projection request accepted through `--request-json`.
34    #[serde(rename = "mant.request/v0.11")]
35    V0Dot11,
36}
37
38impl RequestSchema {
39    /// Serialized identifier of the current request contract.
40    pub const ID: &'static str = "mant.request/v0.11";
41}
42
43/// Source selected by one public query request.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
45#[serde(
46    tag = "kind",
47    rename_all = "kebab-case",
48    rename_all_fields = "camelCase",
49    deny_unknown_fields
50)]
51pub enum QueryInput {
52    /// Resolve personal Markdown first, then configured sources around the
53    /// priority-zero native-manual baseline.
54    Document {
55        /// Hierarchical catalog path or unqualified component-suffix selector.
56        #[schemars(length(min = 1, max = MAX_DOCUMENT_SELECTOR_CHARS))]
57        selector: String,
58        /// Optional configured Markdown source. It bypasses root documents and manuals.
59        #[schemars(length(min = 1, max = MAX_SOURCE_SELECTOR_CHARS))]
60        #[serde(skip_serializing_if = "Option::is_none")]
61        source: Option<String>,
62        /// Optional native manual category such as `1` or `3p`.
63        #[schemars(length(min = 1, max = MAX_MANUAL_SECTION_CHARS))]
64        #[serde(skip_serializing_if = "Option::is_none")]
65        manual_section: Option<String>,
66    },
67    /// Read and parse one explicit local Markdown or roff file.
68    File {
69        /// Physical path supplied by the caller.
70        path: String,
71        /// Parser-selection policy for the file.
72        format: InputFormat,
73    },
74}
75
76/// Parser selected for an explicit physical input.
77#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78#[serde(rename_all = "kebab-case")]
79pub enum InputFormat {
80    /// Infer the parser from extension and content conventions.
81    #[default]
82    Auto,
83    /// Parse the input as Markdown.
84    Markdown,
85    /// Parse the input as roff with libmandoc.
86    Roff,
87}
88
89/// Projection requested after loading one complete structured document.
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
91#[serde(
92    tag = "kind",
93    rename_all = "kebab-case",
94    rename_all_fields = "camelCase",
95    deny_unknown_fields
96)]
97pub enum QueryView {
98    /// Return the complete structured query bundle.
99    Full {},
100    /// Return a navigable structural projection.
101    Outline {
102        /// Semantic entry material included beneath structural nodes.
103        #[serde(default)]
104        entries: EntryProjection,
105        /// Optional section or entry used as the outline root.
106        #[serde(skip_serializing_if = "Option::is_none")]
107        root: Option<ContentSelector>,
108        /// Independent bounded inventory of real inline link occurrences.
109        #[serde(default)]
110        references: crate::ReferenceProjection,
111    },
112    /// Return content selected by one or more explicit local node paths or IDs.
113    Excerpt {
114        /// Ordered selectors resolved by the engine.
115        #[schemars(length(min = 1, max = MAX_NODE_SELECTORS))]
116        selectors: Vec<ContentSelector>,
117    },
118    /// Collect independent semantic and bounded literal evidence.
119    Explain {
120        /// Documented name, full authored form, exact entry ID/path, or literal support.
121        #[schemars(length(min = 1, max = MAX_SEMANTIC_ENTRY_CHARS))]
122        entry: String,
123        /// Semantic result and original-content budgets.
124        #[serde(default)]
125        options: crate::ExplanationOptions,
126    },
127    /// Search visible document content with bounded pagination.
128    Search {
129        /// Literal or regular-expression search pattern.
130        #[schemars(length(min = 1, max = 4096))]
131        pattern: String,
132        /// Pattern language.
133        #[serde(default)]
134        syntax: SearchSyntax,
135        /// Case-matching policy.
136        #[serde(default)]
137        case: SearchCase,
138        /// Semantic content included in the search.
139        #[serde(default)]
140        scope: SearchScope,
141        /// Require matches to be bounded by word boundaries.
142        #[serde(default)]
143        word: bool,
144        /// Neighboring rendered lines included around each match.
145        #[serde(default)]
146        #[schemars(range(max = 100))]
147        context_lines: u16,
148        /// Maximum number of matches returned.
149        #[serde(default = "default_search_limit")]
150        #[schemars(range(min = 1, max = 10000))]
151        limit: u32,
152        /// Number of matching results skipped before collection.
153        #[serde(default)]
154        offset: u32,
155    },
156}
157
158/// Native use-case input. The engine validates semantic constraints before I/O.
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
160#[serde(rename_all = "camelCase", deny_unknown_fields)]
161#[schemars(extend("$id" = "urn:mant:request:v0.11"))]
162pub struct QueryRequest {
163    /// Exact request schema discriminator.
164    pub schema: RequestSchema,
165    /// Document source to resolve.
166    pub input: QueryInput,
167    /// Projection applied after the document is loaded.
168    pub view: QueryView,
169}
170
171/// Versioned full-query result emitted at CLI and request JSON boundaries.
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
173#[serde(rename_all = "camelCase", deny_unknown_fields)]
174#[schemars(extend("$id" = "urn:mant:query:v0.11"))]
175pub struct QueryBundle {
176    /// Exact response schema discriminator.
177    pub schema: QuerySchema,
178    /// Human-readable selected-document label.
179    pub label: String,
180    /// Exact registered address selected for this query. Direct input paths
181    /// and standard input do not belong to the registered catalog.
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub address: Option<DocumentAddress>,
184    /// Authoritative structured document, when found.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub document: Option<DocumentResponse>,
187    /// Optional quick-reference page resolved alongside the document.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub tldr: Option<TldrDocument>,
190}
191
192impl From<&ResolvedContent> for QueryBundle {
193    fn from(content: &ResolvedContent) -> Self {
194        Self {
195            schema: QuerySchema::V0Dot11,
196            label: content.label.clone(),
197            address: content.address.clone(),
198            document: content.document.as_ref().map(Into::into),
199            tldr: content.tldr.clone(),
200        }
201    }
202}
203
204impl From<QueryBundle> for ResolvedContent {
205    fn from(bundle: QueryBundle) -> Self {
206        Self {
207            label: bundle.label,
208            address: bundle.address,
209            document: bundle.document.map(Into::into),
210            tldr: bundle.tldr,
211        }
212    }
213}