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