// A line diff, for showing what a proposed policy actually changes before anyone saves it. // // Hand-rolled rather than a dependency: this is one screen showing one document, the inputs are a few // hundred lines at most, and the alternative is adding a package to the frozen lockfile for forty lines of // well-understood algorithm. If a second surface ever needs a diff, that trade flips. export type DiffLine = { kind: 'same' | 'add' | 'remove'; text: string }; /** Longest common subsequence table over the two line arrays. O(n·m) — fine at document scale. */ function lcsLengths(a: string[], b: string[]): number[][] { const table: number[][] = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0)); for (let i = a.length - 1; i >= 0; i--) { for (let j = b.length - 1; j >= 0; j--) { table[i]![j] = a[i] === b[j] ? table[i + 1]![j + 1]! + 1 : Math.max(table[i + 1]![j]!, table[i]![j + 1]!); } } return table; } /** Every line of both documents, in order, tagged with what happened to it. */ export function diffLines(before: string, after: string): DiffLine[] { const a = before.split('\n'); const b = after.split('\n'); const table = lcsLengths(a, b); const out: DiffLine[] = []; let i = 0; let j = 0; while (i < a.length && j < b.length) { if (a[i] === b[j]) { out.push({ kind: 'same', text: a[i]! }); i++; j++; } else if (table[i + 1]![j]! >= table[i]![j + 1]!) { out.push({ kind: 'remove', text: a[i]! }); i++; } else { out.push({ kind: 'add', text: b[j]! }); j++; } } while (i < a.length) out.push({ kind: 'remove', text: a[i++]! }); while (j < b.length) out.push({ kind: 'add', text: b[j++]! }); return out; } /** * Drop long runs of unchanged lines, keeping `context` either side of every change. * * A policy is mostly unchanged by any one edit, and an unabridged diff buries the three lines that matter. * `null` marks each elision so the view can draw a gap rather than pretend the lines are adjacent. */ export function collapseUnchanged(lines: DiffLine[], context = 3): (DiffLine | null)[] { const keep = new Array(lines.length).fill(false); lines.forEach((line, index) => { if (line.kind === 'same') return; for (let k = Math.max(0, index - context); k <= Math.min(lines.length - 1, index + context); k++) keep[k] = true; }); const out: (DiffLine | null)[] = []; let gap = false; lines.forEach((line, index) => { if (keep[index]) { out.push(line); gap = false; } else if (!gap) { out.push(null); gap = true; } }); return out; } export const diffCounts = (lines: DiffLine[]) => ({ added: lines.filter((l) => l.kind === 'add').length, removed: lines.filter((l) => l.kind === 'remove').length, });