Skip to main content

lemma/planning/
explanation.rs

1//! Explanation API types built by narration after evaluation.
2//!
3//! Planning ships THE DAG (`NormalForm` nodes with optional fold `origin`).
4//! Narration ([`crate::evaluation::narration`]) reads the filled value table,
5//! follows origins so rewritten cells display their pre-image, and fills these
6//! nodes for the response.
7//!
8//! Each narrated rule is stored once in an arena and referenced by
9//! [`ExplanationNode::RuleRef`]. The public [`Explanation`] carries a `rules`
10//! table of reachable rules with compact [`ExplainedRuleId`]s.
11//!
12//! Bound data narration is `Data` with a flattened [`RuleResultValue`] (result
13//! plus typed fields). Structural mentions of paths that were never looked up
14//! are `DataUnused` (no value fields), distinct from a Missing-data veto on a
15//! visited leaf.
16
17use crate::parsing::ast::ArithmeticComputation;
18use crate::planning::semantics::{DataPath, RulePath};
19use crate::result_value::RuleResultValue;
20use serde::{Serialize, Serializer};
21
22pub(crate) fn serialize_rule_path_as_name<S>(
23    path: &RulePath,
24    serializer: S,
25) -> Result<S::Ok, S::Error>
26where
27    S: Serializer,
28{
29    serializer.serialize_str(&path.input_key())
30}
31
32pub(crate) fn serialize_data_path_as_name<S>(
33    path: &DataPath,
34    serializer: S,
35) -> Result<S::Ok, S::Error>
36where
37    S: Serializer,
38{
39    serializer.serialize_str(&path.input_key())
40}
41
42/// Compact id of a rule in an [`crate::evaluation::explanations::Explanation`]'s
43/// `rules` table. Assigned in first-reach order from the root.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
45#[serde(transparent)]
46pub struct ExplainedRuleId(pub u32);
47
48/// One narrated rule body stored in an explanation's `rules` table.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
50pub struct ExplainedRule {
51    pub id: ExplainedRuleId,
52    #[serde(serialize_with = "serialize_rule_path_as_name")]
53    pub name: RulePath,
54    #[serde(flatten)]
55    pub result: RuleResultValue,
56    pub body: String,
57    #[serde(skip_serializing_if = "Vec::is_empty")]
58    pub causes: Vec<Cause>,
59    #[serde(skip_serializing_if = "Vec::is_empty")]
60    pub children: Vec<ExplanationNode>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum ExplanationNode {
66    /// Reference to a rule in the enclosing explanation's `rules` table.
67    RuleRef {
68        id: ExplainedRuleId,
69        #[serde(serialize_with = "serialize_rule_path_as_name")]
70        name: RulePath,
71        #[serde(flatten)]
72        result: RuleResultValue,
73    },
74    Compose {
75        expression: String,
76        #[serde(skip_serializing_if = "Option::is_none")]
77        operator: Option<ArithmeticComputation>,
78        operands: Vec<ExplanationNode>,
79    },
80    /// Evaluated / bound data narration. Result fields flatten like [`crate::evaluation::response::RuleResult`].
81    Data {
82        #[serde(serialize_with = "serialize_data_path_as_name")]
83        name: DataPath,
84        #[serde(flatten)]
85        result: RuleResultValue,
86    },
87    /// Structural mention of a data path that was not looked up for this cause
88    /// (short-circuit skip or static record narration without a binding).
89    DataUnused {
90        #[serde(serialize_with = "serialize_data_path_as_name")]
91        name: DataPath,
92    },
93    Conversion {
94        expression: String,
95        steps: Vec<SerializedConversionTraceStep>,
96        operands: Vec<ExplanationNode>,
97    },
98    Veto {
99        #[serde(skip_serializing_if = "Option::is_none")]
100        message: Option<String>,
101    },
102    /// One instance-list row the enclosing command read.
103    Row {
104        number: usize,
105        #[serde(skip_serializing_if = "std::ops::Not::not")]
106        appended: bool,
107        #[serde(skip_serializing_if = "Vec::is_empty")]
108        key: Vec<ExplanationNode>,
109        fields: Vec<ExplanationNode>,
110    },
111    /// Summary of a `where` filter: how many of the input rows matched.
112    Filter {
113        condition: String,
114        matched: usize,
115        total: usize,
116        #[serde(skip_serializing_if = "Vec::is_empty")]
117        children: Vec<ExplanationNode>,
118    },
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
122pub struct Cause {
123    pub condition: String,
124    pub value: String,
125    #[serde(skip_serializing_if = "Vec::is_empty")]
126    pub children: Vec<ExplanationNode>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
130#[serde(rename_all = "snake_case")]
131pub enum ConversionTraceRole {
132    Outcome,
133    Rule,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
137pub struct SerializedConversionTraceStep {
138    pub(crate) role: ConversionTraceRole,
139    pub(crate) text: String,
140}