Skip to main content

mant_protocol/
references.rs

1//! Bounded reference inventories, independent from readable outline nodes.
2
3pub use mant_ir::ReferenceTargetType;
4use mant_ir::{
5    ContentLocation, ContentReveal, DocumentAddress, LinkTarget, ReferenceScanReport,
6    ReferenceScanStop,
7};
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11/// Whether reference facts are omitted, counted, or materialized.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
13#[serde(rename_all = "kebab-case")]
14pub enum ReferenceProjectionMode {
15    /// Do not traverse reference content.
16    None,
17    /// Count references without cloning labels or positions.
18    #[default]
19    Summary,
20    /// Return a bounded occurrence page as well as counts.
21    All,
22}
23
24/// Independent reference selection policy; entry filtering never changes it.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct ReferenceProjection {
28    /// Projection mode.
29    #[serde(default)]
30    pub mode: ReferenceProjectionMode,
31    /// Original typed target kinds, defaulting to document/manual.
32    #[serde(default = "default_target_types")]
33    pub target_types: Vec<ReferenceTargetType>,
34    /// Zero-based selected occurrence offset, not distinct-target offset.
35    #[serde(default)]
36    pub offset: u32,
37    /// Maximum materialized occurrences, between 1 and 1,000.
38    #[serde(default = "default_limit")]
39    pub limit: u32,
40}
41
42fn default_target_types() -> Vec<ReferenceTargetType> {
43    vec![ReferenceTargetType::Document, ReferenceTargetType::Manual]
44}
45const fn default_limit() -> u32 {
46    100
47}
48
49impl Default for ReferenceProjection {
50    fn default() -> Self {
51        Self {
52            mode: ReferenceProjectionMode::Summary,
53            target_types: default_target_types(),
54            offset: 0,
55            limit: 100,
56        }
57    }
58}
59
60impl ReferenceProjection {
61    /// Validate a bounded request before scanning. An empty type set deliberately selects none.
62    ///
63    /// # Errors
64    /// Rejects excessive or repeated kinds and invalid page limits.
65    pub fn validate(&self) -> Result<(), &'static str> {
66        if self.limit == 0 || self.limit > 1000 {
67            return Err("reference limit must be between 1 and 1000");
68        }
69        if self.target_types.len() > 5 {
70            return Err("at most five reference target types are accepted");
71        }
72        for (index, kind) in self.target_types.iter().enumerate() {
73            if self.target_types[..index].contains(kind) {
74                return Err("reference target types must not repeat");
75            }
76        }
77        Ok(())
78    }
79}
80
81/// Why bounded traversal could not inspect all selected source content.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
83#[serde(rename_all = "kebab-case")]
84pub enum ReferenceLimitReason {
85    /// Shared work-unit ceiling.
86    Steps,
87    /// Structural/inline nesting ceiling.
88    Depth,
89    /// Inspected target/label/form byte ceiling.
90    Bytes,
91    /// A position cannot be represented within its encoded-size ceiling.
92    Position,
93    /// Selected source root is invalid for the loaded snapshot.
94    InvalidRoot,
95    /// Consumer deliberately stopped the traversal.
96    ConsumerStop,
97}
98
99impl From<ReferenceScanStop> for ReferenceLimitReason {
100    fn from(value: ReferenceScanStop) -> Self {
101        match value {
102            ReferenceScanStop::Steps => Self::Steps,
103            ReferenceScanStop::Depth => Self::Depth,
104            ReferenceScanStop::Bytes => Self::Bytes,
105            ReferenceScanStop::Position => Self::Position,
106            ReferenceScanStop::InvalidRoot => Self::InvalidRoot,
107            ReferenceScanStop::Visitor => Self::ConsumerStop,
108        }
109    }
110}
111
112/// Coverage is independent of target-set precision and returned-page truncation.
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
114#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
115pub enum ReferenceCoverageStatus {
116    /// Every selected content root was traversed.
117    Complete {},
118    /// Traversal stopped at a resource or structural boundary.
119    Limited {
120        /// First limit reached.
121        reason: ReferenceLimitReason,
122    },
123    /// Reference projection was disabled.
124    NotScanned {},
125}
126
127/// Accounted work and actual content coverage for one scan.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
129#[serde(rename_all = "camelCase", deny_unknown_fields)]
130pub struct ReferenceCoverage {
131    /// Charged traversal and optional validation steps.
132    pub steps: u32,
133    /// Charged target, label and form inspection bytes.
134    pub bytes: u32,
135    /// Whether all selected source content was inspected.
136    pub status: ReferenceCoverageStatus,
137}
138
139impl ReferenceCoverage {
140    /// Convert a bounded IR scan report without inventing complete coverage.
141    #[must_use]
142    pub fn from_report(report: ReferenceScanReport) -> Self {
143        Self {
144            steps: u32::try_from(report.steps).unwrap_or(u32::MAX),
145            bytes: u32::try_from(report.bytes).unwrap_or(u32::MAX),
146            status: report
147                .stopped
148                .map_or(ReferenceCoverageStatus::Complete {}, |reason| {
149                    ReferenceCoverageStatus::Limited {
150                        reason: reason.into(),
151                    }
152                }),
153        }
154    }
155}
156
157/// Precision of an occurrence or distinct-target count.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
159#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
160pub enum ReferenceCount {
161    /// A complete count of the selected population.
162    Exact {
163        /// Exact count.
164        value: u64,
165    },
166    /// A proven lower bound; unseen targets are not guessed.
167    LowerBound {
168        /// Number certainly observed.
169        value: u64,
170    },
171    /// Counting was not attempted.
172    Unknown {
173        /// Why no count was established.
174        reason: ReferenceUnknownReason,
175    },
176}
177
178/// Why a requested quantity has no observation.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
180#[serde(rename_all = "kebab-case")]
181pub enum ReferenceUnknownReason {
182    /// The caller selected references=none.
183    Disabled,
184    /// No authoritative document content was available for scanning.
185    NotScanned,
186    /// The caller supplied an invalid projection policy.
187    InvalidPolicy,
188}
189
190/// A return-page cap, not a scan-coverage claim.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
192#[serde(rename_all = "kebab-case")]
193pub enum ReferencePageLimit {
194    /// Requested occurrence limit.
195    Records,
196    /// Total retained labels, positions, targets and metadata budget.
197    MaterializationBytes,
198    /// Exact position cannot fit its independent bound.
199    Position,
200    /// Traversal or optional association work was exhausted.
201    Scan,
202}
203
204/// A bounded page over original link occurrences in source order.
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
206#[serde(rename_all = "camelCase", deny_unknown_fields)]
207pub struct ReferencePage {
208    /// Requested selected-occurrence offset.
209    pub offset: u32,
210    /// Number of retained records.
211    pub returned: u32,
212    /// Why additional requested records were not retained, if applicable.
213    pub limited: Option<ReferencePageLimit>,
214    /// Offered only when a fresh bounded scan has proven it can reach another occurrence.
215    pub next_offset: Option<u32>,
216}
217
218/// Validation state of optional original form bindings.
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
220#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
221pub enum ReferenceAssociation {
222    /// No attached semantic owner or no recorded forms.
223    Unrecorded {},
224    /// Entire original form set was validated; indices refer to that owner.
225    Valid {
226        /// Exact semantic owner whose full form set was validated. This can be
227        /// an ancestor of the nearest ordinary content item in `record.owner`.
228        owner: ContentReveal,
229        /// Zero-based original form indices sharing this occurrence.
230        forms: Vec<u32>,
231    },
232    /// Invalid bindings are not partially projected.
233    Invalid {},
234    /// Association inspection hit a shared operation limit.
235    Limited {},
236}
237
238/// Fragment facts permitted before a target document is loaded.
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
240#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
241pub enum UnloadedFragment {
242    /// Original target did not specify a fragment.
243    Absent {},
244    /// Original fragment is retained in the target, but has not been checked.
245    Unchecked {},
246}
247
248/// Fragment outcome in an actually loaded target snapshot.
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
250#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
251pub enum LoadedFragment {
252    /// Original target did not specify a fragment.
253    Absent {},
254    /// Exactly one logical content destination was found.
255    Valid {
256        /// Precise reveal destination, not a fabricated readable subtree.
257        reveal: ContentReveal,
258    },
259    /// A complete target scan found no destination.
260    Missing {},
261    /// More than one logical destination matches.
262    Ambiguous {},
263    /// Limits prevented a complete destination check; this is not absence.
264    Limited {},
265}
266
267/// Staged resolution; locating an address never implies loading or validating it.
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
269#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
270pub enum ReferenceResolution {
271    /// External/email targets are not probed.
272    NotApplicable {},
273    /// Catalog lookup is required, such as a manual without a section.
274    NotQueried {
275        /// Only unloaded fragment facts may be stated.
276        fragment: UnloadedFragment,
277    },
278    /// A direct-file source has no registered namespace for relative resolution.
279    MissingContext {
280        /// Original fragment remains unchecked.
281        fragment: UnloadedFragment,
282    },
283    /// Namespace rules derive this logical address; existence is not established.
284    LogicalAddress {
285        /// Namespace-confined logical destination.
286        address: DocumentAddress,
287        /// No target document has been loaded.
288        fragment: UnloadedFragment,
289    },
290    /// The target is the already loaded source document; no extra I/O occurred.
291    Loaded {
292        /// Logical address when the source is registered; never a host path.
293        address: Option<DocumentAddress>,
294        /// Result based on that exact loaded snapshot.
295        fragment: LoadedFragment,
296    },
297    /// Namespace/address grammar does not permit this logical reference.
298    Restricted {},
299}
300
301/// One occurrence, not a grouped or deduplicated navigation row.
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
303#[serde(rename_all = "camelCase", deny_unknown_fields)]
304pub struct ReferenceRecord {
305    /// Snapshot-local location of the original `Inline::Link`.
306    pub origin: ContentLocation,
307    /// Explicit containing readable source subtree. This is not the occurrence
308    /// itself and never follows the reference's destination.
309    pub source_read: crate::ContentSelector,
310    /// Nearest original content item, when present.
311    pub owner: Option<ContentReveal>,
312    /// Visible plain label from original children; may be empty.
313    pub label: String,
314    /// Whether the UTF-8-safe label prefix was bounded.
315    pub label_truncated: bool,
316    /// Complete original typed target, including its fragment.
317    pub target: LinkTarget,
318    /// Atomic validation outcome of original form bindings.
319    pub association: ReferenceAssociation,
320    /// Facts established without loading other documents or probing external URIs.
321    pub resolution: ReferenceResolution,
322}
323
324/// Independent reference output embedded alongside an outline's content tree.
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
326#[serde(rename_all = "camelCase", deny_unknown_fields)]
327pub struct ReferenceInventory {
328    /// Effective request policy.
329    pub policy: ReferenceProjection,
330    /// Source content scan coverage.
331    pub coverage: ReferenceCoverage,
332    /// Separate local-destination validation pass, sharing the operation budget.
333    /// A limited target pass does not make an already exact occurrence count inexact.
334    pub target_coverage: Option<ReferenceCoverage>,
335    /// Occurrences within source root and selected target types.
336    pub occurrences: ReferenceCount,
337    /// Distinct complete typed targets, including fragments.
338    pub targets: ReferenceCount,
339    /// Occurrence-based return window.
340    pub page: ReferencePage,
341    /// Bounded records, empty for summary/none.
342    pub records: Vec<ReferenceRecord>,
343}
344
345impl ReferenceInventory {
346    /// Empty, explicitly unscanned result, including tldr-only responses.
347    #[must_use]
348    pub fn not_scanned(policy: ReferenceProjection) -> Self {
349        let reason = if policy.mode == ReferenceProjectionMode::None {
350            ReferenceUnknownReason::Disabled
351        } else {
352            ReferenceUnknownReason::NotScanned
353        };
354        Self {
355            page: ReferencePage {
356                offset: policy.offset,
357                returned: 0,
358                limited: None,
359                next_offset: None,
360            },
361            policy,
362            coverage: ReferenceCoverage {
363                steps: 0,
364                bytes: 0,
365                status: ReferenceCoverageStatus::NotScanned {},
366            },
367            target_coverage: None,
368            occurrences: ReferenceCount::Unknown { reason },
369            targets: ReferenceCount::Unknown { reason },
370            records: Vec::new(),
371        }
372    }
373}
374
375impl Default for ReferenceInventory {
376    fn default() -> Self {
377        Self::not_scanned(ReferenceProjection::default())
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    #[test]
385    fn policy_is_bounded_and_resolution_stages_are_closed() {
386        assert!(ReferenceProjection::default().validate().is_ok());
387        assert!(
388            ReferenceProjection {
389                limit: 1001,
390                ..Default::default()
391            }
392            .validate()
393            .is_err()
394        );
395        assert!(
396            ReferenceProjection {
397                target_types: vec![ReferenceTargetType::Local; 2],
398                ..Default::default()
399            }
400            .validate()
401            .is_err()
402        );
403        for json in [
404            r#"{"kind":"logical-address","address":{"kind":"markdown","path":"x","origin":{"kind":"documents"}},"fragment":{"kind":"valid","reveal":{"kind":"document"}}}"#,
405            r#"{"kind":"not-applicable","resolved":true}"#,
406            r#"{"kind":"loaded","address":null,"fragment":{"kind":"absent","reveal":{"kind":"document"}}}"#,
407        ] {
408            assert!(
409                serde_json::from_str::<ReferenceResolution>(json).is_err(),
410                "{json}"
411            );
412        }
413    }
414
415    #[test]
416    fn count_precision_and_loaded_fragment_wire_have_no_contradictory_variants() {
417        for value in [
418            r#"{"kind":"exact","value":1,"reason":"disabled"}"#,
419            r#"{"kind":"lower-bound"}"#,
420            r#"{"kind":"unknown","reason":"disabled","value":0}"#,
421        ] {
422            assert!(
423                serde_json::from_str::<ReferenceCount>(value).is_err(),
424                "{value}"
425            );
426        }
427        for value in [
428            r#"{"kind":"missing","reveal":{"kind":"document"}}"#,
429            r#"{"kind":"valid"}"#,
430            r#"{"kind":"ambiguous","address":null}"#,
431        ] {
432            assert!(
433                serde_json::from_str::<LoadedFragment>(value).is_err(),
434                "{value}"
435            );
436        }
437        let valid = ReferenceResolution::LogicalAddress {
438            address: DocumentAddress::parse_catalog_path("documents/topic").unwrap(),
439            fragment: UnloadedFragment::Unchecked {},
440        };
441        let wire = serde_json::to_string(&valid).unwrap();
442        assert_eq!(
443            serde_json::from_str::<ReferenceResolution>(&wire).unwrap(),
444            valid
445        );
446    }
447}