//! EXPLAIN and EXPLAIN ANALYZE for field observations (Phase 11.5, ADR-0026).
//!
//! [`explain`] is a pure function of validated metadata: it never materializes a
//! node and never writes to the store. [`explain_analyze`] executes the
//! observation and returns both the intended plan and the measured actual work,
//! which is the central evidence surface for Phase-11 claims (ADR-0027).
//!
//! The JSON key sets are frozen and exact:
//!
//! * plan — `selector`, `representation`, `format`, `adapter`, `capability`,
//! `index_route`, `shape`, `index_reads`, `required_nodes`,
//! `will_materialize`, `will_not_materialize`.
//! * actual — `format`, `adapter`, `index_nodes_read`, `seed_nodes_fetched`,
//! `seed_nodes_materialized`, `member_decodes`, `xml_parses`,
//! `nodes_id_shared`, `shared_resource_ids`, `inverse_work_units`,
//! `descriptor_bytes_read`, `descriptor_read_mode`, `manifest_bytes_read`,
//! `index_bytes_read`, `seed_bytes_read`, `bytes_read`, `bytes_returned`,
//! `deepened`, `whole_source_materialized`, `wall_micros`, `basis`, `exact`.
//!
//! `format`/`adapter` are the detected format and its adapter; `capability` is the
//! resolved path (`native`, or `common:<selector> -> <adapter>`); `index_route` is
//! `hier-index`/`none`. `member_decodes`/`xml_parses` are observation-boundary
//! materialization requests (see [`ObserveStats`]); `whole_source_materialized` is
//! yes exactly when the answer verified whole-source integrity.
//!
//! `bytes_read` is the **sum** of the four `*_bytes_read` classes: total physical
//! bytes this observation made the OS fetch, including the descriptor blob. It is
//! deliberately not the seed-store-only number it was before Phase 11.9's
//! review (ADR-0027 accounting).
use crate::error::Result;
use crate::field::document_format::DocumentFormat;
use crate::field::manifest::FieldRoot;
use crate::field::observe::{ObserveRequest, ObserveStats, OpenedField, observe_opened};
use crate::field::plan::{ObservePlan, plan};
use crate::field::provenance::{Basis, IntegrityScope, json_escape};
use crate::field::{FieldId, FieldStore};
use crate::limits::Limits;
/// The intended plan plus its canonical JSON rendering.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplainPlan {
/// The structured plan.
pub plan: ObservePlan,
/// The plan as a flat JSON object with exactly the documented keys.
pub json: String,
}
/// The measured cost of an executed observation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplainActual {
/// The observation's stats.
pub stats: ObserveStats,
/// The basis of the answer that was produced.
pub answer_basis: Basis,
/// Whether the answer was exact.
pub exact: bool,
/// The detected document format (`"unknown"` when the manifest predates it).
pub format: String,
/// The adapter that served the answer.
pub adapter: String,
/// Whether the whole source was materialized (whole-source integrity checked).
pub whole_source_materialized: bool,
}
impl ExplainActual {
/// The actual work as a flat JSON object with exactly the documented keys.
pub fn to_json(&self) -> String {
let s = &self.stats;
format!(
concat!(
"{{",
"\"format\":\"{}\",",
"\"adapter\":\"{}\",",
"\"index_nodes_read\":{},",
"\"seed_nodes_fetched\":{},",
"\"seed_nodes_materialized\":{},",
"\"member_decodes\":{},",
"\"xml_parses\":{},",
"\"nodes_id_shared\":{},",
"\"shared_resource_ids\":{},",
"\"inverse_work_units\":{},",
"\"descriptor_bytes_read\":{},",
"\"descriptor_read_mode\":\"{}\",",
"\"manifest_bytes_read\":{},",
"\"index_bytes_read\":{},",
"\"seed_bytes_read\":{},",
"\"bytes_read\":{},",
"\"bytes_returned\":{},",
"\"deepened\":{},",
"\"whole_source_materialized\":{},",
"\"wall_micros\":{},",
"\"basis\":\"{}\",",
"\"exact\":{}",
"}}"
),
json_escape(&self.format),
json_escape(&self.adapter),
s.index_nodes_read,
s.seed_nodes_fetched,
s.seed_nodes_materialized,
s.member_decodes,
s.xml_parses,
s.nodes_id_shared,
s.shared_resource_ids,
s.seed_nodes_executed.saturating_add(s.bytes_read),
s.descriptor_bytes_read,
s.descriptor_read_mode.name(),
s.manifest_bytes_read,
s.index_bytes_read,
s.seed_bytes_read,
s.bytes_read,
s.bytes_returned,
s.deepened,
self.whole_source_materialized,
s.wall_micros,
self.answer_basis.name(),
self.exact,
)
}
}
/// Explain an observation without executing it. Pure.
pub fn explain(
manifest: &FieldRoot,
store: &FieldStore,
req: &ObserveRequest,
) -> Result<ExplainPlan> {
let plan = plan(manifest, store, req)?;
let json = plan_json(req, &plan, manifest);
Ok(ExplainPlan { plan, json })
}
/// Execute an observation and report both the intended plan and actual work.
///
/// The field is opened **once** here and reused for both the plan and the
/// evaluation (`observe_with_field`), so a cold `explain --analyze` reads the
/// descriptor blob exactly once (review fix #2). The reported `wall_micros`
/// covers the whole analysis: open + plan + evaluation.
pub fn explain_analyze(
store: &mut FieldStore,
id: &FieldId,
req: &ObserveRequest,
limits: Limits,
) -> Result<(ExplainPlan, ExplainActual)> {
let started = std::time::Instant::now();
let opened = OpenedField::open(store, id, req, limits)?;
let planned = plan(opened.manifest(), store, req)?;
let json = plan_json(req, &planned, opened.manifest());
let (answer, mut stats, _) = observe_opened(store, &opened, req, limits)?;
stats.wall_micros = started.elapsed().as_micros().min(u128::from(u64::MAX)) as u64;
let fmt = DocumentFormat::from_provenance(&opened.manifest().provenance);
let actual = ExplainActual {
stats,
answer_basis: answer.basis,
exact: answer.exact,
format: fmt.map_or("unknown", |f| f.name()).to_string(),
adapter: fmt.map_or("unknown", |f| f.adapter()).to_string(),
whole_source_materialized: answer.integrity_scope == IntegrityScope::WholeSource,
};
Ok((
ExplainPlan {
plan: planned,
json,
},
actual,
))
}
fn plan_json(req: &ObserveRequest, plan: &ObservePlan, manifest: &FieldRoot) -> String {
let fmt = DocumentFormat::from_provenance(&manifest.provenance);
let format_name = fmt.map_or("unknown", |f| f.name());
let adapter = fmt.map_or("unknown", |f| f.adapter());
let capability = match crate::field::capabilities::common_selector_name(&req.selector) {
Some(name) => format!("common:{name} -> {adapter}"),
None => "native".to_string(),
};
let index_route = if manifest.has_index() {
"hier-index"
} else {
"none"
};
format!(
concat!(
"{{",
"\"selector\":\"{}\",",
"\"representation\":\"{}\",",
"\"format\":\"{}\",",
"\"adapter\":\"{}\",",
"\"capability\":\"{}\",",
"\"index_route\":\"{}\",",
"\"shape\":\"{}\",",
"\"index_reads\":{},",
"\"required_nodes\":{},",
"\"will_materialize\":{},",
"\"will_not_materialize\":{}",
"}}"
),
json_escape(&req.selector.canonical()),
json_escape(req.representation.name()),
json_escape(format_name),
json_escape(adapter),
json_escape(&capability),
index_route,
plan.shape.name(),
plan.index_reads,
plan.required_nodes,
string_array(&plan.will_materialize),
string_array(&plan.will_not_materialize),
)
}
fn string_array(items: &[String]) -> String {
let body = items
.iter()
.map(|s| format!("\"{}\"", json_escape(s)))
.collect::<Vec<_>>()
.join(",");
format!("[{body}]")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn actual_json_has_exactly_the_documented_keys() {
let actual = ExplainActual {
stats: ObserveStats::default(),
answer_basis: Basis::DirectlyObserved,
exact: true,
format: "unknown".to_string(),
adapter: "unknown".to_string(),
whole_source_materialized: false,
};
assert_eq!(
actual.to_json(),
"{\"format\":\"unknown\",\"adapter\":\"unknown\",\"index_nodes_read\":0,\"seed_nodes_fetched\":0,\"seed_nodes_materialized\":0,\"member_decodes\":0,\"xml_parses\":0,\"nodes_id_shared\":0,\"shared_resource_ids\":0,\"inverse_work_units\":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,\"whole_source_materialized\":false,\"wall_micros\":0,\"basis\":\"directly-observed\",\"exact\":true}"
);
}
#[test]
fn string_array_escapes() {
assert_eq!(
string_array(&["a".into(), "b\"c".into()]),
"[\"a\",\"b\\\"c\"]"
);
assert_eq!(string_array(&[]), "[]");
}
}