feat(f7): block-level diff with atomic fences (#21)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-11 08:42:51 -07:00
parent 247a450e10
commit 6ffbf67871
2 changed files with 102 additions and 1 deletions
+50 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { splitBlocks } from "../src/trackChangesModel";
import { splitBlocks, diffBlocks } from "../src/trackChangesModel";
describe("splitBlocks", () => {
it("splits prose paragraphs on blank lines, dropping empties", () => {
@@ -27,3 +27,52 @@ describe("splitBlocks", () => {
expect(blocks[0].key).toBe("hello world");
});
});
describe("diffBlocks", () => {
const kinds = (base: string, cur: string) => diffBlocks(base, cur).map((o) => o.kind);
it("unchanged doc → all unchanged", () => {
const doc = "Alpha.\n\nBravo.\n";
expect(kinds(doc, doc)).toEqual(["unchanged", "unchanged"]);
});
it("a pure addition → an added op", () => {
const ops = diffBlocks("Alpha.\n", "Alpha.\n\nBravo.\n");
expect(ops.map((o) => o.kind)).toEqual(["unchanged", "added"]);
});
it("a pure deletion → a removed op", () => {
const ops = diffBlocks("Alpha.\n\nBravo.\n", "Alpha.\n");
expect(ops.map((o) => o.kind)).toEqual(["unchanged", "removed"]);
});
it("a prose modification → a non-atomic changed op", () => {
const ops = diffBlocks("The quick fox.\n", "The slow fox.\n");
expect(ops).toHaveLength(1);
expect(ops[0].kind).toBe("changed");
expect(ops[0].kind === "changed" && ops[0].atomic).toBe(false);
});
it("a code-fence change → an ATOMIC changed op (INV-23)", () => {
const base = "```ts\nconst a = 1;\n```\n";
const cur = "```ts\nconst a = 2;\n```\n";
const ops = diffBlocks(base, cur);
expect(ops).toHaveLength(1);
expect(ops[0].kind).toBe("changed");
expect(ops[0].kind === "changed" && ops[0].atomic).toBe(true);
});
it("a mermaid-fence change → an ATOMIC changed op", () => {
const base = "```mermaid\nflowchart LR\n a-->b\n```\n";
const cur = "```mermaid\nflowchart LR\n a-->c\n```\n";
const ops = diffBlocks(base, cur);
expect(ops[0].kind).toBe("changed");
expect(ops[0].kind === "changed" && ops[0].atomic).toBe(true);
});
it("a reorder → keeps a matched block unchanged", () => {
const ops = diffBlocks("One.\n\nTwo.\n", "Two.\n\nOne.\n");
const k = ops.map((o) => o.kind);
expect(k).toContain("unchanged");
});
});