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}