Skip to main content

formal_ai/
selection.rs

1//! Issue #559 (R339): registry-driven method selection trace.
2//!
3//! The meta core resolves every atomic work-unit leaf with a *method* through a
4//! single, data-driven authority:
5//! [`MethodRegistry::method_for_route`](crate::method_registry::MethodRegistry::method_for_route)
6//! (R331/R336). The route→method alias link data (R336) lets a leaf whose route
7//! slug differs from the method name still resolve (e.g. `write_program` →
8//! `write_script`). There is no second authority: the legacy hardcoded route
9//! mapper was removed once the registry became the sole dispatch authority, so
10//! this module records *what the registry selects*, not a comparison.
11//!
12//! For every atomic leaf it records, as Links Notation, the method the registry
13//! resolves (or marks the leaf `unresolved` when no method serves it). The
14//! selection step of the algorithm is thus an inspectable white-box artifact:
15//! given a work-unit tree, the trace shows exactly which method each leaf was
16//! dispatched to.
17//!
18//! Recording is governed by [`SelectionMode`]: the default [`SelectionMode::Off`]
19//! records nothing, leaving the trace exactly as it was before this artifact
20//! existed; [`SelectionMode::Record`] emits one `selection` event (a header plus
21//! one record per leaf).
22
23use crate::event_log::EventLog;
24use crate::links_format::format_lino_record;
25use crate::meta_frame::WorkUnit;
26use crate::method_registry::MethodRegistry;
27
28/// Whether the meta core records the per-leaf method-selection artifact. The live
29/// solver always dispatches through the registry; this knob only changes trace
30/// verbosity, never routing or any answer (R13).
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum SelectionMode {
33    /// Record nothing, reproducing the pre-artifact trace exactly.
34    #[default]
35    Off,
36    /// Record the registry-resolved method for every atomic leaf.
37    Record,
38}
39
40impl SelectionMode {
41    /// Whether a selection artifact should be emitted at all.
42    #[must_use]
43    pub const fn emits_artifact(self) -> bool {
44        matches!(self, Self::Record)
45    }
46
47    /// The stable slug used in traces and config parsing.
48    #[must_use]
49    pub const fn slug(self) -> &'static str {
50        match self {
51            Self::Off => "off",
52            Self::Record => "record",
53        }
54    }
55
56    /// Parse a slug back into a mode, accepting the canonical spellings.
57    #[must_use]
58    pub fn from_slug(slug: &str) -> Option<Self> {
59        match slug.trim().to_ascii_lowercase().as_str() {
60            "off" => Some(Self::Off),
61            "record" => Some(Self::Record),
62            _ => None,
63        }
64    }
65}
66
67/// The method the registry selects for one atomic work-unit leaf.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct LeafSelection {
70    /// The leaf whose method is being selected.
71    pub unit_id: String,
72    /// Recursion depth of the leaf (0 at the root).
73    pub depth: u8,
74    /// The route slug the leaf carries, when one was recognized.
75    pub route: Option<String>,
76    /// The method the registry resolves for the leaf's route (alias-aware), or
77    /// `None` when no method serves it (an honestly blocked leaf).
78    pub method: Option<String>,
79}
80
81impl LeafSelection {
82    /// Resolve the registry method for one leaf.
83    #[must_use]
84    fn resolve(unit: &WorkUnit, registry: &MethodRegistry) -> Self {
85        let route = unit.route.clone();
86        let method = route
87            .as_deref()
88            .and_then(|route| registry.method_for_route(route))
89            .map(|method| method.name.clone());
90        Self {
91            unit_id: unit.unit_id.clone(),
92            depth: unit.depth,
93            route,
94            method,
95        }
96    }
97
98    /// Whether the registry resolved a method for this leaf.
99    #[must_use]
100    pub const fn is_resolved(&self) -> bool {
101        self.method.is_some()
102    }
103
104    /// Render one leaf's selection as a `leaf_selection` record. A resolved leaf
105    /// names its `method`; an unresolved leaf is marked `unresolved` so a blocked
106    /// leaf is recorded rather than dropped.
107    #[must_use]
108    fn to_links_notation(&self) -> String {
109        let mut pairs: Vec<(&str, String)> = vec![
110            ("record_type", "leaf_selection".to_owned()),
111            ("unit_id", self.unit_id.clone()),
112            ("depth", self.depth.to_string()),
113        ];
114        if let Some(route) = &self.route {
115            pairs.push(("route", route.clone()));
116        }
117        match &self.method {
118            Some(method) => pairs.push(("method", method.clone())),
119            None => pairs.push(("method", "unresolved".to_owned())),
120        }
121        format_lino_record(&self.unit_id, &pairs)
122    }
123}
124
125/// The per-leaf method selection across a work-unit tree.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct MethodSelection {
128    /// The root unit the selection was built for.
129    pub root_id: String,
130    /// One entry per atomic leaf, in source order.
131    pub leaves: Vec<LeafSelection>,
132}
133
134impl MethodSelection {
135    /// Build the selection for a work-unit tree, resolving the registry method for
136    /// every atomic leaf.
137    #[must_use]
138    pub fn for_unit(root: &WorkUnit, registry: &MethodRegistry) -> Self {
139        let mut leaves = Vec::new();
140        collect_leaf_selections(root, registry, &mut leaves);
141        Self {
142            root_id: root.unit_id.clone(),
143            leaves,
144        }
145    }
146
147    /// Number of atomic leaves selected.
148    #[must_use]
149    pub const fn leaf_count(&self) -> usize {
150        self.leaves.len()
151    }
152
153    /// Leaves for which the registry resolved a method.
154    #[must_use]
155    pub fn resolved_count(&self) -> usize {
156        self.leaves.iter().filter(|leaf| leaf.is_resolved()).count()
157    }
158
159    /// Leaves for which no method was resolved (honestly blocked leaves).
160    #[must_use]
161    pub fn unresolved_count(&self) -> usize {
162        self.leaf_count() - self.resolved_count()
163    }
164
165    /// Render the selection as a `selection` header plus one record per leaf.
166    #[must_use]
167    pub fn to_links_notation(&self) -> String {
168        let pairs: Vec<(&str, String)> = vec![
169            ("record_type", "selection".to_owned()),
170            ("root_id", self.root_id.clone()),
171            ("leaf_count", self.leaf_count().to_string()),
172            ("resolved_count", self.resolved_count().to_string()),
173            ("unresolved_count", self.unresolved_count().to_string()),
174        ];
175        let mut out = format_lino_record("selection", &pairs);
176        for leaf in &self.leaves {
177            out.push('\n');
178            out.push_str(&leaf.to_links_notation());
179        }
180        out
181    }
182}
183
184/// Collect a selection entry for every atomic leaf, in source order.
185fn collect_leaf_selections(
186    unit: &WorkUnit,
187    registry: &MethodRegistry,
188    leaves: &mut Vec<LeafSelection>,
189) {
190    if unit.children.is_empty() {
191        leaves.push(LeafSelection::resolve(unit, registry));
192        return;
193    }
194    for child in &unit.children {
195        collect_leaf_selections(child, registry, leaves);
196    }
197}
198
199/// Emit the per-leaf method selection as an optional trace event, gated by `mode`.
200///
201/// Returns `None` when `mode` is [`SelectionMode::Off`], so the default leaves the
202/// trace exactly as it was before this artifact existed. When emitted, it appends
203/// one `selection` event (the serialized header plus every leaf). It is pure
204/// analysis: it changes neither routing nor any answer (R13).
205pub(crate) fn record_selection(
206    log: &mut EventLog,
207    root: &WorkUnit,
208    registry: &MethodRegistry,
209    mode: SelectionMode,
210) -> Option<MethodSelection> {
211    if !mode.emits_artifact() {
212        return None;
213    }
214    let selection = MethodSelection::for_unit(root, registry);
215    log.append("selection", selection.to_links_notation());
216    Some(selection)
217}