Skip to main content

vole_document/field/
provenance.rs

1//! Provenance and typed answers for field observations (Phase 11.5, ADR-0026).
2//!
3//! Every observation returns a [`FieldAnswer`] that carries its epistemic
4//! [`Basis`], the scope over which integrity was actually verified, the exact
5//! source span (when one exists), and the dependency ids that were read. An
6//! authored or directly-observed answer is exact; a derived, inferred, or
7//! heuristic answer is **never** labelled exact, so `Q_ref` and `Q_gen` cannot be
8//! blurred (ADR-0026, DEC-8).
9
10use crate::store::NodeId;
11
12/// The epistemic basis of an answer.
13///
14/// `is_exact` is true only for answers that are byte-identical observations of
15/// the source. A deterministically-derived projection (e.g. inflated stream
16/// bytes) is reproducible but is not an exact source span, so it is not exact.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Basis {
19    /// The value was authored as a literal (never produced by this module yet;
20    /// reserved for `Literal` seed nodes).
21    Authored,
22    /// The exact source bytes were observed directly.
23    DirectlyObserved,
24    /// A deterministic projection of retained state (inflated bytes, structure).
25    DeterministicallyDerived,
26    /// Inferred from partial evidence; never exact.
27    Inferred,
28    /// A best-effort heuristic projection (e.g. text runs); never exact.
29    Heuristic,
30    /// Could not be resolved.
31    Unresolved,
32}
33
34impl Basis {
35    /// Stable lower-case name (used in JSON and receipts).
36    pub const fn name(self) -> &'static str {
37        match self {
38            Basis::Authored => "authored",
39            Basis::DirectlyObserved => "directly-observed",
40            Basis::DeterministicallyDerived => "deterministically-derived",
41            Basis::Inferred => "inferred",
42            Basis::Heuristic => "heuristic",
43            Basis::Unresolved => "unresolved",
44        }
45    }
46
47    /// Whether an answer with this basis is an exact observation of the source.
48    pub const fn is_exact(self) -> bool {
49        matches!(self, Basis::Authored | Basis::DirectlyObserved)
50    }
51}
52
53/// How much of the source integrity an observation actually verified.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum IntegrityScope {
56    /// No source integrity was re-verified by this observation.
57    None,
58    /// A single seed node's bytes were content-verified.
59    Node,
60    /// The whole source SHA-256 was verified (full materialization only).
61    WholeSource,
62}
63
64impl IntegrityScope {
65    /// Stable lower-case name (used in JSON).
66    pub const fn name(self) -> &'static str {
67        match self {
68            IntegrityScope::None => "none",
69            IntegrityScope::Node => "node",
70            IntegrityScope::WholeSource => "whole-source",
71        }
72    }
73}
74
75/// The payload of a [`FieldAnswer`].
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum AnswerValue {
78    /// Exact bytes.
79    Bytes(Vec<u8>),
80    /// UTF-8 (possibly lossy) text.
81    Text(String),
82    /// A JSON document body.
83    Json(String),
84    /// No value.
85    None,
86}
87
88impl AnswerValue {
89    /// The length in bytes this value contributes to the output budget.
90    pub fn byte_len(&self) -> u64 {
91        match self {
92            AnswerValue::Bytes(b) => b.len() as u64,
93            AnswerValue::Text(s) | AnswerValue::Json(s) => s.len() as u64,
94            AnswerValue::None => 0,
95        }
96    }
97}
98
99/// A typed observation with full provenance.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct FieldAnswer {
102    /// The observed value.
103    pub value: AnswerValue,
104    /// The epistemic basis.
105    pub basis: Basis,
106    /// Canonical selector text, e.g. `page:1` or `byte-range:10:4`.
107    pub selector: String,
108    /// Canonical representation text, e.g. `text` or `exact`.
109    pub representation: String,
110    /// The exact source span this answer maps to, when one exists.
111    pub source_span: Option<(u64, u64)>,
112    /// Short provenance/basis string: which layer and format-specific identity
113    /// produced this answer (e.g. a DOCX story, part, table/row/cell, and the
114    /// extraction profile). Advisory and deterministic; never authority.
115    pub provenance: String,
116    /// The seed nodes actually read to produce this answer.
117    pub dependency_ids: Vec<NodeId>,
118    /// How much source integrity was verified.
119    pub integrity_scope: IntegrityScope,
120    /// Whether this answer is an exact observation of the source.
121    pub exact: bool,
122}
123
124/// Minimal deterministic JSON string escaping (no escaping of `/`).
125pub(crate) fn json_escape(s: &str) -> String {
126    let mut out = String::with_capacity(s.len() + 2);
127    for c in s.chars() {
128        match c {
129            '"' => out.push_str("\\\""),
130            '\\' => out.push_str("\\\\"),
131            '\n' => out.push_str("\\n"),
132            '\r' => out.push_str("\\r"),
133            '\t' => out.push_str("\\t"),
134            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
135            c => out.push(c),
136        }
137    }
138    out
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn only_authored_and_observed_are_exact() {
147        assert!(Basis::Authored.is_exact());
148        assert!(Basis::DirectlyObserved.is_exact());
149        assert!(!Basis::DeterministicallyDerived.is_exact());
150        assert!(!Basis::Inferred.is_exact());
151        assert!(!Basis::Heuristic.is_exact());
152        assert!(!Basis::Unresolved.is_exact());
153    }
154
155    #[test]
156    fn json_escape_is_deterministic_and_safe() {
157        assert_eq!(json_escape("a\"b\\c\nd"), "a\\\"b\\\\c\\nd");
158        assert_eq!(json_escape("plain"), "plain");
159    }
160}