/** * Heuristic diff classifier for feedback analysis. * Analyzes editDiffs from a completed job and produces feedback classifications. * Layer 1: rule-based. Layer 2 (Claude skill) will enrich with source code analysis. */ type EditDiff = { id: string; subtitleIndex: number; field: string; oldValue: string | null; newValue: string | null; rationale: string; changeCategory: string; }; type FeedbackClassification = { errorType: "input_error" | "cicd_needed" | "style_preference" | "false_positive"; confidenceScore: number; description: string; affectedInput: "neon" | "alto_curated" | "lsx_prompt" | "lsx_logic" | "arc_config" | null; }; // Keyword patterns for rationale analysis const INPUT_ERROR_KEYWORDS = ["wrong", "incorrect", "mistranslat", "error in source", "bad input", "original is wrong", "source error"]; const STYLE_KEYWORDS = ["style", "preference", "natural", "sounds better", "flow", "readability", "tone", "nuance"]; const NEON_KEYWORDS = ["neon", "source", "template", "original", "input text", "transcription"]; const ALTO_KEYWORDS = ["alto", "curated", "reference", "base translation"]; const LSX_LOGIC_KEYWORDS = ["timing", "cpl", "cps", "overlap", "gap", "duration", "frame", "sync"]; const LSX_PROMPT_KEYWORDS = ["format", "style guide", "capitalization", "punctuation", "tag", "italic"]; function matchesKeywords(text: string, keywords: string[]): boolean { const lower = text.toLowerCase(); return keywords.some((k) => lower.includes(k)); } function classifySingleDiff(diff: EditDiff): FeedbackClassification { const { changeCategory, rationale, field } = diff; const rat = rationale.toLowerCase(); // timing_fix → usually cicd_needed (LSX should handle timing) if (changeCategory === "timing_fix") { return { errorType: "cicd_needed", confidenceScore: matchesKeywords(rat, LSX_LOGIC_KEYWORDS) ? 0.85 : 0.65, description: `Timing correction on subtitle ${diff.subtitleIndex + 1}: ${rationale}`, affectedInput: "lsx_logic", }; } // cpl_fix → cicd_needed (LSX should auto-fix CPL violations) if (changeCategory === "cpl_fix") { return { errorType: "cicd_needed", confidenceScore: 0.8, description: `CPL violation fix on subtitle ${diff.subtitleIndex + 1}: ${rationale}`, affectedInput: "lsx_logic", }; } // split_merge → cicd_needed (LSX cueing should handle this) if (changeCategory === "split_merge") { return { errorType: "cicd_needed", confidenceScore: 0.75, description: `Split/merge on subtitle ${diff.subtitleIndex + 1}: ${rationale}`, affectedInput: "lsx_logic", }; } // style_correction → style_preference if (changeCategory === "style_correction") { return { errorType: "style_preference", confidenceScore: 0.7, description: `Style correction on subtitle ${diff.subtitleIndex + 1}: ${rationale}`, affectedInput: matchesKeywords(rat, LSX_PROMPT_KEYWORDS) ? "lsx_prompt" : null, }; } // translation_fix → depends on rationale if (changeCategory === "translation_fix") { if (matchesKeywords(rat, INPUT_ERROR_KEYWORDS)) { return { errorType: "input_error", confidenceScore: 0.8, description: `Translation error on subtitle ${diff.subtitleIndex + 1}: ${rationale}`, affectedInput: matchesKeywords(rat, ALTO_KEYWORDS) ? "alto_curated" : matchesKeywords(rat, NEON_KEYWORDS) ? "neon" : "neon", }; } if (matchesKeywords(rat, STYLE_KEYWORDS)) { return { errorType: "style_preference", confidenceScore: 0.65, description: `Style preference on subtitle ${diff.subtitleIndex + 1}: ${rationale}`, affectedInput: "lsx_prompt", }; } // Default translation fix → could be input error but low confidence return { errorType: "input_error", confidenceScore: 0.5, description: `Translation change on subtitle ${diff.subtitleIndex + 1}: ${rationale}`, affectedInput: "neon", }; } // other / fallback → false_positive with low confidence return { errorType: "false_positive", confidenceScore: 0.4, description: `Edit on subtitle ${diff.subtitleIndex + 1} (${field}): ${rationale}`, affectedInput: null, }; } /** * Classify all diffs from a completed job. * Groups similar patterns to avoid 1:1 diff→analysis noise. * Returns deduplicated feedback classifications. */ export function classifyDiffs(diffs: EditDiff[]): FeedbackClassification[] { if (diffs.length === 0) return []; // Classify each diff const classified = diffs.map(classifySingleDiff); // Group by errorType + affectedInput to reduce noise const groups = new Map(); for (const c of classified) { const key = `${c.errorType}:${c.affectedInput || "none"}`; if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push(c); } // Produce one analysis per group with aggregated description const results: FeedbackClassification[] = []; for (const [, items] of groups) { if (items.length === 0) continue; const first = items[0]; const avgConfidence = items.reduce((sum, i) => sum + i.confidenceScore, 0) / items.length; results.push({ errorType: first.errorType, confidenceScore: Math.round(avgConfidence * 100) / 100, description: items.length === 1 ? first.description : `${items.length} ${first.errorType.replace("_", " ")} issues detected. Examples: ${items .slice(0, 3) .map((i) => i.description) .join("; ")}`, affectedInput: first.affectedInput, }); } // Sort by confidence descending results.sort((a, b) => b.confidenceScore - a.confidenceScore); return results; }