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, Diagnostic, DocumentAddress, DocumentMeta, DocumentReference, DocumentSource, EntryKind,
8    EntrySummary, NameCase, NodeId, Section, TldrDocument,
9};
10
11use crate::{ContentSelector, NodePath, 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.11 of the pre-stable outline protocol.
17    #[serde(rename = "mant.outline/v0.11")]
18    V0Dot11,
19}
20
21impl OutlineSchema {
22    /// Serialized identifier of the current outline contract.
23    pub const ID: &'static str = "mant.outline/v0.11";
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", deny_unknown_fields)]
99#[schemars(extend("$id" = "urn:mant:outline:v0.11"))]
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<ContentSelector>,
108    /// Real reference inventory, independent of entry visibility and grouping.
109    pub references: crate::ReferenceInventory,
110    /// Human-readable selected-document label.
111    pub label: String,
112    /// Plain display title derived from authoritative heading or native metadata.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub display_title: Option<String>,
115    /// Exact logical document address, absent for direct-file input.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub address: Option<DocumentAddress>,
118    /// Authoritative document source, when one was loaded.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub source: Option<DocumentSource>,
121    /// Document metadata, when an authoritative document was loaded.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub meta: Option<DocumentMeta>,
124    /// Recoverable parser findings available to diagnostic-oriented transports.
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    pub diagnostics: Vec<Diagnostic>,
127    /// False when lowering or shared IR validation found incomplete semantics.
128    ///
129    /// The field is omitted for complete outlines so compact transports pay no
130    /// steady-state bandwidth cost.
131    #[serde(default = "default_true", skip_serializing_if = "is_true")]
132    pub semantics_complete: bool,
133    /// Addressable nodes in document order.
134    pub nodes: Vec<OutlineNode>,
135}
136
137/// One exact cross-document destination declared by a semantic entry term.
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
139#[serde(rename_all = "camelCase", deny_unknown_fields)]
140pub struct EntryDocumentTarget {
141    /// Visible term text associated with this destination.
142    #[schemars(length(min = 1))]
143    pub label: String,
144    /// Source-authored logical reference.
145    pub reference: DocumentReference,
146    /// Exact logical destination resolved in the source document namespace.
147    ///
148    /// This is absent for direct-file inputs and references, such as an
149    /// unqualified manual name, that require catalog lookup.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub address: Option<DocumentAddress>,
152}
153
154/// Resolved value space accepted by one semantic entry.
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
156#[serde(
157    tag = "kind",
158    rename_all = "kebab-case",
159    rename_all_fields = "camelCase",
160    deny_unknown_fields
161)]
162pub enum EntryValueDomain {
163    /// Values represented by nested entry nodes.
164    Choices {
165        /// True when the listed choices are known to be exhaustive.
166        exhaustive: bool,
167    },
168    /// Entries owned by another logical document form the value space.
169    EntrySet {
170        /// Source-authored logical reference.
171        reference: DocumentReference,
172        /// Exact logical destination when namespace-only resolution suffices.
173        #[serde(skip_serializing_if = "Option::is_none")]
174        address: Option<DocumentAddress>,
175        /// Accepted semantic categories in the referenced document.
176        #[schemars(length(min = 1, max = 9))]
177        entry_kinds: Vec<EntryKind>,
178    },
179}
180
181const fn default_true() -> bool {
182    true
183}
184
185// Serde's `skip_serializing_if` predicate receives a reference.
186#[allow(clippy::trivially_copy_pass_by_ref)]
187const fn is_true(value: &bool) -> bool {
188    *value
189}
190
191/// One uniquely addressable node in a query outline.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
193#[serde(
194    tag = "kind",
195    rename_all = "kebab-case",
196    rename_all_fields = "camelCase",
197    deny_unknown_fields
198)]
199pub enum OutlineNode {
200    /// Optional quick-reference node.
201    Tldr {
202        /// Canonical structural outline path.
203        path: NodePath,
204        /// Stable document-local identity.
205        id: NodeId,
206        /// Display title.
207        title: String,
208    },
209    /// Addressable document content that precedes the first heading.
210    DocumentRoot {
211        /// Canonical structural outline path.
212        path: NodePath,
213        /// Virtual document-root identity.
214        id: NodeId,
215        /// Display title for the leading content.
216        title: String,
217        /// Recursive semantic entry coverage for this scope.
218        #[serde(skip_serializing_if = "Option::is_none")]
219        entry_summary: Option<EntrySummary>,
220        /// Nested semantic entries when explicitly expanded.
221        #[serde(default, skip_serializing_if = "Vec::is_empty")]
222        children: Vec<OutlineNode>,
223    },
224    /// One semantic document section.
225    DocumentSection {
226        /// Canonical structural outline path.
227        path: NodePath,
228        /// Stable document-local section identity.
229        id: NodeId,
230        /// Section heading text.
231        title: String,
232        /// Recursive semantic entry coverage owned directly by this section.
233        #[serde(skip_serializing_if = "Option::is_none")]
234        entry_summary: Option<EntrySummary>,
235        /// Nested section and entry nodes.
236        children: Vec<OutlineNode>,
237    },
238    /// One source-neutral semantic definition.
239    DocumentEntry {
240        /// Exact original list/definition item in the loaded document, independent
241        /// of semantic IDs, display labels and filtered outline paths.
242        owner: Box<mant_ir::ContentReveal>,
243        /// Canonical structural outline path.
244        path: NodePath,
245        /// Stable document-local entry identity.
246        id: NodeId,
247        /// Primary display term.
248        title: String,
249        /// Semantic category of the entry.
250        entry_kind: EntryKind,
251        /// Alias case-matching policy.
252        case: NameCase,
253        /// Exact selectable names, not proof of behavioral equivalence.
254        names: Vec<String>,
255        /// Explicit owner-local equivalence groups; no default canonical member.
256        #[serde(default, skip_serializing_if = "Vec::is_empty")]
257        alias_groups: Vec<Vec<String>>,
258        /// Explicit same-document entry relationship; bodies stay independent.
259        #[serde(default, skip_serializing_if = "Option::is_none")]
260        alias_of: Option<NodeId>,
261        /// Author-written input forms.
262        forms: Vec<String>,
263        /// Exact cross-document destinations declared by linked entry terms.
264        #[serde(default, skip_serializing_if = "Vec::is_empty")]
265        document_targets: Vec<EntryDocumentTarget>,
266        /// Optional finite or cross-document value space.
267        #[serde(skip_serializing_if = "Option::is_none")]
268        value_domain: Option<Box<EntryValueDomain>>,
269        /// Recursive semantic entry coverage owned by this entry.
270        #[serde(skip_serializing_if = "Option::is_none")]
271        entry_summary: Option<EntrySummary>,
272        /// Nested entry nodes.
273        #[serde(default, skip_serializing_if = "Vec::is_empty")]
274        children: Vec<OutlineNode>,
275    },
276}
277
278impl OutlineNode {
279    /// Return the canonical structural path.
280    #[must_use]
281    pub fn path(&self) -> &str {
282        match self {
283            Self::Tldr { path, .. }
284            | Self::DocumentRoot { path, .. }
285            | Self::DocumentSection { path, .. }
286            | Self::DocumentEntry { path, .. } => path,
287        }
288    }
289
290    /// Return the stable document-local identity.
291    #[must_use]
292    pub fn id(&self) -> &str {
293        match self {
294            Self::Tldr { id, .. }
295            | Self::DocumentRoot { id, .. }
296            | Self::DocumentSection { id, .. }
297            | Self::DocumentEntry { id, .. } => id,
298        }
299    }
300
301    /// Return the node's display title.
302    #[must_use]
303    pub fn title(&self) -> &str {
304        match self {
305            Self::Tldr { title, .. }
306            | Self::DocumentRoot { title, .. }
307            | Self::DocumentSection { title, .. }
308            | Self::DocumentEntry { title, .. } => title,
309        }
310    }
311
312    /// Return child nodes, or an empty slice for leaf variants.
313    #[must_use]
314    pub fn children(&self) -> &[Self] {
315        match self {
316            Self::DocumentRoot { children, .. }
317            | Self::DocumentSection { children, .. }
318            | Self::DocumentEntry { children, .. } => children,
319            Self::Tldr { .. } => &[],
320        }
321    }
322}
323
324/// Exact schema marker for selected query content.
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
326pub enum ExcerptSchema {
327    /// Version 0.11 of the pre-stable excerpt protocol.
328    #[serde(rename = "mant.excerpt/v0.11")]
329    V0Dot11,
330}
331
332impl ExcerptSchema {
333    /// Serialized identifier of the current excerpt contract.
334    pub const ID: &'static str = "mant.excerpt/v0.11";
335}
336
337/// One or more independently selected nodes from a complete query.
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
339#[serde(rename_all = "camelCase", deny_unknown_fields)]
340#[schemars(extend("$id" = "urn:mant:excerpt:v0.11"))]
341pub struct QueryExcerpt {
342    /// Exact response schema discriminator.
343    pub schema: ExcerptSchema,
344    /// Human-readable selected-document label.
345    pub label: String,
346    /// Plain display title derived from authoritative heading or native metadata.
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub display_title: Option<String>,
349    /// Logical namespace for references in the selected raw IR content.
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub address: Option<DocumentAddress>,
352    /// False when the selected document has rejected or invalid semantics.
353    /// This is document-wide, not a claim restricted to the selected range.
354    #[serde(default = "default_true", skip_serializing_if = "is_true")]
355    pub semantics_complete: bool,
356    /// Process and parser provenance, when a document was loaded.
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub producer: Option<Producer>,
359    /// Authoritative document source, when one was loaded.
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub source: Option<DocumentSource>,
362    /// Document metadata, when one was loaded.
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub meta: Option<DocumentMeta>,
365    /// Recoverable parser and validation findings.
366    #[serde(default, skip_serializing_if = "Vec::is_empty")]
367    pub diagnostics: Vec<Diagnostic>,
368    /// Selected nodes in canonical source order after duplicate selectors are removed.
369    pub selections: Vec<ExcerptSelection>,
370}
371
372/// One selected document node together with its location in the complete outline.
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
374#[serde(
375    tag = "kind",
376    rename_all = "kebab-case",
377    rename_all_fields = "camelCase",
378    deny_unknown_fields
379)]
380pub enum ExcerptSelection {
381    /// Optional quick-reference content preceding the primary document.
382    Tldr {
383        /// Complete logical location in the document outline.
384        outline: OutlineTrail,
385        /// Complete quick-reference content.
386        document: TldrDocument,
387    },
388    /// Complete document content that appears before the first heading.
389    DocumentRoot {
390        /// Complete logical location in the document outline.
391        outline: OutlineTrail,
392        /// Original visible document title, not a generated report label.
393        #[serde(default, skip_serializing_if = "Option::is_none")]
394        heading: Option<mant_ir::Heading>,
395        /// Complete leading blocks.
396        blocks: Vec<Block>,
397    },
398    /// Complete selected document node, including all descendant sections.
399    DocumentSection {
400        /// Complete logical location in the document outline.
401        outline: OutlineTrail,
402        /// Complete selected section including descendants.
403        section: Section,
404    },
405    /// One addressable owner with its complete, original content.
406    DocumentEntry {
407        /// Complete logical location in the document outline.
408        outline: OutlineTrail,
409        /// A single-item list or definition list, retaining original layout and
410        /// numbering. Its sole item owns the entry facts; no body is rewritten.
411        entry: Block,
412    },
413}
414
415impl ExcerptSelection {
416    /// Return the complete logical location of this selection.
417    #[must_use]
418    pub const fn outline(&self) -> &OutlineTrail {
419        match self {
420            Self::Tldr { outline, .. }
421            | Self::DocumentRoot { outline, .. }
422            | Self::DocumentSection { outline, .. }
423            | Self::DocumentEntry { outline, .. } => outline,
424        }
425    }
426}
427
428/// Complete logical location of one addressable document node.
429#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
430#[serde(rename_all = "camelCase", deny_unknown_fields)]
431pub struct OutlineTrail {
432    /// Ordered ancestors from the document root to the direct parent.
433    #[serde(default, skip_serializing_if = "Vec::is_empty")]
434    pub ancestors: Vec<OutlineReference>,
435    /// Selected or matching node at the end of the trail.
436    pub node: OutlineNodeReference,
437}
438
439impl OutlineTrail {
440    /// Return the canonical structural path of the terminal node.
441    #[must_use]
442    pub fn path(&self) -> &str {
443        self.node.path()
444    }
445
446    /// Return the display title of the terminal node.
447    #[must_use]
448    pub fn title(&self) -> &str {
449        self.node.title()
450    }
451}
452
453/// Compact ancestor identity attached to an excerpt selection.
454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
455#[serde(rename_all = "camelCase", deny_unknown_fields)]
456pub struct OutlineReference {
457    /// Canonical structural outline path.
458    pub path: NodePath,
459    /// Stable document-local identity.
460    pub id: NodeId,
461    /// Display title.
462    pub title: String,
463}
464
465/// Compact typed identity for the terminal node in an [`OutlineTrail`].
466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
467#[serde(
468    tag = "kind",
469    rename_all = "kebab-case",
470    rename_all_fields = "camelCase",
471    deny_unknown_fields
472)]
473pub enum OutlineNodeReference {
474    /// Optional quick-reference node.
475    Tldr {
476        /// Canonical structural outline path.
477        path: NodePath,
478        /// Stable document-local identity.
479        id: NodeId,
480        /// Display title.
481        title: String,
482    },
483    /// Addressable content before the first heading.
484    DocumentRoot {
485        /// Canonical structural outline path.
486        path: NodePath,
487        /// Virtual document-root identity.
488        id: NodeId,
489        /// Display title.
490        title: String,
491    },
492    /// One semantic document section.
493    DocumentSection {
494        /// Canonical structural outline path.
495        path: NodePath,
496        /// Stable document-local identity.
497        id: NodeId,
498        /// Section heading text.
499        title: String,
500    },
501    /// One semantic command, option, or variable definition.
502    DocumentEntry {
503        /// Canonical structural outline path.
504        path: NodePath,
505        /// Stable document-local identity.
506        id: NodeId,
507        /// Primary display term.
508        title: String,
509        /// Semantic category of the definition.
510        entry_kind: EntryKind,
511        /// Alias case-matching policy.
512        case: NameCase,
513        /// Validated selectable names, not an equivalence assertion.
514        names: Vec<String>,
515    },
516}
517
518impl OutlineNodeReference {
519    /// Return the canonical structural path.
520    #[must_use]
521    pub fn path(&self) -> &str {
522        match self {
523            Self::Tldr { path, .. }
524            | Self::DocumentRoot { path, .. }
525            | Self::DocumentSection { path, .. }
526            | Self::DocumentEntry { path, .. } => path,
527        }
528    }
529
530    /// Return the stable document-local identity.
531    #[must_use]
532    pub fn id(&self) -> &str {
533        match self {
534            Self::Tldr { id, .. }
535            | Self::DocumentRoot { id, .. }
536            | Self::DocumentSection { id, .. }
537            | Self::DocumentEntry { id, .. } => id,
538        }
539    }
540
541    /// Return the display title.
542    #[must_use]
543    pub fn title(&self) -> &str {
544        match self {
545            Self::Tldr { title, .. }
546            | Self::DocumentRoot { title, .. }
547            | Self::DocumentSection { title, .. }
548            | Self::DocumentEntry { title, .. } => title,
549        }
550    }
551}