chore(sf): judgment-log + auto-post-unit + milestone-framing-check cleanup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-02 02:43:28 +02:00
parent 070c0eb802
commit effada2bb4

View file

@ -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 };
}