mant_protocol/scope.rs
1//! Stable contracts for bounded queries over a linked set of documents.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 DocumentAddress, QueryExcerpt, QuerySearch, SearchCase, SearchQuery, SearchScope, SearchSyntax,
8 default_search_limit,
9};
10
11/// Maximum number of initial documents accepted by the native scope contract.
12pub const MAX_SCOPE_DOCUMENTS: usize = 16;
13/// Default maximum number of link edges followed from an initial document.
14pub const DEFAULT_SCOPE_DEPTH: u16 = 8;
15/// Hard maximum number of link edges accepted by the native scope contract.
16pub const MAX_SCOPE_DEPTH: u16 = 32;
17/// Default maximum number of distinct documents in one resolved scope.
18pub const DEFAULT_SCOPE_DOCUMENT_LIMIT: u32 = 64;
19/// Hard maximum number of distinct documents in one resolved scope.
20pub const MAX_SCOPE_DOCUMENT_LIMIT: u32 = 256;
21/// Maximum aggregate normalized-document payload retained by one scope.
22///
23/// Scope resolution keeps each parsed document in memory so later search,
24/// explanation, and interactive navigation observe one consistent graph. This
25/// independent guard prevents a small number of individually valid documents
26/// from creating an unbounded aggregate allocation.
27pub const MAX_SCOPE_CONTENT_BYTES: u64 = 64 * 1024 * 1024;
28/// Maximum UTF-8 byte length of one logical document selector.
29pub const MAX_DOCUMENT_SELECTOR_BYTES: usize = 1024;
30/// Maximum UTF-8 byte length of one semantic-entry selector.
31pub const MAX_SEMANTIC_ENTRY_BYTES: usize = 512;
32
33/// One violated runtime constraint shared by scope-query request adapters.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ScopeTextError {
36 /// The value was empty after trimming surrounding whitespace.
37 Empty,
38 /// The value contained a terminal or structural control character.
39 ControlCharacter,
40 /// The UTF-8 byte length exceeded the declared maximum.
41 TooLong {
42 /// Inclusive maximum accepted byte length.
43 maximum: usize,
44 },
45}
46
47/// Validate one bounded logical selector at the native request boundary.
48///
49/// JSON Schema advertises the same limits, but native `--request-json` callers
50/// do not pass through a schema validator, so the runtime contract must check
51/// them independently.
52///
53/// # Errors
54///
55/// Returns the precise empty, control-character, or byte-length violation.
56pub fn validate_scope_text(value: &str, maximum: usize) -> Result<(), ScopeTextError> {
57 if value.trim().is_empty() {
58 return Err(ScopeTextError::Empty);
59 }
60 if value.chars().any(char::is_control) {
61 return Err(ScopeTextError::ControlCharacter);
62 }
63 if value.len() > maximum {
64 return Err(ScopeTextError::TooLong { maximum });
65 }
66 Ok(())
67}
68
69/// One logical document selector before catalog resolution.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
71#[serde(rename_all = "camelCase", deny_unknown_fields)]
72pub struct DocumentSelector {
73 /// Unqualified name or complete catalog path.
74 #[schemars(length(min = 1, max = MAX_DOCUMENT_SELECTOR_BYTES))]
75 pub selector: String,
76 /// Optional configured Markdown source for an unqualified selector.
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub source: Option<String>,
79 /// Optional native manual category for an unqualified selector.
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub manual_section: Option<String>,
82}
83
84/// Bounded traversal applied after resolving the initial documents.
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
86#[serde(rename_all = "camelCase", deny_unknown_fields)]
87pub struct DocumentTraversal {
88 /// Follow typed links to other registered documents.
89 #[serde(default)]
90 pub follow_links: bool,
91 /// Optional maximum number of link edges from an initial document.
92 ///
93 /// Omission selects [`DEFAULT_SCOPE_DEPTH`] when [`Self::follow_links`] is
94 /// true. The field is invalid when link traversal is disabled.
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 #[schemars(range(max = 32))]
97 pub max_depth: Option<u16>,
98 /// Optional maximum number of distinct documents, including roots.
99 ///
100 /// Omission selects [`DEFAULT_SCOPE_DOCUMENT_LIMIT`] when
101 /// [`Self::follow_links`] is true. The field is invalid when link traversal
102 /// is disabled.
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 #[schemars(range(min = 1, max = 256))]
105 pub max_documents: Option<u32>,
106}
107
108impl DocumentTraversal {
109 /// Effective edge limit after applying the native default.
110 #[must_use]
111 pub fn effective_max_depth(self) -> u16 {
112 self.max_depth.unwrap_or(DEFAULT_SCOPE_DEPTH)
113 }
114
115 /// Effective document budget after applying the native default.
116 #[must_use]
117 pub fn effective_max_documents(self) -> u32 {
118 self.max_documents.unwrap_or(DEFAULT_SCOPE_DOCUMENT_LIMIT)
119 }
120}
121
122/// Return [`DEFAULT_SCOPE_DEPTH`].
123#[must_use]
124pub const fn default_scope_depth() -> u16 {
125 DEFAULT_SCOPE_DEPTH
126}
127
128/// Return [`DEFAULT_SCOPE_DOCUMENT_LIMIT`].
129#[must_use]
130pub const fn default_scope_document_limit() -> u32 {
131 DEFAULT_SCOPE_DOCUMENT_LIMIT
132}
133
134/// Initial documents and the link policy used to expand them.
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
136#[serde(rename_all = "camelCase", deny_unknown_fields)]
137pub struct DocumentScope {
138 /// Ordered initial documents. The first one is the initial TUI page.
139 #[schemars(length(min = 1, max = 16))]
140 pub documents: Vec<DocumentSelector>,
141 /// Deterministic outbound-link traversal policy.
142 #[serde(default)]
143 pub traversal: DocumentTraversal,
144}
145
146/// Exact schema marker for a scope-query request.
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
148pub enum ScopeRequestSchema {
149 /// Version 0.9 of the pre-stable scope-query request.
150 #[serde(rename = "mant.scope-request/v0.9")]
151 V0Dot9,
152}
153
154impl ScopeRequestSchema {
155 /// Serialized identifier of the current request contract.
156 pub const ID: &'static str = "mant.scope-request/v0.9";
157}
158
159/// Query projection supported over a document set.
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
161#[serde(
162 tag = "kind",
163 rename_all = "kebab-case",
164 rename_all_fields = "camelCase",
165 deny_unknown_fields
166)]
167pub enum ScopeQueryView {
168 /// Resolve one semantic entry independently in every document.
169 Explain {
170 /// Exact alias, outline path, or stable ID.
171 #[schemars(length(min = 1, max = MAX_SEMANTIC_ENTRY_BYTES))]
172 entry: String,
173 },
174 /// Search visible or generated-Markdown text over the complete scope.
175 Search {
176 /// Literal or regular-expression search pattern.
177 #[schemars(length(min = 1, max = 4096))]
178 pattern: String,
179 /// Pattern language.
180 #[serde(default)]
181 syntax: SearchSyntax,
182 /// Case-matching policy.
183 #[serde(default)]
184 case: SearchCase,
185 /// Semantic representation searched.
186 #[serde(default)]
187 scope: SearchScope,
188 /// Require Unicode-aware word boundaries.
189 #[serde(default)]
190 word: bool,
191 /// Neighboring rendered lines included around a match.
192 #[serde(default)]
193 #[schemars(range(max = 100))]
194 context_lines: u16,
195 /// Global maximum number of matching line groups returned.
196 #[serde(default = "default_search_limit")]
197 #[schemars(range(min = 1, max = 10000))]
198 limit: u32,
199 /// Global number of matching line groups skipped.
200 #[serde(default)]
201 offset: u32,
202 },
203}
204
205/// Native request for a bounded multi-document query.
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
207#[serde(rename_all = "camelCase", deny_unknown_fields)]
208#[schemars(extend("$id" = "urn:mant:scope-request:v0.9"))]
209pub struct ScopeQueryRequest {
210 /// Exact request schema discriminator.
211 pub schema: ScopeRequestSchema,
212 /// Initial documents and traversal limits.
213 pub scope: DocumentScope,
214 /// Projection applied independently to resolved documents.
215 pub view: ScopeQueryView,
216}
217
218/// Exact schema marker for a resolved scope query.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
220pub enum ScopeQuerySchema {
221 /// Version 0.9 of the pre-stable scope-query result.
222 #[serde(rename = "mant.scope-query/v0.9")]
223 V0Dot9,
224}
225
226impl ScopeQuerySchema {
227 /// Serialized identifier of the current result contract.
228 pub const ID: &'static str = "mant.scope-query/v0.9";
229}
230
231/// Typed cross-document edge retained in a resolved scope.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
233#[serde(rename_all = "kebab-case")]
234pub enum DocumentEdgeKind {
235 /// A relative Markdown link inside one registered namespace.
236 Document,
237 /// A semantic native-manual reference.
238 Manual,
239}
240
241/// Traversal bound that excluded an outbound logical link.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
243#[serde(rename_all = "kebab-case")]
244pub enum TraversalLimit {
245 /// The maximum number of followed link edges was reached.
246 MaxDepth,
247 /// The maximum number of distinct loaded documents was reached.
248 MaxDocuments,
249 /// Retaining another normalized document would exceed the aggregate
250 /// semantic-content budget.
251 MaxContentBytes,
252}
253
254/// One typed outbound link excluded by a traversal bound.
255///
256/// A frontier retains the logical selector rather than requiring a resolved
257/// address: resolving a target may itself exceed the requested bound.
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
259#[serde(rename_all = "camelCase")]
260pub struct DocumentFrontier {
261 /// Address containing the excluded link.
262 pub from: DocumentAddress,
263 /// Logical target that would be resolved if traversal continued.
264 pub target: DocumentSelector,
265 /// Semantic link family.
266 pub kind: DocumentEdgeKind,
267 /// Bound that prevented traversal of this link.
268 pub limit: TraversalLimit,
269}
270
271/// One resolved edge in source order.
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
273#[serde(rename_all = "camelCase")]
274pub struct DocumentEdge {
275 /// Address containing the link.
276 pub from: DocumentAddress,
277 /// Resolved linked address.
278 pub to: DocumentAddress,
279 /// Semantic link family.
280 pub kind: DocumentEdgeKind,
281}
282
283/// One distinct document in breadth-first traversal order.
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
285#[serde(rename_all = "camelCase")]
286pub struct ScopedDocument {
287 /// Stable logical document identity.
288 pub address: DocumentAddress,
289 /// Minimum outbound-link distance from any initial document.
290 pub depth: u16,
291 /// Initial document positions that resolve to this address.
292 #[serde(default, skip_serializing_if = "Vec::is_empty")]
293 pub root_indices: Vec<u16>,
294 /// Distinct documents whose links reached this address.
295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
296 pub reached_from: Vec<DocumentAddress>,
297}
298
299/// A seed or typed link that could not be resolved.
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
301#[serde(rename_all = "camelCase")]
302pub struct UnresolvedDocument {
303 /// Referring document, omitted for an initial selector.
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub from: Option<DocumentAddress>,
306 /// Original logical selector or link target.
307 pub selector: DocumentSelector,
308 /// Stable, concise resolution diagnostic.
309 pub reason: String,
310}
311
312/// Logical graph produced before applying a projection.
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
314#[serde(rename_all = "camelCase")]
315pub struct ResolvedDocumentScope {
316 /// Original normalized scope request.
317 pub query: DocumentScope,
318 /// Distinct documents in deterministic breadth-first order.
319 pub documents: Vec<ScopedDocument>,
320 /// Successfully resolved typed edges in source order.
321 pub edges: Vec<DocumentEdge>,
322 /// Typed outbound links excluded by depth, document, or content limits.
323 #[serde(default, skip_serializing_if = "Vec::is_empty")]
324 pub frontier: Vec<DocumentFrontier>,
325 /// Seeds and edges that could not resolve to a readable document.
326 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub unresolved: Vec<UnresolvedDocument>,
328}
329
330/// One document's search hits inside a globally paginated scope result.
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
332#[serde(rename_all = "camelCase")]
333pub struct ScopedSearchDocument {
334 /// Stable logical document identity.
335 pub address: DocumentAddress,
336 /// Distance retained from the resolved scope.
337 pub depth: u16,
338 /// Matching line groups retained for this global page together with their
339 /// document-local coordinate contract.
340 pub search: QuerySearch,
341}
342
343/// Globally paginated search over a resolved document scope.
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
345#[serde(rename_all = "camelCase")]
346pub struct ScopeSearch {
347 /// Normalized search configuration.
348 pub query: SearchQuery,
349 /// Matching line groups across all documents before pagination.
350 pub total: u32,
351 /// Matching line groups present in this response.
352 pub returned: u32,
353 /// Applied global zero-based offset.
354 pub offset: u32,
355 /// Whether additional matching line groups remain.
356 pub truncated: bool,
357 /// Global offset for the next page.
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub next_offset: Option<u32>,
360 /// Non-empty document groups in scope order.
361 pub documents: Vec<ScopedSearchDocument>,
362}
363
364/// One successful semantic-entry selection in a document scope.
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
366#[serde(rename_all = "camelCase")]
367pub struct ScopedExplanation {
368 /// Stable logical document identity.
369 pub address: DocumentAddress,
370 /// Distance retained from the resolved scope.
371 pub depth: u16,
372 /// Complete selected semantic entry or ambiguity candidates.
373 pub excerpt: QueryExcerpt,
374}
375
376/// One per-document projection failure that does not invalidate other results.
377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
378#[serde(rename_all = "camelCase")]
379pub struct ScopedQueryFailure {
380 /// Stable logical document identity.
381 pub address: DocumentAddress,
382 /// Concise projection diagnostic.
383 pub reason: String,
384}
385
386/// Projection result carried by a scope-query response.
387#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
388#[serde(
389 tag = "kind",
390 rename_all = "kebab-case",
391 rename_all_fields = "camelCase"
392)]
393pub enum ScopeQueryResult {
394 /// Semantic entries found across the scope.
395 Explain {
396 /// Requested entry selector.
397 entry: String,
398 /// Documents with one or more exact candidates.
399 matches: Vec<ScopedExplanation>,
400 /// Resolved documents in which the entry was not present.
401 missed: u32,
402 /// Ambiguity or projection failures, excluding ordinary misses.
403 #[serde(default, skip_serializing_if = "Vec::is_empty")]
404 failures: Vec<ScopedQueryFailure>,
405 },
406 /// Globally paginated text search.
407 Search {
408 /// Search result grouped by document.
409 search: ScopeSearch,
410 },
411}
412
413/// Complete bounded multi-document response.
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
415#[serde(rename_all = "camelCase")]
416#[schemars(extend("$id" = "urn:mant:scope-query:v0.9"))]
417pub struct ScopeQueryResponse {
418 /// Exact response schema discriminator.
419 pub schema: ScopeQuerySchema,
420 /// Resolved logical graph, including missing links and truncation.
421 pub scope: ResolvedDocumentScope,
422 /// Requested projection over that graph.
423 pub result: ScopeQueryResult,
424}