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//! Bound data narration is `Data` with a flattened [`RuleResultValue`] (result
9//! plus typed fields). Structural mentions of paths that were never looked up
10//! are `DataUnused` (no value fields), distinct from a Missing-data veto on a
11//! visited leaf.
12
13use crate::parsing::ast::ArithmeticComputation;
14use crate::planning::semantics::{DataPath, RulePath};
15use crate::result_value::RuleResultValue;
16use serde::{Serialize, Serializer};
17
18pub(crate) fn serialize_rule_path_as_name<S>(
19    path: &RulePath,
20    serializer: S,
21) -> Result<S::Ok, S::Error>
22where
23    S: Serializer,
24{
25    serializer.serialize_str(&path.input_key())
26}
27
28pub(crate) fn serialize_data_path_as_name<S>(
29    path: &DataPath,
30    serializer: S,
31) -> Result<S::Ok, S::Error>
32where
33    S: Serializer,
34{
35    serializer.serialize_str(&path.input_key())
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
39#[serde(tag = "type", rename_all = "snake_case")]
40pub enum ExplanationNode {
41    Rule {
42        #[serde(serialize_with = "serialize_rule_path_as_name")]
43        name: RulePath,
44        #[serde(flatten)]
45        result: RuleResultValue,
46        body: String,
47        #[serde(skip_serializing_if = "Vec::is_empty")]
48        causes: Vec<Cause>,
49        #[serde(skip_serializing_if = "Vec::is_empty")]
50        children: Vec<ExplanationNode>,
51    },
52    Compose {
53        expression: String,
54        #[serde(skip_serializing_if = "Option::is_none")]
55        operator: Option<ArithmeticComputation>,
56        operands: Vec<ExplanationNode>,
57    },
58    /// Evaluated / bound data narration. Result fields flatten like [`crate::evaluation::response::RuleResult`].
59    Data {
60        #[serde(serialize_with = "serialize_data_path_as_name")]
61        name: DataPath,
62        #[serde(flatten)]
63        result: RuleResultValue,
64    },
65    /// Structural mention of a data path that was not looked up for this cause
66    /// (short-circuit skip or static record narration without a binding).
67    DataUnused {
68        #[serde(serialize_with = "serialize_data_path_as_name")]
69        name: DataPath,
70    },
71    Conversion {
72        expression: String,
73        steps: Vec<SerializedConversionTraceStep>,
74        operands: Vec<ExplanationNode>,
75    },
76    Veto {
77        #[serde(skip_serializing_if = "Option::is_none")]
78        message: Option<String>,
79    },
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
83pub struct Cause {
84    pub condition: String,
85    pub value: String,
86    #[serde(skip_serializing_if = "Vec::is_empty")]
87    pub children: Vec<ExplanationNode>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
91#[serde(rename_all = "snake_case")]
92pub enum ConversionTraceRole {
93    Outcome,
94    Rule,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
98pub struct SerializedConversionTraceStep {
99    pub(crate) role: ConversionTraceRole,
100    pub(crate) text: String,
101}