import { readFile, writeFile, appendFile } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join, resolve } from 'node:path'; export interface UserCorrectionRecord { id: string; record_type: 'conversation'; action_type: 'user_correction'; priority: 'critical_for_recovery'; timestamp: string; original_claim: string; correction: string; principle_extracted: string; persisted_to: string[]; } const CORRECTIONS_REL = '.boo/corrections/index.json'; function correctionsDir(basePath?: string): string { return resolve(basePath ?? process.cwd(), '.boo/corrections'); } function correctionsPath(basePath?: string): string { return resolve(basePath ?? process.cwd(), CORRECTIONS_REL); } function tryParseJson(raw: string): T | null { try { return JSON.parse(raw) as T; } catch { return null; } } export interface CorrectionsIndex { corrections: UserCorrectionRecord[]; } async function readCorrections(basePath?: string): Promise { try { const raw = await readFile(correctionsPath(basePath), 'utf-8'); return tryParseJson(raw) ?? { corrections: [] }; } catch { return { corrections: [] }; } } async function writeCorrections(idx: CorrectionsIndex, basePath?: string): Promise { const dir = correctionsDir(basePath); if (!existsSync(dir)) { const { mkdir } = await import('node:fs/promises'); await mkdir(dir, { recursive: true }); } await writeFile(correctionsPath(basePath), JSON.stringify(idx, null, 2), 'utf-8'); } let idCounter = 0; function nextId(): string { idCounter++; return `uc_${Date.now()}_${idCounter}`; } function isoNow(): string { return new Date().toISOString(); } /** * Record a user correction. Stores it in .boo/corrections/index.json * and returns the record with the assigned id. */ export async function recordCorrection( originalClaim: string, correction: string, principleExtracted: string, persistedTo: string[] = [], basePath?: string, ): Promise { const idx = await readCorrections(basePath); const record: UserCorrectionRecord = { id: nextId(), record_type: 'conversation', action_type: 'user_correction', priority: 'critical_for_recovery', timestamp: isoNow(), original_claim: originalClaim, correction, principle_extracted: principleExtracted, persisted_to: persistedTo, }; idx.corrections.push(record); await writeCorrections(idx, basePath); return record; } /** * Scan an audit_trail.jsonl file for user_correction records. * Returns all matching records found in the file. */ export async function scanForCorrections( auditPath: string, ): Promise { try { const raw = await readFile(auditPath, 'utf-8'); const lines = raw.split('\n').filter(Boolean); const corrections: UserCorrectionRecord[] = []; for (const line of lines) { const record = tryParseJson>(line); if (record?.action_type === 'user_correction') { corrections.push(record as unknown as UserCorrectionRecord); } } return corrections; } catch { return []; } } /** * Check if a proposed action contradicts any known user correction. * Returns an array of contradiction warnings — empty means no contradictions. */ export function checkContradiction( action: string, corrections: UserCorrectionRecord[], ): { contradicts: boolean; warnings: { correction: UserCorrectionRecord; reason: string }[] } { const warnings: { correction: UserCorrectionRecord; reason: string }[] = []; for (const c of corrections) { // Check if the action mentions the original claim's topic const actionLower = action.toLowerCase(); const claimFragments = c.original_claim.toLowerCase().split(/\s+/).filter((w) => w.length > 4); // If any significant word from the original claim appears in the proposed action, // flag this as a potential contradiction const matchingFragments = claimFragments.filter((f) => actionLower.includes(f)); if (matchingFragments.length >= 2) { warnings.push({ correction: c, reason: `Action "${action.slice(0, 60)}" may contradict prior correction: "${c.original_claim}" → "${c.correction}" (principle: ${c.principle_extracted})`, }); } } return { contradicts: warnings.length > 0, warnings, }; } /** * Add a file path to a correction's persisted_to array. */ export async function markPersisted( correctionId: string, filePath: string, basePath?: string, ): Promise { const idx = await readCorrections(basePath); const record = idx.corrections.find((c) => c.id === correctionId); if (!record) return null; if (!record.persisted_to.includes(filePath)) { record.persisted_to.push(filePath); } await writeCorrections(idx, basePath); return record; } /** * Get all stored user corrections. */ export async function listCorrections(basePath?: string): Promise { const idx = await readCorrections(basePath); return idx.corrections; } /** * Append a correction record to an audit_trail.jsonl file (inline storage). */ export async function appendCorrectionToTrail( trailPath: string, correction: UserCorrectionRecord, ): Promise { await appendFile(trailPath, JSON.stringify(correction) + '\n', 'utf-8'); }