import { existsSync } from 'fs'; import { readFile, writeFile, mkdir, readdir } from 'fs/promises'; import { join, dirname } from 'path'; import { DatasetLoader } from '../data/loader'; import { PersistableInMemoryMemory } from '../storage'; import type { LongMemEvalQuestion, FailureCategory } from '../data/types'; // Re-export FailureCategory for backwards compatibility export type { FailureCategory } from '../data/types'; export interface QuestionProgress { status: 'pending' | 'investigated' | 'fix-implemented' | 'synced'; category?: FailureCategory; investigatedAt?: string; } export interface InvestigationProgress { runId: string; config: string; dataset: string; createdAt: string; totalFailed: number; investigated: number; questions: Record; } export interface InvestigateOptions { runId?: string; list?: boolean; config?: string; status?: boolean; next?: boolean; done?: string; sync?: boolean; fixed?: string; // Mark a question as fix-implemented outputDir?: string; resultsDir?: string; preparedDataDir?: string; datasetDir?: string; editor?: string; // Investigation utilities search?: string; // Search observations for keywords trace?: string; // Trace information flow for a keyword questionId?: string; // Question ID for search/trace/date/session inspect?: string; // Inspect a specific question's data date?: string; // View observations around a specific date (e.g., "2023/05/29" or "May 29") context?: number; // Number of days of context around the date (default: 1) session?: number; // View a specific session from original dataset listSessions?: boolean; // List all sessions with dates for a question // Data freshness detection checkStale?: boolean; // Check if prepared data is stale (pre-cursor-fix) staleOnly?: boolean; // Only list stale questions from failures // Original dataset search searchOriginal?: string; // Search original dataset for a keyword // Improve question/answer in dataset improve?: string; // Question ID to add improvements for improveQuestion?: string; // Improved question text improveAnswer?: string; // Improved answer text improveNote?: string; // Improvement note category?: FailureCategory; // Failure category for this question clearImproved?: string | boolean; // Clear specific field(s): 'all', 'question', 'answer', 'note', 'category' or true for all // Check for duplicate observations checkDuplicates?: boolean; // Check for duplicate thread blocks in observations // Baseline check - comprehensive data quality check baseline?: boolean; // Run all data quality checks for a question // Prepare stale questions prepareStale?: boolean; // Find and prepare all stale pending questions (deprecated, use --print-prepare-command) dryRun?: boolean; // Just show what would be prepared, don't run printPrepareCommand?: boolean; // Print a prepare command for stale questions } interface FailuresFile { questionIds: string[]; runId: string; config: string; dataset: string; timestamp: string; totalFailed: number; totalQuestions: number; } interface EvaluationResult { question_id: string; question: string; expected_answer: string; hypothesis: string; // The agent's answer is_correct: boolean; question_type: string; improved_question?: string; improved_answer?: string; improved_is_correct?: boolean; has_improvement_info?: boolean; improved_regression?: boolean; } // ============================================================================ // Analysis Template // ============================================================================ function generateAnalysisTemplate( questionId: string, question: string, expectedAnswer: string, actualAnswer: string, questionType: string, improvedQuestion?: string, improvedAnswer?: string, ): string { return `# Investigation: ${questionId} ## Question Type ${questionType} ## Question ${question} ## Expected Answer ${expectedAnswer} ## Agent Answer ${actualAnswer} ${improvedQuestion ? `## Improved Question (existing)\n${improvedQuestion}\n` : ''} ${improvedAnswer ? `## Improved Answer (existing)\n${improvedAnswer}\n` : ''} --- ## Failure Category - [ ] Observer missed critical information - [ ] Reflector lost/merged information incorrectly - [ ] Agent reasoning error (had info, wrong conclusion) - [ ] Ambiguous/poorly-worded question - [ ] Dataset inconsistency/error - [ ] RAG retrieval miss (if applicable) - [ ] Other: ___ ## Root Cause Analysis ## Evidence --- ## Potential Improvements ### Observer/Reflector Changes - **Likelihood**: Low / Medium / High - **Reasoning**: - **Suggested prompt change**: ### Fixed Question/Answer - **Likelihood**: Low / Medium / High - **improved_question**: - **improved_answer**: - **improvement_note**: ### Other Improvements --- ## Status - [ ] Investigated - [ ] Fix implemented - [ ] Synced to longmemeval_s.json `; } // ============================================================================ // Investigation Command // ============================================================================ export class InvestigateCommand { private investigationsDir: string; private resultsDir: string; private preparedDataDir: string; private datasetDir: string; private editor: string; constructor(options: InvestigateOptions = {}) { this.investigationsDir = options.outputDir || './investigations'; this.resultsDir = options.resultsDir || './results'; this.preparedDataDir = options.preparedDataDir || './prepared-data'; this.datasetDir = options.datasetDir || './data'; this.editor = options.editor || process.env.EDITOR || 'code'; } async run(options: InvestigateOptions): Promise { // Handle different modes if (options.list) { await this.listFailures(options.config); return; } if (options.status) { await this.showStatus(); return; } if (options.next) { await this.openNext(); return; } if (options.done) { await this.markDone(options.done); return; } if (options.fixed) { await this.markFixed(options.fixed); return; } if (options.sync) { await this.syncToDataset(); return; } // Investigation utilities if (options.inspect) { await this.inspectQuestion(options.inspect); return; } if (options.search && options.questionId) { await this.searchObservations(options.questionId, options.search); return; } if (options.trace && options.questionId) { await this.traceInformation(options.questionId, options.trace); return; } // Date-based observation viewer if (options.date && options.questionId) { await this.viewObservationsAroundDate(options.questionId, options.date, options.context ?? 1); return; } // Session viewer if (options.listSessions && options.questionId) { await this.listSessions(options.questionId); return; } if (options.session !== undefined && options.questionId) { await this.viewSession(options.questionId, options.session); return; } // Data freshness detection if (options.checkStale) { await this.checkStaleData(options.questionId, options.staleOnly); return; } // Original dataset search if (options.searchOriginal && options.questionId) { await this.searchOriginalDataset(options.questionId, options.searchOriginal); return; } // Improve question/answer in dataset if (options.improve) { await this.addImprovement( options.improve, options.improveQuestion, options.improveAnswer, options.improveNote, options.category, options.clearImproved, ); return; } // Check for duplicate observations if (options.checkDuplicates) { await this.checkDuplicateObservations(options.questionId); return; } // Baseline check - comprehensive data quality check if (options.baseline && options.questionId) { await this.runBaselineCheck(options.questionId); return; } // Prepare stale questions (deprecated) if (options.prepareStale) { await this.prepareStaleQuestions(options.dryRun); return; } // Print prepare command for stale questions if (options.printPrepareCommand) { await this.printPrepareCommand(); return; } // Default: setup investigation from run if (options.runId) { await this.setupInvestigation(options.runId); return; } // No args: show help this.showHelp(); } // -------------------------------------------------------------------------- // List Failures // -------------------------------------------------------------------------- private async listFailures(configFilter?: string): Promise { console.log('\nšŸ” Scanning for benchmark runs with failures...\n'); interface RunInfo { runId: string; config: string; dataset: string; timestamp: string; totalFailed: number; totalQuestions: number; failuresPath: string; } const runs: RunInfo[] = []; // Scan results directory for all configs if (!existsSync(this.resultsDir)) { console.log('No results directory found.'); return; } const configs = await readdir(this.resultsDir); for (const config of configs) { // Skip if filtering by config and doesn't match if (configFilter && !config.includes(configFilter)) { continue; } const configDir = join(this.resultsDir, config); const stat = await import('fs/promises').then(fs => fs.stat(configDir)); if (!stat.isDirectory()) continue; const runDirs = await readdir(configDir); for (const runDir of runDirs) { if (!runDir.startsWith('run_')) continue; const failuresPath = join(configDir, runDir, 'failures.json'); if (!existsSync(failuresPath)) continue; try { const failures = JSON.parse(await readFile(failuresPath, 'utf-8')) as FailuresFile; runs.push({ runId: failures.runId, config: failures.config, dataset: failures.dataset, timestamp: failures.timestamp, totalFailed: failures.totalFailed, totalQuestions: failures.totalQuestions, failuresPath, }); } catch { // Skip invalid files } } } if (runs.length === 0) { console.log('No runs with failures found.'); return; } // Sort by timestamp (newest first) runs.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); // Group by config const byConfig = new Map(); for (const run of runs) { const key = `${run.dataset}/${run.config}`; if (!byConfig.has(key)) { byConfig.set(key, []); } byConfig.get(key)!.push(run); } // Display console.log(`Found ${runs.length} runs with failures across ${byConfig.size} configs:\n`); for (const [configKey, configRuns] of byConfig) { console.log(`šŸ“ ${configKey}`); // Show latest run prominently const latest = configRuns[0]; const failRate = ((latest.totalFailed / latest.totalQuestions) * 100).toFixed(1); console.log(` Latest: ${latest.runId}`); console.log(` Failed: ${latest.totalFailed}/${latest.totalQuestions} (${failRate}%)`); console.log(` Date: ${new Date(latest.timestamp).toLocaleString()}`); if (configRuns.length > 1) { console.log(` (${configRuns.length - 1} older runs)`); } console.log(); } // Show usage hint console.log('To investigate a run:'); console.log(` pnpm investigate `); console.log(` pnpm investigate ${runs[0].runId}`); } // -------------------------------------------------------------------------- // Setup Investigation // -------------------------------------------------------------------------- private async setupInvestigation(runIdOrPath: string): Promise { console.log(`\nšŸ” Setting up investigation for: ${runIdOrPath}\n`); // Find the failures.json file const { failuresPath, failures } = await this.findFailures(runIdOrPath); console.log(`šŸ“ Found failures file: ${failuresPath}`); console.log(` Config: ${failures.config}`); console.log(` Dataset: ${failures.dataset}`); console.log(` Failed: ${failures.totalFailed}/${failures.totalQuestions}`); // Create investigation directory const investigationDir = join(this.investigationsDir, failures.runId); await mkdir(investigationDir, { recursive: true }); // Load existing progress or create new const progressPath = join(investigationDir, 'progress.json'); let progress: InvestigationProgress; if (existsSync(progressPath)) { progress = JSON.parse(await readFile(progressPath, 'utf-8')); console.log(`\nšŸ“Š Resuming existing investigation (${progress.investigated}/${progress.totalFailed} done)`); } else { progress = { runId: failures.runId, config: failures.config, dataset: failures.dataset, createdAt: new Date().toISOString(), totalFailed: failures.totalFailed, investigated: 0, questions: {}, }; for (const qid of failures.questionIds) { progress.questions[qid] = { status: 'pending' }; } } // Load results.jsonl to get actual answers const resultsDir = dirname(failuresPath); const resultsPath = join(resultsDir, 'results.jsonl'); const resultsMap = new Map(); if (existsSync(resultsPath)) { const resultsContent = await readFile(resultsPath, 'utf-8'); for (const line of resultsContent.split('\n').filter(l => l.trim())) { try { const result = JSON.parse(line) as EvaluationResult; resultsMap.set(result.question_id, result); } catch { // Skip invalid lines } } } // Load dataset for original questions const loader = new DatasetLoader(this.datasetDir); const dataset = await loader.loadDataset(failures.dataset as 'longmemeval_s' | 'longmemeval_m'); const questionMap = new Map(); for (const q of dataset) { questionMap.set(q.question_id, q); } // Setup each failed question let created = 0; let skipped = 0; for (const questionId of failures.questionIds) { const questionDir = join(investigationDir, questionId); const dataDir = join(questionDir, 'data'); const analysisPath = join(questionDir, 'analysis.md'); // Skip if already exists if (existsSync(analysisPath)) { skipped++; continue; } await mkdir(dataDir, { recursive: true }); // Get question data const question = questionMap.get(questionId); const result = resultsMap.get(questionId); if (!question) { console.warn(` āš ļø Question ${questionId} not found in dataset`); continue; } // Copy original question data await writeFile(join(dataDir, 'original.json'), JSON.stringify(question, null, 2)); // Copy result if available if (result) { await writeFile(join(dataDir, 'result.json'), JSON.stringify(result, null, 2)); } // Copy prepared data files const preparedDir = join(this.preparedDataDir, failures.dataset, failures.config, questionId); const filesToCopy = ['om.md', 'om.json', 'meta.json']; for (const file of filesToCopy) { const srcPath = join(preparedDir, file); if (existsSync(srcPath)) { const content = await readFile(srcPath, 'utf-8'); await writeFile(join(dataDir, file), content); } } // Generate analysis template const template = generateAnalysisTemplate( questionId, question.question, question.answer, result?.hypothesis || '(not available)', question.question_type, question.improved_question, question.improved_answer, ); await writeFile(analysisPath, template); created++; } // Save progress await writeFile(progressPath, JSON.stringify(progress, null, 2)); console.log(`\nāœ… Investigation setup complete!`); console.log(` Created: ${created} new question directories`); console.log(` Skipped: ${skipped} existing directories`); console.log(` Location: ${investigationDir}`); console.log(`\nšŸ“ Next steps:`); console.log(` pnpm investigate --status # Check progress`); console.log(` pnpm investigate --next # Open next question`); console.log(` pnpm investigate --done # Mark as investigated`); } // -------------------------------------------------------------------------- // Status // -------------------------------------------------------------------------- private async showStatus(): Promise { const investigations = await this.listInvestigations(); if (investigations.length === 0) { console.log('\nšŸ“­ No investigations found.'); console.log(' Run: pnpm investigate to start one.\n'); return; } console.log('\nšŸ“Š Investigation Status\n'); for (const inv of investigations) { const progress = await this.loadProgress(inv); if (!progress) continue; const pending = Object.values(progress.questions).filter(q => q.status === 'pending').length; const investigated = Object.values(progress.questions).filter(q => q.status === 'investigated').length; const fixImplemented = Object.values(progress.questions).filter(q => q.status === 'fix-implemented').length; const synced = Object.values(progress.questions).filter(q => q.status === 'synced').length; const pct = (((progress.totalFailed - pending) / progress.totalFailed) * 100).toFixed(1); console.log(`šŸ“ ${inv}`); console.log(` Config: ${progress.config}`); console.log(` Progress: ${progress.totalFailed - pending}/${progress.totalFailed} (${pct}%)`); console.log(` ā”œā”€ Pending: ${pending}`); console.log(` ā”œā”€ Investigated: ${investigated}`); console.log(` ā”œā”€ Fix Implemented: ${fixImplemented}`); console.log(` └─ Synced: ${synced}`); // Show category breakdown if any investigated if (investigated + fixImplemented + synced > 0) { const categories = new Map(); for (const q of Object.values(progress.questions)) { if (q.category) { categories.set(q.category, (categories.get(q.category) || 0) + 1); } } if (categories.size > 0) { console.log(` Categories:`); for (const [cat, count] of [...categories.entries()].sort((a, b) => b[1] - a[1])) { console.log(` ${cat}: ${count}`); } } } console.log(''); } } // -------------------------------------------------------------------------- // Open Next // -------------------------------------------------------------------------- private async openNext(): Promise { const investigations = await this.listInvestigations(); if (investigations.length === 0) { console.log('\nšŸ“­ No investigations found.\n'); return; } // Use most recent investigation const latestInv = investigations[investigations.length - 1]; const progress = await this.loadProgress(latestInv); if (!progress) { console.log('\nāŒ Could not load progress.\n'); return; } // Find next pending question const pendingId = Object.entries(progress.questions).find(([_, q]) => q.status === 'pending')?.[0]; if (!pendingId) { console.log('\nšŸŽ‰ All questions investigated!\n'); return; } const analysisPath = join(this.investigationsDir, latestInv, pendingId, 'analysis.md'); if (!existsSync(analysisPath)) { console.log(`\nāŒ Analysis file not found: ${analysisPath}\n`); return; } console.log(`\nšŸ“ Opening: ${pendingId}`); console.log(` File: ${analysisPath}`); console.log(` Editor: ${this.editor}\n`); // Open in editor const { spawn } = await import('child_process'); spawn(this.editor, [analysisPath], { detached: true, stdio: 'ignore', }).unref(); // Also print some context const dataDir = join(this.investigationsDir, latestInv, pendingId, 'data'); const resultPath = join(dataDir, 'result.json'); if (existsSync(resultPath)) { const result = JSON.parse(await readFile(resultPath, 'utf-8')) as EvaluationResult; const expectedStr = String(result.expected_answer); const hypothesisStr = String(result.hypothesis); console.log(`šŸ“‹ Quick Context:`); console.log(` Type: ${result.question_type}`); console.log(` Q: ${result.question.substring(0, 100)}...`); console.log(` Expected: ${expectedStr.substring(0, 100)}${expectedStr.length > 100 ? '...' : ''}`); console.log(` Got: ${hypothesisStr.substring(0, 100)}${hypothesisStr.length > 100 ? '...' : ''}`); console.log(''); } console.log(`When done, run: pnpm investigate --done ${pendingId}\n`); } // -------------------------------------------------------------------------- // Mark Done // -------------------------------------------------------------------------- private async markDone(questionId: string): Promise { const investigations = await this.listInvestigations(); if (investigations.length === 0) { console.log('\nšŸ“­ No investigations found.\n'); return; } // Find investigation containing this question let foundInv: string | null = null; let progress: InvestigationProgress | null = null; for (const inv of investigations) { const p = await this.loadProgress(inv); if (p && questionId in p.questions) { foundInv = inv; progress = p; break; } } if (!foundInv || !progress) { console.log(`\nāŒ Question ${questionId} not found in any investigation.\n`); return; } // Parse analysis.md to extract category const analysisPath = join(this.investigationsDir, foundInv, questionId, 'analysis.md'); let category: FailureCategory | undefined; if (existsSync(analysisPath)) { const content = await readFile(analysisPath, 'utf-8'); category = this.extractCategory(content); } // Update progress progress.questions[questionId] = { status: 'investigated', category, investigatedAt: new Date().toISOString(), }; progress.investigated = Object.values(progress.questions).filter(q => q.status !== 'pending').length; // Save progress const progressPath = join(this.investigationsDir, foundInv, 'progress.json'); await writeFile(progressPath, JSON.stringify(progress, null, 2)); const remaining = progress.totalFailed - progress.investigated; console.log(`\nāœ… Marked ${questionId} as investigated`); if (category) { console.log(` Category: ${category}`); } console.log(` Progress: ${progress.investigated}/${progress.totalFailed}`); console.log(` Remaining: ${remaining}`); if (remaining > 0) { console.log(`\n Run: pnpm investigate --next\n`); } else { console.log(`\nšŸŽ‰ All questions investigated!`); console.log(` Run: pnpm investigate --sync to sync fixes to dataset\n`); } } private async markFixed(questionId: string): Promise { const investigations = await this.listInvestigations(); if (investigations.length === 0) { console.log('\nšŸ“­ No investigations found.\n'); return; } // Find investigation containing this question let foundInv: string | null = null; let progress: InvestigationProgress | null = null; for (const inv of investigations) { const p = await this.loadProgress(inv); if (p && questionId in p.questions) { foundInv = inv; progress = p; break; } } if (!foundInv || !progress) { console.log(`\nāŒ Question ${questionId} not found in any investigation.\n`); return; } const currentStatus = progress.questions[questionId]?.status; if (currentStatus === 'pending') { console.log(`\nāš ļø Question ${questionId} hasn't been investigated yet.`); console.log(` Run: pnpm investigate --done ${questionId} first\n`); return; } // Update status to fix-implemented progress.questions[questionId] = { ...progress.questions[questionId], status: 'fix-implemented', }; // Save progress const progressPath = join(this.investigationsDir, foundInv, 'progress.json'); await writeFile(progressPath, JSON.stringify(progress, null, 2)); const fixedCount = Object.values(progress.questions).filter( q => q.status === 'fix-implemented' || q.status === 'synced', ).length; console.log(`\nāœ… Marked ${questionId} as fix-implemented`); console.log(` Fixed: ${fixedCount}/${progress.totalFailed}`); console.log(`\n When ready, run: pnpm investigate --sync\n`); } // -------------------------------------------------------------------------- // Sync to Dataset // -------------------------------------------------------------------------- private async syncToDataset(): Promise { const investigations = await this.listInvestigations(); if (investigations.length === 0) { console.log('\nšŸ“­ No investigations found.\n'); return; } // Use most recent investigation const latestInv = investigations[investigations.length - 1]; const progress = await this.loadProgress(latestInv); if (!progress) { console.log('\nāŒ Could not load progress.\n'); return; } // Load dataset const datasetPath = join(this.datasetDir, `${progress.dataset}.json`); if (!existsSync(datasetPath)) { console.log(`\nāŒ Dataset not found: ${datasetPath}\n`); return; } const dataset = JSON.parse(await readFile(datasetPath, 'utf-8')) as LongMemEvalQuestion[]; const datasetMap = new Map(); for (const q of dataset) { datasetMap.set(q.question_id, q); } let synced = 0; let skipped = 0; console.log(`\nšŸ”„ Syncing fixes to ${progress.dataset}.json...\n`); for (const [questionId, qProgress] of Object.entries(progress.questions)) { if (qProgress.status === 'pending') { continue; } const analysisPath = join(this.investigationsDir, latestInv, questionId, 'analysis.md'); if (!existsSync(analysisPath)) { continue; } const content = await readFile(analysisPath, 'utf-8'); const fixes = this.extractFixes(content); if (!fixes.improved_question && !fixes.improved_answer && !fixes.improvement_note) { skipped++; continue; } const question = datasetMap.get(questionId); if (!question) { console.warn(` āš ļø Question ${questionId} not found in dataset`); continue; } // Update question let updated = false; if (fixes.improved_question && fixes.improved_question !== question.improved_question) { question.improved_question = fixes.improved_question; updated = true; } if (fixes.improved_answer && fixes.improved_answer !== question.improved_answer) { question.improved_answer = fixes.improved_answer; updated = true; } if (fixes.improvement_note && fixes.improvement_note !== question.improvement_note) { question.improvement_note = fixes.improvement_note; updated = true; } if (updated) { console.log(` āœ“ ${questionId}`); synced++; // Update progress status progress.questions[questionId].status = 'synced'; } else { skipped++; } } // Save dataset with 4-space indentation await writeFile(datasetPath, JSON.stringify(dataset, null, 4)); // Save progress const progressPath = join(this.investigationsDir, latestInv, 'progress.json'); await writeFile(progressPath, JSON.stringify(progress, null, 2)); console.log(`\nāœ… Sync complete!`); console.log(` Synced: ${synced}`); console.log(` Skipped: ${skipped} (no changes or pending)`); console.log(`\nšŸ’” Don't forget to run: pnpm run sync-improved-om-qa\n`); } // -------------------------------------------------------------------------- // Helpers // -------------------------------------------------------------------------- private async findFailures(runIdOrPath: string): Promise<{ failuresPath: string; failures: FailuresFile }> { // Check if it's a direct path if (existsSync(runIdOrPath)) { const content = await readFile(runIdOrPath, 'utf-8'); return { failuresPath: runIdOrPath, failures: JSON.parse(content) }; } // Search in results directory const configs = await readdir(this.resultsDir).catch(() => []); for (const config of configs) { const configDir = join(this.resultsDir, config); const runs = await readdir(configDir).catch(() => []); for (const run of runs) { if (run === runIdOrPath || run.includes(runIdOrPath)) { const failuresPath = join(configDir, run, 'failures.json'); if (existsSync(failuresPath)) { const content = await readFile(failuresPath, 'utf-8'); return { failuresPath, failures: JSON.parse(content) }; } } } } throw new Error(`Could not find failures.json for: ${runIdOrPath}`); } private async listInvestigations(): Promise { if (!existsSync(this.investigationsDir)) { return []; } const entries = await readdir(this.investigationsDir); const investigations: string[] = []; for (const entry of entries) { const progressPath = join(this.investigationsDir, entry, 'progress.json'); if (existsSync(progressPath)) { investigations.push(entry); } } return investigations.sort(); } private async loadProgress(investigation: string): Promise { const progressPath = join(this.investigationsDir, investigation, 'progress.json'); if (!existsSync(progressPath)) { return null; } return JSON.parse(await readFile(progressPath, 'utf-8')); } private extractCategory(content: string): FailureCategory | undefined { const categoryMap: Record = { 'Observer missed critical information': 'observer-miss', 'Reflector lost/merged information incorrectly': 'reflector-loss', 'Agent reasoning error': 'agent-reasoning', 'Ambiguous/poorly-worded question': 'dataset-error', // Ambiguous questions are dataset errors 'Dataset inconsistency/error': 'dataset-error', 'RAG retrieval miss': 'rag-miss', }; for (const [text, category] of Object.entries(categoryMap)) { // Look for checked checkbox const pattern = new RegExp(`\\[x\\]\\s*${text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i'); if (pattern.test(content)) { return category; } } // Check for "Other" if (/\[x\]\s*Other:/i.test(content)) { return 'other'; } return undefined; } private extractFixes(content: string): { improved_question?: string; improved_answer?: string; improvement_note?: string; } { const fixes: { improved_question?: string; improved_answer?: string; improvement_note?: string; } = {}; // Extract improved_question const iqMatch = content.match(/\*\*improved_question\*\*:\s*(.+?)(?:\n|$)/); if (iqMatch && iqMatch[1].trim() && !iqMatch[1].trim().startsWith('