Skip to main content

vole_document/field/
explain.rs

1//! EXPLAIN and EXPLAIN ANALYZE for field observations (Phase 11.5, ADR-0026).
2//!
3//! [`explain`] is a pure function of validated metadata: it never materializes a
4//! node and never writes to the store. [`explain_analyze`] executes the
5//! observation and returns both the intended plan and the measured actual work,
6//! which is the central evidence surface for Phase-11 claims (ADR-0027).
7//!
8//! The JSON key sets are frozen and exact:
9//!
10//! * plan   — `selector`, `representation`, `shape`, `index_reads`,
11//!   `required_nodes`, `will_materialize`, `will_not_materialize`.
12//! * actual — `index_nodes_read`, `seed_nodes_fetched`, `seed_nodes_materialized`,
13//!   `descriptor_bytes_read`, `descriptor_read_mode`, `manifest_bytes_read`,
14//!   `index_bytes_read`, `seed_bytes_read`, `bytes_read`, `bytes_returned`,
15//!   `deepened`, `wall_micros`, `basis`, `exact`.
16//!
17//! `bytes_read` is the **sum** of the four `*_bytes_read` classes: total physical
18//! bytes this observation made the OS fetch, including the descriptor blob. It is
19//! deliberately not the seed-store-only number it was before Phase 11.9's
20//! review (ADR-0027 accounting).
21
22use crate::error::Result;
23use crate::field::manifest::FieldRoot;
24use crate::field::observe::{ObserveRequest, ObserveStats, OpenedField, observe_opened};
25use crate::field::plan::{ObservePlan, plan};
26use crate::field::provenance::{Basis, json_escape};
27use crate::field::{FieldId, FieldStore};
28use crate::limits::Limits;
29
30/// The intended plan plus its canonical JSON rendering.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct ExplainPlan {
33    /// The structured plan.
34    pub plan: ObservePlan,
35    /// The plan as a flat JSON object with exactly the documented keys.
36    pub json: String,
37}
38
39/// The measured cost of an executed observation.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ExplainActual {
42    /// The observation's stats.
43    pub stats: ObserveStats,
44    /// The basis of the answer that was produced.
45    pub answer_basis: Basis,
46    /// Whether the answer was exact.
47    pub exact: bool,
48}
49
50impl ExplainActual {
51    /// The actual work as a flat JSON object with exactly the documented keys.
52    pub fn to_json(&self) -> String {
53        let s = &self.stats;
54        format!(
55            concat!(
56                "{{",
57                "\"index_nodes_read\":{},",
58                "\"seed_nodes_fetched\":{},",
59                "\"seed_nodes_materialized\":{},",
60                "\"descriptor_bytes_read\":{},",
61                "\"descriptor_read_mode\":\"{}\",",
62                "\"manifest_bytes_read\":{},",
63                "\"index_bytes_read\":{},",
64                "\"seed_bytes_read\":{},",
65                "\"bytes_read\":{},",
66                "\"bytes_returned\":{},",
67                "\"deepened\":{},",
68                "\"wall_micros\":{},",
69                "\"basis\":\"{}\",",
70                "\"exact\":{}",
71                "}}"
72            ),
73            s.index_nodes_read,
74            s.seed_nodes_fetched,
75            s.seed_nodes_materialized,
76            s.descriptor_bytes_read,
77            s.descriptor_read_mode.name(),
78            s.manifest_bytes_read,
79            s.index_bytes_read,
80            s.seed_bytes_read,
81            s.bytes_read,
82            s.bytes_returned,
83            s.deepened,
84            s.wall_micros,
85            self.answer_basis.name(),
86            self.exact,
87        )
88    }
89}
90
91/// Explain an observation without executing it. Pure.
92pub fn explain(
93    manifest: &FieldRoot,
94    store: &FieldStore,
95    req: &ObserveRequest,
96) -> Result<ExplainPlan> {
97    let plan = plan(manifest, store, req)?;
98    let json = plan_json(req, &plan);
99    Ok(ExplainPlan { plan, json })
100}
101
102/// Execute an observation and report both the intended plan and actual work.
103///
104/// The field is opened **once** here and reused for both the plan and the
105/// evaluation (`observe_with_field`), so a cold `explain --analyze` reads the
106/// descriptor blob exactly once (review fix #2). The reported `wall_micros`
107/// covers the whole analysis: open + plan + evaluation.
108pub fn explain_analyze(
109    store: &mut FieldStore,
110    id: &FieldId,
111    req: &ObserveRequest,
112    limits: Limits,
113) -> Result<(ExplainPlan, ExplainActual)> {
114    let started = std::time::Instant::now();
115    let opened = OpenedField::open(store, id, req, limits)?;
116    let planned = plan(opened.manifest(), store, req)?;
117    let json = plan_json(req, &planned);
118    let (answer, mut stats, _) = observe_opened(store, &opened, req, limits)?;
119    stats.wall_micros = started.elapsed().as_micros().min(u128::from(u64::MAX)) as u64;
120    let actual = ExplainActual {
121        stats,
122        answer_basis: answer.basis,
123        exact: answer.exact,
124    };
125    Ok((
126        ExplainPlan {
127            plan: planned,
128            json,
129        },
130        actual,
131    ))
132}
133
134fn plan_json(req: &ObserveRequest, plan: &ObservePlan) -> String {
135    format!(
136        concat!(
137            "{{",
138            "\"selector\":\"{}\",",
139            "\"representation\":\"{}\",",
140            "\"shape\":\"{}\",",
141            "\"index_reads\":{},",
142            "\"required_nodes\":{},",
143            "\"will_materialize\":{},",
144            "\"will_not_materialize\":{}",
145            "}}"
146        ),
147        json_escape(&req.selector.canonical()),
148        json_escape(req.representation.name()),
149        plan.shape.name(),
150        plan.index_reads,
151        plan.required_nodes,
152        string_array(&plan.will_materialize),
153        string_array(&plan.will_not_materialize),
154    )
155}
156
157fn string_array(items: &[String]) -> String {
158    let body = items
159        .iter()
160        .map(|s| format!("\"{}\"", json_escape(s)))
161        .collect::<Vec<_>>()
162        .join(",");
163    format!("[{body}]")
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn actual_json_has_exactly_the_fifteen_keys() {
172        let actual = ExplainActual {
173            stats: ObserveStats::default(),
174            answer_basis: Basis::DirectlyObserved,
175            exact: true,
176        };
177        assert_eq!(
178            actual.to_json(),
179            "{\"index_nodes_read\":0,\"seed_nodes_fetched\":0,\"seed_nodes_materialized\":0,\"descriptor_bytes_read\":0,\"descriptor_read_mode\":\"full\",\"manifest_bytes_read\":0,\"index_bytes_read\":0,\"seed_bytes_read\":0,\"bytes_read\":0,\"bytes_returned\":0,\"deepened\":false,\"wall_micros\":0,\"basis\":\"directly-observed\",\"exact\":true}"
180        );
181    }
182
183    #[test]
184    fn string_array_escapes() {
185        assert_eq!(
186            string_array(&["a".into(), "b\"c".into()]),
187            "[\"a\",\"b\\\"c\"]"
188        );
189        assert_eq!(string_array(&[]), "[]");
190    }
191}