1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
//! Issue #559: the upward construction pass and the `recursion_mode` knob.
//!
//! The meta core's downward pass ([`crate::meta_frame::WorkUnit`]) decomposes a
//! request into a bounded work-unit tree, and the downward reasoning
//! ([`crate::meta_reasoning`], R337) explains *why* each unit was split or judged
//! atomic. Decomposition is only half of a recursive algorithm: the other half is
//! **construction** — composing the children's results back up into the parent's
//! answer, leaf to root. This module records that upward pass explicitly.
//!
//! The construction is a post-order (bottom-up) walk of the same tree: each leaf
//! is constructed directly from the method that resolves it (the base case), and
//! each parent is constructed by composing its already-constructed children in
//! source order (the recursive case), terminating at the root. Serialized to
//! Links Notation as `construction_step` records under one `upward_construction`
//! header, it makes the compositional direction as inspectable as the
//! decompositional one.
//!
//! Which directions the meta core emits is governed by [`RecursionMode`]:
//!
//! * [`RecursionMode::Down`] (the default) — emit the downward reasoning only, so
//! the trace is exactly what shipped before this knob existed (behavior-
//! preserving, R13);
//! * [`RecursionMode::Up`] — emit the upward construction only;
//! * [`RecursionMode::Both`] — emit both directions.
//!
//! The work-unit decomposition events (`work_unit:enter` / `work_unit:exit`) are
//! structural and always emitted; the knob gates only the directional *reasoning*
//! artifacts, none of which change routing or the answer.
use crate::event_log::EventLog;
use crate::links_format::format_lino_record;
use crate::meta_frame::WorkUnit;
use crate::method_registry::MethodRegistry;
/// Which direction(s) of recursive reasoning the meta core emits.
///
/// `Down` is the default and reproduces the pre-knob trace exactly, so enabling
/// the upward pass is always an explicit opt-in.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RecursionMode {
/// Emit the downward decomposition reasoning only (default, behavior-preserving).
#[default]
Down,
/// Emit the upward construction reasoning only.
Up,
/// Emit both the downward and the upward reasoning.
Both,
}
impl RecursionMode {
/// Whether the downward decomposition reasoning (R337) should be emitted.
#[must_use]
pub const fn emits_downward(self) -> bool {
matches!(self, Self::Down | Self::Both)
}
/// Whether the upward construction pass should be emitted.
#[must_use]
pub const fn emits_upward(self) -> bool {
matches!(self, Self::Up | Self::Both)
}
/// The stable slug used in traces and config parsing.
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::Down => "down",
Self::Up => "up",
Self::Both => "both",
}
}
/// Parse a slug back into a mode, accepting the canonical spellings.
#[must_use]
pub fn from_slug(slug: &str) -> Option<Self> {
match slug.trim().to_ascii_lowercase().as_str() {
"down" => Some(Self::Down),
"up" => Some(Self::Up),
"both" => Some(Self::Both),
_ => None,
}
}
}
/// How one work unit's answer is constructed during the upward pass.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstructionStep {
/// The unit whose answer this step constructs.
pub unit_id: String,
/// Recursion depth of the unit (0 at the root).
pub depth: u8,
/// Post-order position (1-based): children are always constructed before
/// their parent, and the root is last.
pub order: usize,
/// `leaf_method` (base case) or `compose` (recursive case).
pub kind: String,
/// The method whose result a leaf is constructed from, when one resolves.
pub method: Option<String>,
/// The child unit ids composed into a parent's answer, in source order.
pub inputs: Vec<String>,
/// Why the answer is constructed this way (human-readable).
pub rationale: String,
}
/// The upward construction pass: a post-order list of construction steps.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpwardConstruction {
/// The root unit the construction terminates at.
pub root_id: String,
/// One step per unit, in post-order (leaves first, root last).
pub steps: Vec<ConstructionStep>,
}
impl UpwardConstruction {
/// Build the upward construction for a work-unit tree, resolving leaf methods
/// through the same `method_for_route` bridge the evidence join uses.
#[must_use]
pub fn for_unit(root: &WorkUnit, registry: &MethodRegistry) -> Self {
let mut steps = Vec::new();
visit_post_order(root, registry, &mut steps);
Self {
root_id: root.unit_id.clone(),
steps,
}
}
/// Number of construction steps (one per unit).
#[must_use]
pub const fn step_count(&self) -> usize {
self.steps.len()
}
/// Render the construction as a header plus one record per step.
#[must_use]
pub fn to_links_notation(&self) -> String {
let header = format_lino_record(
"upward_construction",
&[
("record_type", "upward_construction".to_owned()),
("root_id", self.root_id.clone()),
("step_count", self.steps.len().to_string()),
],
);
let mut out = header;
for step in &self.steps {
out.push('\n');
out.push_str(&step.to_links_notation());
}
out
}
}
impl ConstructionStep {
/// Render one construction step as a `construction_step` record.
#[must_use]
fn to_links_notation(&self) -> String {
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "construction_step".to_owned()),
("unit_id", self.unit_id.clone()),
("depth", self.depth.to_string()),
("order", self.order.to_string()),
("kind", self.kind.clone()),
("rationale", self.rationale.clone()),
];
if let Some(method) = &self.method {
pairs.push(("method", method.clone()));
}
for input in &self.inputs {
pairs.push(("input", input.clone()));
}
format_lino_record(&self.unit_id, &pairs)
}
}
/// Append each unit's construction step in post-order (children before parent).
fn visit_post_order(unit: &WorkUnit, registry: &MethodRegistry, steps: &mut Vec<ConstructionStep>) {
for child in &unit.children {
visit_post_order(child, registry, steps);
}
let order = steps.len() + 1;
let step = if unit.children.is_empty() {
let method = unit
.route
.as_ref()
.and_then(|route| registry.method_for_route(route))
.map(|method| method.name.clone());
let rationale = method.as_ref().map_or_else(
|| {
"Base case: no method resolves this leaf, so its answer is a blocked \
marker — nothing is constructed."
.to_owned()
},
|method| {
format!(
"Base case: construct this leaf's answer directly from the result of \
method `{method}`."
)
},
);
ConstructionStep {
unit_id: unit.unit_id.clone(),
depth: unit.depth,
order,
kind: "leaf_method".to_owned(),
method,
inputs: Vec::new(),
rationale,
}
} else {
let inputs = unit
.children
.iter()
.map(|child| child.unit_id.clone())
.collect::<Vec<_>>();
let rationale = format!(
"Recursive case: compose the {} already-constructed children in source order \
into this unit's answer.",
inputs.len()
);
ConstructionStep {
unit_id: unit.unit_id.clone(),
depth: unit.depth,
order,
kind: "compose".to_owned(),
method: None,
inputs,
rationale,
}
};
steps.push(step);
}
/// Emit the upward construction pass as a trace-only event, gated by `mode`.
///
/// Returns `None` when `mode` does not request the upward direction, so the
/// default ([`RecursionMode::Down`]) leaves the trace exactly as it was before
/// this pass existed (R13). When emitted, it appends one `upward_construction`
/// event (the serialized header plus every step) and a compact
/// `upward_construction:steps`.
pub(crate) fn record_upward_construction(
log: &mut EventLog,
root: &WorkUnit,
registry: &MethodRegistry,
mode: RecursionMode,
) -> Option<UpwardConstruction> {
if !mode.emits_upward() {
return None;
}
let construction = UpwardConstruction::for_unit(root, registry);
log.append("upward_construction", construction.to_links_notation());
log.append(
"upward_construction:steps",
construction.step_count().to_string(),
);
Some(construction)
}