Zion Boggan zionboggan.com ↗

Synthesize concrete, correctly-attributed lesson bodies

Lessons were per-type boilerplate with an Evidence quote pulled from an
unrelated correction node (the security lesson quoted a fable-5 question).
Feed each lesson the originating failure's own evidence/summary so the
body is concrete and tied to the right node, track only originating
failure nodes as source nodes, and cap the rendered list at 8.
7b477a5   Zion Boggan committed on Jun 13, 2026 (1 week ago)
src/analyze.js +12 -9
@@ -181,7 +181,7 @@ export function analyzeTree(tree) {
if (confidence > existing.confidence) existing.confidence = confidence;
if (tierRank(tier) > tierRank(existing.tier)) existing.tier = tier;
const lr = lessonByType.get(type);
- if (lr) lr.nodeIds = uniq([...lr.nodeIds, ...ids]);
+ if (lr) lr.nodeIds = uniq([...lr.nodeIds, failureNode.id]);
const er = evalByType.get(evalTypeFor(type));
if (er) er.sourceNodeIds = uniq([...er.sourceNodeIds, ...ids]);
if (correctionNode && !existing.correctedByNodeId) existing.correctedByNodeId = correctionNode.id;
@@ -189,14 +189,14 @@ export function analyzeTree(tree) {
return existing;
}
- const lesson = lessonFor(type, correctionNode || failureNode);
+ const lesson = lessonFor(type, { evidence, summary });
let lessonRec = lessonByType.get(type);
if (!lessonRec) {
- lessonRec = { id: `lesson_${pad(lessons.length + 1)}`, title: lesson.title, nodeIds: ids, text: lesson.text };
+ lessonRec = { id: `lesson_${pad(lessons.length + 1)}`, title: lesson.title, nodeIds: [failureNode.id], text: lesson.text };
lessons.push(lessonRec);
lessonByType.set(type, lessonRec);
} else {
- lessonRec.nodeIds = uniq([...lessonRec.nodeIds, ...ids]);
+ lessonRec.nodeIds = uniq([...lessonRec.nodeIds, failureNode.id]);
}
const evalType = evalTypeFor(type);
@@ -402,7 +402,9 @@ export function renderLessonsMarkdown(tree, opts = {}) {
lines.push('');
lines.push(escapeMd(lesson.text));
lines.push('');
- lines.push(`Source nodes: ${lesson.nodeIds.join(', ')}`);
+ const ids = lesson.nodeIds;
+ const shown = ids.slice(0, 8).join(', ');
+ lines.push(`Source nodes: ${shown}${ids.length > 8 ? ` (+${ids.length - 8} more)` : ''}`);
lines.push('');
});
return lines.join('\n');
@@ -746,8 +748,7 @@ function summarizeFailure(type, failureNode, correctionNode) {
}
}
-function lessonFor(type, node) {
- const prompt = truncate(node?.text || '', 180);
+function lessonFor(type, { evidence = '', summary = '' } = {}) {
const titles = {
ignored_constraint: 'Preserve explicit constraints',
misunderstood_goal: 'Re-check the actual goal',
@@ -763,7 +764,7 @@ function lessonFor(type, node) {
user_frustration: 'Escalate when user frustration appears',
abandoned_path: 'Avoid abandoned paths unless explicitly revived',
};
- const text = {
+ const guidance = {
ignored_constraint: 'Future agents should carry explicit user constraints forward as high-priority requirements.',
misunderstood_goal: 'Future agents should restate and verify the goal before continuing after a correction.',
scope_drift: 'Future agents should preserve the corrected scope and avoid adding unrequested product shape.',
@@ -778,9 +779,11 @@ function lessonFor(type, node) {
user_frustration: 'Future agents should treat frustration as a signal to slow down, verify assumptions, and correct course.',
abandoned_path: 'Future agents should avoid resurrecting abandoned branches unless the user explicitly asks for them.',
};
+ const base = guidance[type] || 'Future agents should preserve this correction.';
+ const concrete = String(evidence || summary || '').replace(/\s+/g, ' ').trim();
return {
title: titles[type] || 'Preserve the correction',
- text: `${text[type] || 'Future agents should preserve this correction.'}${prompt ? ` Evidence: "${prompt}"` : ''}`,
+ text: concrete ? `${base} Specifically: ${truncate(concrete, 220)}` : base,
};
}