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