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