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
//! Issue #559 (R334): the evidence pipeline — the unifying audit projection.
//!
//! The meta core records several link artifacts as it works: the problem frame
//! (every detected need), the recursive work-unit tree (the decomposition), the
//! need-satisfaction ledger (one status per need), and the method registry (the
//! catalogue of resolving handlers). On their own each answers a different
//! question. The evidence pipeline *joins* them into one coherent record: for
//! every need it traces the full chain `frame need → work-unit leaf → ledger
//! status → catalogued method`, so "ensure every detected need is addressed"
//! becomes an end-to-end auditable fact rather than four separate projections a
//! reader must reconcile by hand.
//!
//! This projection reads the artifacts the loop already produced and emits the
//! `solution_evidence` audit event. Runtime method selection happens through the
//! same registry resolver, while this module stays focused on the evidence join.
use crate::event_log::EventLog;
use crate::links_format::format_lino_record;
use crate::meta_frame::{NeedLedger, NeedStatus, ProblemFrame};
use crate::method_registry::MethodRegistry;
/// One need's full evidence chain through the meta core.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvidenceTrail {
/// The need this trail accounts for.
pub need_id: String,
/// The need's source span (provenance).
pub source_span: String,
/// The work-unit leaf that resolves the need, when the decomposition reached
/// one — the link back to the recursive downward pass.
pub work_unit_id: Option<String>,
/// The need's resolved status from the ledger.
pub status: NeedStatus,
/// The route the resolving leaf carries, when matched.
pub route: Option<String>,
/// The catalogued method this trail resolves to, when the route names a
/// registered handler (directly or via a route→method alias) — the link to
/// the method registry.
pub method: Option<String>,
/// Whether the method was reached through a route→method alias rather than a
/// direct name match (provenance for the resolution, R334).
pub method_via_alias: bool,
/// Whether the chain is connected end to end: a leaf was reached and the need
/// carries an explicit, non-pending status.
pub connected: bool,
}
impl EvidenceTrail {
#[must_use]
fn to_links_notation(&self) -> String {
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "evidence_trail".to_owned()),
("need_id", self.need_id.clone()),
("source_span", self.source_span.clone()),
("status", self.status.slug().to_owned()),
("connected", self.connected.to_string()),
];
if let Some(unit_id) = &self.work_unit_id {
pairs.push(("work_unit", unit_id.clone()));
}
if let Some(route) = &self.route {
pairs.push(("route", route.clone()));
}
if let Some(method) = &self.method {
pairs.push(("method", method.clone()));
if self.method_via_alias {
pairs.push(("method_via_alias", "true".to_owned()));
}
}
format_lino_record(&self.need_id, &pairs)
}
}
/// The end-to-end evidence that the solve addressed every detected need.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SolutionEvidence {
/// The frame this evidence audits.
pub frame_id: String,
/// One trail per detected need, in frame order.
pub trails: Vec<EvidenceTrail>,
}
impl SolutionEvidence {
/// Assemble the evidence by joining the frame's needs (via the ledger) to the
/// work-unit leaves that resolve them and the catalogued methods they route
/// to.
#[must_use]
pub fn assemble(frame: &ProblemFrame, ledger: &NeedLedger, registry: &MethodRegistry) -> Self {
let trails = ledger
.rows
.iter()
.map(|row| {
let resolved = row
.route
.as_ref()
.and_then(|route| registry.method_for_route(route));
let method = resolved.map(|method| method.name.clone());
// The method was reached via an alias when the resolved name
// differs from the route slug that produced it.
let method_via_alias = match (&row.route, &method) {
(Some(route), Some(name)) => route != name,
_ => false,
};
let connected = row.unit_id.is_some() && row.status != NeedStatus::Pending;
EvidenceTrail {
need_id: row.need_id.clone(),
source_span: row.source_span.clone(),
work_unit_id: row.unit_id.clone(),
status: row.status,
route: row.route.clone(),
method,
method_via_alias,
connected,
}
})
.collect();
Self {
frame_id: frame.frame_id.clone(),
trails,
}
}
/// Every need has a connected chain and a non-pending status — the structural
/// form of "every detected need is accounted for in the response".
#[must_use]
pub fn accounted_for(&self) -> bool {
!self.trails.is_empty() && self.trails.iter().all(|trail| trail.connected)
}
/// Every need is `Satisfied` by a connected chain — a stronger guarantee than
/// merely accounted-for (which also counts an explicitly blocked need).
#[must_use]
pub fn fully_resolved(&self) -> bool {
!self.trails.is_empty()
&& self
.trails
.iter()
.all(|trail| trail.connected && trail.status == NeedStatus::Satisfied)
}
/// Number of trails that resolve to a catalogued method.
#[must_use]
pub fn resolved_to_method(&self) -> usize {
self.trails
.iter()
.filter(|trail| trail.method.is_some())
.count()
}
/// Render the evidence and every trail as Links Notation records (R311).
#[must_use]
pub fn to_links_notation(&self) -> String {
let mut pairs: Vec<(&str, String)> = vec![
("record_type", "solution_evidence".to_owned()),
("frame_id", self.frame_id.clone()),
("trail_count", self.trails.len().to_string()),
("accounted_for", self.accounted_for().to_string()),
("fully_resolved", self.fully_resolved().to_string()),
("resolved_to_method", self.resolved_to_method().to_string()),
];
for trail in &self.trails {
pairs.push(("trail", trail.need_id.clone()));
}
let mut out = format_lino_record("solution_evidence", &pairs);
for trail in &self.trails {
out.push('\n');
out.push_str(&trail.to_links_notation());
}
out
}
}
/// Assemble the solution evidence and emit it as a loop event plus its Links
/// Notation trace.
///
/// Append one `solution_evidence` event (the serialized chain, which enumerates
/// every trail) and a compact `solution_evidence:accounted_for`, so the
/// end-to-end audit is observable in the event log.
pub(crate) fn record_solution_evidence(
log: &mut EventLog,
frame: &ProblemFrame,
ledger: &NeedLedger,
registry: &MethodRegistry,
) -> SolutionEvidence {
let evidence = SolutionEvidence::assemble(frame, ledger, registry);
log.append("solution_evidence", evidence.to_links_notation());
log.append(
"solution_evidence:accounted_for",
evidence.accounted_for().to_string(),
);
evidence
}