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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
//! The closed self-healing / auto-learning loop (issue #558).
//!
//! Issue #558 ("Auto learning") asks for a meta-algorithm that, when it *cannot
//! answer an input*, reasons about the failure in its own trace, maps it back onto
//! the source that would have to change, proposes a fix, tests the new version
//! against an acceptance gate, and — only with human approval — promotes the
//! lesson so it generalises. The prior slices of this vision already exist but are
//! disconnected: [`crate::self_improvement`] turns unknown traces into gated rule
//! proposals, [`crate::agentic_coding::self_ast`] translates the system's own Rust
//! source to the links/meta language *and back* (round-trip), and
//! [`crate::self_improvement::BenchmarkGateReport`] is the acceptance gate.
//!
//! This module closes the loop by composing those pieces into a single, auditable
//! [`RepairCase`]: one failure the system could not answer → the source it maps
//! onto (with a verified source↔links round-trip) → the candidate lesson learned
//! from the trace → the benchmark gate → a human-review outcome. Crucially, it is
//! **proposal-only and human-gated**, exactly like the pieces it composes: a
//! `RepairCase` is a record for review, never an automatic write-back to source or
//! seed data. The "recompile itself and reattach to the UI" guardrail from the
//! issue is honoured by keeping every step observable, testable, reversible, and
//! approved by a human before anything is adopted.
use std::fmt::Write as _;
use crate::agentic_coding::self_ast;
use crate::engine::stable_id;
use crate::event_log::EventLog;
use crate::self_improvement::{
learn_rules_from_unknown_traces, BenchmarkGateReport, LearningRun, UnknownTrace,
};
/// The verified source ↔ links round-trip for the module a repair case maps onto.
///
/// This is the concrete realisation of issue #558's "translate the source code to
/// the meta language and back": before proposing any change, the loop proves the
/// located source can be represented losslessly as the links/meta language and
/// reconstructed byte-for-byte, so an edit could in principle be expressed against
/// the links and rendered back to compilable Rust.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceRoundTrip {
/// The repository-relative path of the module that was mapped and round-tripped.
pub module_path: String,
/// Total links in the lossless network (a size signal for the mapped source).
pub total_link_count: usize,
/// Named abstract-syntax nodes recovered from the parse (the AST proper).
pub named_node_count: usize,
/// Whether `source → links → source` reproduced the input byte-for-byte.
pub faithful: bool,
}
impl SourceRoundTrip {
/// Map a failure onto `source` (identified by `module_path`) and verify the
/// source↔links round-trip through the sole CST/AST engine in this repo.
#[must_use]
pub fn for_module(module_path: impl Into<String>, source: &str) -> Self {
// A single parse yields both the census and the round-trip verdict:
// `AstCensus::text_preserved` *is* `reconstruct_text() == source`, i.e. the
// source → links → source round-trip, so there is no need to parse twice.
let census = self_ast::ast_census(source);
Self {
module_path: module_path.into(),
total_link_count: census.total_link_count,
named_node_count: census.named_node_count,
faithful: census.text_preserved,
}
}
/// Map a failure onto the pinned self-inspection target (the deterministic
/// planner — the module that routes inputs, so the natural site for a routing
/// repair). Uses the source embedded at build time.
#[must_use]
pub fn for_pinned_target() -> Self {
Self::for_module(self_ast::TARGET_MODULE_PATH, self_ast::target_source())
}
}
/// Where a repair case stands after the loop reasons about it — always short of an
/// automatic change, so the terminal step is a human decision.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepairOutcome {
/// A candidate lesson was synthesised and the benchmark gate permits adoption;
/// the case is ready for a human to approve promotion. Nothing is written yet.
AwaitingReview,
/// A candidate lesson was synthesised but the benchmark gate did not pass, so
/// adoption is blocked pending a green gate.
BlockedByBenchmark,
/// No candidate lesson could be synthesised from the failure trace; the case is
/// recorded for triage but proposes nothing.
NoCandidate,
}
impl RepairOutcome {
/// The stable slug used in serialization.
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::AwaitingReview => "awaiting_review",
Self::BlockedByBenchmark => "blocked_by_benchmark",
Self::NoCandidate => "no_candidate",
}
}
/// Whether the loop produced something a human could choose to promote.
#[must_use]
pub const fn has_reviewable_proposal(self) -> bool {
matches!(self, Self::AwaitingReview | Self::BlockedByBenchmark)
}
}
/// One closed pass of the self-healing loop over a single failure.
///
/// A `RepairCase` unifies the four stages the issue calls for — the failure, the
/// source it maps onto (round-tripped), the candidate lesson, and the acceptance
/// gate — into one auditable artifact. It never applies anything: adoption is a
/// human review step, so the case's terminal [`RepairOutcome`] is at most
/// `AwaitingReview`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepairCase {
/// Stable content-addressed id for the case.
pub id: String,
/// The original input the system could not answer.
pub failure_prompt: String,
/// The captured failure trace the loop reasons about.
pub trace: UnknownTrace,
/// The source the failure maps onto, with its verified source↔links round-trip.
pub source_round_trip: SourceRoundTrip,
/// The gated learning run that turns the trace into candidate lesson(s).
pub learning: LearningRun,
/// Where the case stands after reasoning — never past `AwaitingReview`.
pub outcome: RepairOutcome,
}
impl RepairCase {
/// Run one self-healing pass: reason about `trace`, map it onto `source_round_trip`,
/// learn a gated lesson under `gate`, and record the review outcome.
#[must_use]
pub fn from_trace(
trace: UnknownTrace,
source_round_trip: SourceRoundTrip,
gate: BenchmarkGateReport,
) -> Self {
let learning = learn_rules_from_unknown_traces(std::slice::from_ref(&trace), gate);
let outcome = if !learning.adoptable_rules().is_empty() {
RepairOutcome::AwaitingReview
} else if !learning.proposals.is_empty() {
RepairOutcome::BlockedByBenchmark
} else {
RepairOutcome::NoCandidate
};
let id = stable_id(
"repair_case",
&format!(
"{}:{}:{}",
trace.id, source_round_trip.module_path, learning.id
),
);
Self {
id,
failure_prompt: trace.prompt.clone(),
trace,
source_round_trip,
learning,
outcome,
}
}
/// Whether adoption of this case's lesson stays a human decision. Always `true`:
/// the loop is proposal-only by construction, mirroring [`crate::self_improvement`]
/// and [`crate::meta_self_improvement`].
#[must_use]
pub const fn is_human_gated(&self) -> bool {
true
}
/// A one-line human-readable summary of the case.
#[must_use]
pub fn summary(&self) -> String {
let round_trip = if self.source_round_trip.faithful {
"round-trips"
} else {
"does not round-trip"
};
match self.outcome {
RepairOutcome::AwaitingReview => format!(
"Failure `{}` maps onto {} (source {round_trip}); {} lesson(s) await human approval.",
self.failure_prompt,
self.source_round_trip.module_path,
self.learning.adoptable_rules().len()
),
RepairOutcome::BlockedByBenchmark => format!(
"Failure `{}` maps onto {} (source {round_trip}); a lesson was learned but the benchmark gate blocks adoption.",
self.failure_prompt, self.source_round_trip.module_path
),
RepairOutcome::NoCandidate => format!(
"Failure `{}` maps onto {} (source {round_trip}); no lesson could be synthesised — recorded for triage.",
self.failure_prompt, self.source_round_trip.module_path
),
}
}
/// Render the whole repair case as Links Notation — the auditable artifact a
/// human reviews before any promotion. Ends trimmed of trailing whitespace.
#[must_use]
pub fn links_notation(&self) -> String {
let mut out = String::from("repair_case\n");
field(&mut out, "id", &self.id);
field(&mut out, "failure_prompt", &self.failure_prompt);
field(&mut out, "outcome", self.outcome.slug());
field(&mut out, "human_gated", "true");
out.push_str(" source_round_trip\n");
nested(&mut out, "module_path", &self.source_round_trip.module_path);
nested(
&mut out,
"total_link_count",
&self.source_round_trip.total_link_count.to_string(),
);
nested(
&mut out,
"named_node_count",
&self.source_round_trip.named_node_count.to_string(),
);
nested(
&mut out,
"faithful",
&self.source_round_trip.faithful.to_string(),
);
// Fold the composed sub-artifacts in as indented blocks so the whole case is
// one reviewable document, then trim to a single logical record.
push_indented(&mut out, "failure_trace", &self.trace.links_notation());
push_indented(&mut out, "learning_run", &self.learning.links_notation());
out.trim_end().to_owned()
}
}
/// A deterministic, canonical failure trace for the self-healing recipe.
///
/// It captures an input the system could not answer directly (a reverse-sort
/// program modifier), together with the rule-synthesis candidate and *passed*
/// verification the loop reasons about. Kept in sync with the real events
/// [`crate::rule_synthesis`] emits.
#[must_use]
pub fn canonical_failure_trace() -> UnknownTrace {
let mut log = EventLog::new();
log.append(
"selected_rule",
"initial unknown reason no_seed_route next try_rule_synthesis",
);
log.append(
"rule_synthesis_candidate",
"rule_synthesis_candidate\n id reverse_sort_list_files\n source constructed_from_operation_vocabulary\n base_task list_files\n modifier reverse_sort\n operation sort\n operation_modifier descending\n target program:last.output_order\n resolved_task list_files_reverse_sort",
);
log.append(
"rule_verification",
"rule_verification\n candidate reverse_sort_list_files\n fixture list_files_output_order\n input a.txt,b.txt,c.txt\n expected_order c.txt,b.txt,a.txt\n lowering_check passed\n render_check passed\n status passed",
);
UnknownTrace::from_event_log(
"List the files but sort the results in reverse order",
"write_program",
&log,
)
.expect("canonical unknown-path trace should accumulate")
}
/// Build the canonical, fully-worked self-healing case.
///
/// The canonical failure is mapped onto the pinned planner target and gated by a
/// passing issue-#362 benchmark, so the outcome is `AwaitingReview` (a lesson
/// ready for human approval). Deterministic and self-contained — used by the
/// agentic recipe, the committed `data/meta/self-healing-case.lino` artifact, and
/// the tests.
#[must_use]
pub fn canonical_case() -> RepairCase {
RepairCase::from_trace(
canonical_failure_trace(),
SourceRoundTrip::for_pinned_target(),
BenchmarkGateReport::issue_362_from_counts(4, 0),
)
}
fn field(out: &mut String, key: &str, value: &str) {
let _ = writeln!(out, " {key} \"{}\"", quote(value));
}
fn nested(out: &mut String, key: &str, value: &str) {
let _ = writeln!(out, " {key} \"{}\"", quote(value));
}
/// Fold a composed sub-record in *under* `label`: the label sits at the case's
/// child indent (two spaces) and every line of the sub-record is pushed two indents
/// deeper (four spaces), so the sub-record nests beneath the label and the combined
/// document stays a single well-formed Links Notation tree.
fn push_indented(out: &mut String, label: &str, block: &str) {
let _ = writeln!(out, " {label}");
for line in block.lines() {
if line.is_empty() {
out.push('\n');
} else {
let _ = writeln!(out, " {line}");
}
}
}
fn quote(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "'")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}