Skip to main content

mant_protocol/
outline.rs

1//! Stable contracts for lightweight query outlines and selected excerpts.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Deserializer, Serialize};
5
6use mant_ir::{
7    Block, DefinitionCase, DefinitionItem, DefinitionRole, Diagnostic, DocumentMeta,
8    DocumentSource, EntryKind, EntrySummary, NodeId, Section, TldrDocument, ValueDomain,
9};
10
11use crate::{NodePath, NodeSelector, Producer};
12
13/// Exact schema marker for a query outline response.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
15pub enum OutlineSchema {
16    /// Version 0.10 of the pre-stable outline protocol.
17    #[serde(rename = "mant.outline/v0.10")]
18    V0Dot10,
19}
20
21impl OutlineSchema {
22    /// Serialized identifier of the current outline contract.
23    pub const ID: &'static str = "mant.outline/v0.10";
24}
25
26/// Semantic entry material included beneath structural outline nodes.
27#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, JsonSchema)]
28#[serde(
29    tag = "kind",
30    rename_all = "kebab-case",
31    rename_all_fields = "camelCase",
32    deny_unknown_fields
33)]
34pub enum EntryProjection {
35    /// Include section topology without entry metadata.
36    None,
37    /// Include recursive entry counts but not individual entry nodes.
38    #[default]
39    Summary,
40    /// Include every nested semantic entry.
41    All,
42    /// Include entries of the selected kinds and the ancestors needed to reach them.
43    Kinds {
44        /// Semantic categories retained by the projection.
45        #[schemars(length(min = 1, max = 9))]
46        kinds: Vec<EntryKind>,
47    },
48}
49
50#[derive(Deserialize)]
51#[serde(
52    tag = "kind",
53    rename_all = "kebab-case",
54    rename_all_fields = "camelCase",
55    deny_unknown_fields
56)]
57enum ClosedEntryProjection {
58    None {},
59    Summary {},
60    All {},
61    Kinds { kinds: Vec<EntryKind> },
62}
63
64impl<'de> Deserialize<'de> for EntryProjection {
65    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66    where
67        D: Deserializer<'de>,
68    {
69        Ok(match ClosedEntryProjection::deserialize(deserializer)? {
70            ClosedEntryProjection::None {} => Self::None,
71            ClosedEntryProjection::Summary {} => Self::Summary,
72            ClosedEntryProjection::All {} => Self::All,
73            ClosedEntryProjection::Kinds { kinds } => Self::Kinds { kinds },
74        })
75    }
76}
77
78/// Compatibility selector for in-process callers migrating from v0.9.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum OutlineDetail {
81    /// Include only section-level navigation nodes.
82    Sections,
83    /// Include sections and every semantic definition entry.
84    Entries,
85}
86
87impl From<OutlineDetail> for EntryProjection {
88    fn from(value: OutlineDetail) -> Self {
89        match value {
90            OutlineDetail::Sections => Self::None,
91            OutlineDetail::Entries => Self::All,
92        }
93    }
94}
95
96/// A block-free tree used to discover selectable query content.
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
98#[serde(rename_all = "camelCase")]
99#[schemars(extend("$id" = "urn:mant:outline:v0.10"))]
100pub struct QueryOutline {
101    /// Exact response schema discriminator.
102    pub schema: OutlineSchema,
103    /// Entry projection used to build this outline.
104    pub entries: EntryProjection,
105    /// Optional section or entry selector used as the projection root.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub root: Option<NodeSelector>,
108    /// Human-readable selected-document label.
109    pub label: String,
110    /// Authoritative document source, when one was loaded.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub source: Option<DocumentSource>,
113    /// Document metadata, when an authoritative document was loaded.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub meta: Option<DocumentMeta>,
116    /// Recoverable parser findings available to diagnostic-oriented transports.
117    #[serde(default, skip_serializing_if = "Vec::is_empty")]
118    pub diagnostics: Vec<Diagnostic>,
119    /// False when semantic-entry declarations were rejected during lowering.
120    ///
121    /// The field is omitted for complete outlines so compact transports pay no
122    /// steady-state bandwidth cost.
123    #[serde(default = "default_true", skip_serializing_if = "is_true")]
124    pub entries_complete: bool,
125    /// Addressable nodes in document order.
126    pub nodes: Vec<OutlineNode>,
127}
128
129const fn default_true() -> bool {
130    true
131}
132
133// Serde's `skip_serializing_if` predicate receives a reference.
134#[allow(clippy::trivially_copy_pass_by_ref)]
135const fn is_true(value: &bool) -> bool {
136    *value
137}
138
139/// One uniquely addressable node in a query outline.
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
141#[serde(
142    tag = "kind",
143    rename_all = "kebab-case",
144    rename_all_fields = "camelCase"
145)]
146pub enum OutlineNode {
147    /// Optional quick-reference node.
148    Tldr {
149        /// Canonical structural outline path.
150        path: NodePath,
151        /// Stable document-local identity.
152        id: NodeId,
153        /// Display title.
154        title: String,
155    },
156    /// Addressable document content that precedes the first heading.
157    DocumentRoot {
158        /// Canonical structural outline path.
159        path: NodePath,
160        /// Virtual document-root identity.
161        id: NodeId,
162        /// Display title for the leading content.
163        title: String,
164        /// Recursive semantic entry coverage for this scope.
165        #[serde(skip_serializing_if = "Option::is_none")]
166        entry_summary: Option<EntrySummary>,
167        /// Nested semantic entries when explicitly expanded.
168        #[serde(default, skip_serializing_if = "Vec::is_empty")]
169        children: Vec<OutlineNode>,
170    },
171    /// One semantic document section.
172    DocumentSection {
173        /// Canonical structural outline path.
174        path: NodePath,
175        /// Stable document-local section identity.
176        id: NodeId,
177        /// Section heading text.
178        title: String,
179        /// Recursive semantic entry coverage owned directly by this section.
180        #[serde(skip_serializing_if = "Option::is_none")]
181        entry_summary: Option<EntrySummary>,
182        /// Nested section and entry nodes.
183        children: Vec<OutlineNode>,
184    },
185    /// One source-neutral semantic definition.
186    DocumentEntry {
187        /// Canonical structural outline path.
188        path: NodePath,
189        /// Stable document-local entry identity.
190        id: NodeId,
191        /// Primary display term.
192        title: String,
193        /// Semantic category of the entry.
194        entry_kind: EntryKind,
195        /// Alias case-matching policy.
196        case: DefinitionCase,
197        /// Exact selectable aliases.
198        aliases: Vec<String>,
199        /// Author-written input forms.
200        forms: Vec<String>,
201        /// Definition nodes supplying content for this concept.
202        targets: Vec<NodeId>,
203        /// Optional finite or cross-document value space.
204        #[serde(skip_serializing_if = "Option::is_none")]
205        value_domain: Option<ValueDomain>,
206        /// Recursive semantic entry coverage owned by this entry.
207        #[serde(skip_serializing_if = "Option::is_none")]
208        entry_summary: Option<EntrySummary>,
209        /// Nested entry nodes.
210        #[serde(default, skip_serializing_if = "Vec::is_empty")]
211        children: Vec<OutlineNode>,
212    },
213}
214
215impl OutlineNode {
216    /// Return the canonical structural path.
217    #[must_use]
218    pub fn path(&self) -> &str {
219        match self {
220            Self::Tldr { path, .. }
221            | Self::DocumentRoot { path, .. }
222            | Self::DocumentSection { path, .. }
223            | Self::DocumentEntry { path, .. } => path,
224        }
225    }
226
227    /// Return the stable document-local identity.
228    #[must_use]
229    pub fn id(&self) -> &str {
230        match self {
231            Self::Tldr { id, .. }
232            | Self::DocumentRoot { id, .. }
233            | Self::DocumentSection { id, .. }
234            | Self::DocumentEntry { id, .. } => id,
235        }
236    }
237
238    /// Return the node's display title.
239    #[must_use]
240    pub fn title(&self) -> &str {
241        match self {
242            Self::Tldr { title, .. }
243            | Self::DocumentRoot { title, .. }
244            | Self::DocumentSection { title, .. }
245            | Self::DocumentEntry { title, .. } => title,
246        }
247    }
248
249    /// Return child nodes, or an empty slice for leaf variants.
250    #[must_use]
251    pub fn children(&self) -> &[Self] {
252        match self {
253            Self::DocumentRoot { children, .. }
254            | Self::DocumentSection { children, .. }
255            | Self::DocumentEntry { children, .. } => children,
256            Self::Tldr { .. } => &[],
257        }
258    }
259}
260
261/// Exact schema marker for selected query content.
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
263pub enum ExcerptSchema {
264    /// Version 0.10 of the pre-stable excerpt protocol.
265    #[serde(rename = "mant.excerpt/v0.10")]
266    V0Dot10,
267}
268
269impl ExcerptSchema {
270    /// Serialized identifier of the current excerpt contract.
271    pub const ID: &'static str = "mant.excerpt/v0.10";
272}
273
274/// One or more independently selected nodes from a complete query.
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
276#[serde(rename_all = "camelCase")]
277#[schemars(extend("$id" = "urn:mant:excerpt:v0.10"))]
278pub struct QueryExcerpt {
279    /// Exact response schema discriminator.
280    pub schema: ExcerptSchema,
281    /// Human-readable selected-document label.
282    pub label: String,
283    /// Process and parser provenance, when a document was loaded.
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub producer: Option<Producer>,
286    /// Authoritative document source, when one was loaded.
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub source: Option<DocumentSource>,
289    /// Document metadata, when one was loaded.
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub meta: Option<DocumentMeta>,
292    /// Recoverable parser and validation findings.
293    #[serde(default, skip_serializing_if = "Vec::is_empty")]
294    pub diagnostics: Vec<Diagnostic>,
295    /// Selected nodes in canonical source order after duplicate selectors are removed.
296    pub selections: Vec<ExcerptSelection>,
297}
298
299/// One selected document node together with its location in the complete outline.
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
301#[serde(
302    tag = "kind",
303    rename_all = "kebab-case",
304    rename_all_fields = "camelCase"
305)]
306pub enum ExcerptSelection {
307    /// Optional quick-reference content preceding the primary document.
308    Tldr {
309        /// Complete logical location in the document outline.
310        outline: OutlineTrail,
311        /// Complete quick-reference content.
312        document: TldrDocument,
313    },
314    /// Complete document content that appears before the first heading.
315    DocumentRoot {
316        /// Complete logical location in the document outline.
317        outline: OutlineTrail,
318        /// Complete leading blocks.
319        blocks: Vec<Block>,
320    },
321    /// Complete selected document node, including all descendant sections.
322    DocumentSection {
323        /// Complete logical location in the document outline.
324        outline: OutlineTrail,
325        /// Complete selected section including descendants.
326        section: Section,
327    },
328    /// One addressable semantic definition and its complete description.
329    DocumentEntry {
330        /// Complete logical location in the document outline.
331        outline: OutlineTrail,
332        /// Complete semantic definition.
333        entry: DefinitionItem,
334    },
335}
336
337impl ExcerptSelection {
338    /// Return the complete logical location of this selection.
339    #[must_use]
340    pub const fn outline(&self) -> &OutlineTrail {
341        match self {
342            Self::Tldr { outline, .. }
343            | Self::DocumentRoot { outline, .. }
344            | Self::DocumentSection { outline, .. }
345            | Self::DocumentEntry { outline, .. } => outline,
346        }
347    }
348}
349
350/// Complete logical location of one addressable document node.
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
352#[serde(rename_all = "camelCase")]
353pub struct OutlineTrail {
354    /// Ordered ancestors from the document root to the direct parent.
355    #[serde(default, skip_serializing_if = "Vec::is_empty")]
356    pub ancestors: Vec<OutlineReference>,
357    /// Selected or matching node at the end of the trail.
358    pub node: OutlineNodeReference,
359}
360
361impl OutlineTrail {
362    /// Return the canonical structural path of the terminal node.
363    #[must_use]
364    pub fn path(&self) -> &str {
365        self.node.path()
366    }
367
368    /// Return the display title of the terminal node.
369    #[must_use]
370    pub fn title(&self) -> &str {
371        self.node.title()
372    }
373}
374
375/// Compact ancestor identity attached to an excerpt selection.
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
377#[serde(rename_all = "camelCase")]
378pub struct OutlineReference {
379    /// Canonical structural outline path.
380    pub path: NodePath,
381    /// Stable document-local identity.
382    pub id: NodeId,
383    /// Display title.
384    pub title: String,
385}
386
387/// Compact typed identity for the terminal node in an [`OutlineTrail`].
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
389#[serde(
390    tag = "kind",
391    rename_all = "kebab-case",
392    rename_all_fields = "camelCase"
393)]
394pub enum OutlineNodeReference {
395    /// Optional quick-reference node.
396    Tldr {
397        /// Canonical structural outline path.
398        path: NodePath,
399        /// Stable document-local identity.
400        id: NodeId,
401        /// Display title.
402        title: String,
403    },
404    /// Addressable content before the first heading.
405    DocumentRoot {
406        /// Canonical structural outline path.
407        path: NodePath,
408        /// Virtual document-root identity.
409        id: NodeId,
410        /// Display title.
411        title: String,
412    },
413    /// One semantic document section.
414    DocumentSection {
415        /// Canonical structural outline path.
416        path: NodePath,
417        /// Stable document-local identity.
418        id: NodeId,
419        /// Section heading text.
420        title: String,
421    },
422    /// One semantic command, option, or variable definition.
423    DocumentEntry {
424        /// Canonical structural outline path.
425        path: NodePath,
426        /// Stable document-local identity.
427        id: NodeId,
428        /// Primary display term.
429        title: String,
430        /// Semantic category of the definition.
431        role: DefinitionRole,
432        /// Alias case-matching policy.
433        case: DefinitionCase,
434        /// Normalized selectable aliases.
435        names: Vec<String>,
436    },
437}
438
439impl OutlineNodeReference {
440    /// Return the canonical structural path.
441    #[must_use]
442    pub fn path(&self) -> &str {
443        match self {
444            Self::Tldr { path, .. }
445            | Self::DocumentRoot { path, .. }
446            | Self::DocumentSection { path, .. }
447            | Self::DocumentEntry { path, .. } => path,
448        }
449    }
450
451    /// Return the stable document-local identity.
452    #[must_use]
453    pub fn id(&self) -> &str {
454        match self {
455            Self::Tldr { id, .. }
456            | Self::DocumentRoot { id, .. }
457            | Self::DocumentSection { id, .. }
458            | Self::DocumentEntry { id, .. } => id,
459        }
460    }
461
462    /// Return the display title.
463    #[must_use]
464    pub fn title(&self) -> &str {
465        match self {
466            Self::Tldr { title, .. }
467            | Self::DocumentRoot { title, .. }
468            | Self::DocumentSection { title, .. }
469            | Self::DocumentEntry { title, .. } => title,
470        }
471    }
472}