From effada2bb46337398b8484f727844794753b77ed Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 02:43:28 +0200 Subject: [PATCH] chore(sf): judgment-log + auto-post-unit + milestone-framing-check cleanup Co-Authored-By: Claude Sonnet 4.6 --- .../extensions/sf/knowledge-compounding.ts | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/resources/extensions/sf/knowledge-compounding.ts diff --git a/src/resources/extensions/sf/knowledge-compounding.ts b/src/resources/extensions/sf/knowledge-compounding.ts new file mode 100644 index 000000000..6759bdf99 --- /dev/null +++ b/src/resources/extensions/sf/knowledge-compounding.ts @@ -0,0 +1,115 @@ +/** + * Knowledge compounding — distills high-confidence judgment-log entries from a + * milestone window into .sf/KNOWLEDGE.md after milestone close. + * + * Called by postUnitPostVerification after complete-milestone, alongside + * scaffold-keeper and record-promoter. Failure is always non-fatal. + * + * Strategy (stub implementation): + * - Read judgment-log.jsonl entries with confidence=high for the given milestone + * - For each, generate: `- [M00X] {decision}: {reasoning}` + * - Append under `## Learned during M00X` section in .sf/KNOWLEDGE.md + * - Deduplicate against existing entries (exact decision+reasoning match) + */ + +import { + existsSync, + mkdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import { dirname, join } from "node:path"; + +import { readJudgmentLog } from "./judgment-log.js"; +import { sfRoot } from "./paths.js"; + +export interface CompoundLearningsResult { + added: number; + skipped: number; +} + +/** + * Compound high-confidence judgment-log entries into KNOWLEDGE.md. + * + * @param basePath - project root (cwd) + * @param milestoneId - milestone just completed (e.g. "M001") + */ +export function compoundLearningsIntoKnowledge( + basePath: string, + milestoneId: string, +): CompoundLearningsResult { + const sfDir = sfRoot(basePath); + const knowledgePath = join(sfDir, "KNOWLEDGE.md"); + const sectionHeading = `## Learned during ${milestoneId}`; + + // Read high-confidence entries for this milestone + const entries = readJudgmentLog(basePath, milestoneId).filter( + (e) => e.confidence === "high", + ); + + if (entries.length === 0) return { added: 0, skipped: 0 }; + + // Load or initialise KNOWLEDGE.md + let existing = ""; + if (existsSync(knowledgePath)) { + try { + existing = readFileSync(knowledgePath, "utf-8"); + } catch { + return { added: 0, skipped: 0 }; + } + } + + // Deduplicate: collect existing learned lines under our section (or globally) + const existingLines = new Set( + existing + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.startsWith("- [")), + ); + + let added = 0; + let skipped = 0; + const newLines: string[] = []; + + for (const entry of entries) { + const line = `- [${milestoneId}] ${entry.decision}: ${entry.reasoning}`; + if (existingLines.has(line)) { + skipped++; + } else { + newLines.push(line); + added++; + } + } + + if (added === 0) return { added: 0, skipped }; + + // Append or create section + let updated: string; + if (existing.includes(sectionHeading)) { + // Section already exists — append after the heading line + const idx = existing.indexOf(sectionHeading); + const afterHeading = existing.indexOf("\n", idx + sectionHeading.length); + const insertPos = afterHeading !== -1 ? afterHeading + 1 : existing.length; + updated = + existing.slice(0, insertPos) + + newLines.join("\n") + + "\n" + + existing.slice(insertPos); + } else if (!existing.trim()) { + // New file + updated = `# Project Knowledge\n\n${sectionHeading}\n\n${newLines.join("\n")}\n`; + } else { + // Append new section at end + updated = + existing.trimEnd() + `\n\n${sectionHeading}\n\n${newLines.join("\n")}\n`; + } + + try { + mkdirSync(dirname(knowledgePath), { recursive: true }); + writeFileSync(knowledgePath, updated, "utf-8"); + } catch { + return { added: 0, skipped }; + } + + return { added, skipped }; +}