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
22/// List command identity on a narrated [`ExplanationNode::ListCommand`].
23///
24/// Unit variants serialize as a bare string (`"sum"`). `Sort` carries direction.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum ExplainedListOp {
28    Find,
29    First,
30    Last,
31    Count,
32    Sum,
33    Avg,
34    Product,
35    Min,
36    Max,
37    Any,
38    All,
39    Where,
40    Sort { descending: bool },
41    Top,
42    With,
43    Key,
44}
45
46pub(crate) fn serialize_rule_path_as_name<S>(
47    path: &RulePath,
48    serializer: S,
49) -> Result<S::Ok, S::Error>
50where
51    S: Serializer,
52{
53    serializer.serialize_str(&path.input_key())
54}
55
56pub(crate) fn serialize_data_path_as_name<S>(
57    path: &DataPath,
58    serializer: S,
59) -> Result<S::Ok, S::Error>
60where
61    S: Serializer,
62{
63    serializer.serialize_str(&path.input_key())
64}
65
66/// Compact id of a rule in an [`crate::evaluation::explanations::Explanation`]'s
67/// `rules` table. Assigned in first-reach order from the root.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
69#[serde(transparent)]
70pub struct ExplainedRuleId(pub u32);
71
72/// One narrated rule body stored in an explanation's `rules` table.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
74pub struct ExplainedRule {
75    pub id: ExplainedRuleId,
76    #[serde(serialize_with = "serialize_rule_path_as_name")]
77    pub name: RulePath,
78    #[serde(flatten)]
79    pub result: RuleResultValue,
80    pub body: String,
81    #[serde(skip_serializing_if = "Vec::is_empty")]
82    pub causes: Vec<Cause>,
83    #[serde(skip_serializing_if = "Vec::is_empty")]
84    pub children: Vec<ExplanationNode>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
88#[serde(tag = "type", rename_all = "snake_case")]
89pub enum ExplanationNode {
90    /// Reference to a rule in the enclosing explanation's `rules` table.
91    RuleRef {
92        id: ExplainedRuleId,
93        #[serde(serialize_with = "serialize_rule_path_as_name")]
94        name: RulePath,
95        #[serde(flatten)]
96        result: RuleResultValue,
97    },
98    Compose {
99        expression: String,
100        #[serde(skip_serializing_if = "Option::is_none")]
101        operator: Option<ArithmeticComputation>,
102        operands: Vec<ExplanationNode>,
103        #[serde(flatten)]
104        result: RuleResultValue,
105    },
106    /// One NormalForm list command cell (`menu -> sum price`).
107    ListCommand {
108        expression: String,
109        /// Command fragment after `->`, from Syntax (not a string split of `expression`).
110        stage: String,
111        op: ExplainedListOp,
112        operands: Vec<ExplanationNode>,
113        #[serde(flatten)]
114        result: RuleResultValue,
115    },
116    /// Evaluated / bound data narration. Result fields flatten like [`crate::evaluation::response::RuleResult`].
117    Data {
118        #[serde(serialize_with = "serialize_data_path_as_name")]
119        name: DataPath,
120        #[serde(flatten)]
121        result: RuleResultValue,
122    },
123    /// Structural mention of a data path that was not looked up for this cause
124    /// (short-circuit skip or static record narration without a binding).
125    DataUnused {
126        #[serde(serialize_with = "serialize_data_path_as_name")]
127        name: DataPath,
128    },
129    Conversion {
130        expression: String,
131        steps: Vec<SerializedConversionTraceStep>,
132        operands: Vec<ExplanationNode>,
133        #[serde(flatten)]
134        result: RuleResultValue,
135    },
136    Veto {
137        #[serde(skip_serializing_if = "Option::is_none")]
138        message: Option<String>,
139    },
140    /// One instance-list row the enclosing command read.
141    Row {
142        number: usize,
143        #[serde(skip_serializing_if = "std::ops::Not::not")]
144        appended: bool,
145        #[serde(skip_serializing_if = "Vec::is_empty")]
146        key: Vec<ExplanationNode>,
147        fields: Vec<ExplanationNode>,
148    },
149    /// Summary of a `where` filter: how many of the input rows matched.
150    Filter {
151        condition: String,
152        matched: usize,
153        total: usize,
154        #[serde(skip_serializing_if = "Vec::is_empty")]
155        children: Vec<ExplanationNode>,
156    },
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
160pub struct Cause {
161    pub condition: String,
162    pub value: String,
163    #[serde(skip_serializing_if = "Vec::is_empty")]
164    pub children: Vec<ExplanationNode>,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
168#[serde(rename_all = "snake_case")]
169pub enum ConversionTraceRole {
170    Outcome,
171    Rule,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
175pub struct SerializedConversionTraceStep {
176    pub(crate) role: ConversionTraceRole,
177    pub(crate) text: String,
178}