CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / integration (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m20s
CI & Build / Build & push image (push) Successful in 40s
Rule 27: a history nobody can read is not shipped. `RuleHistoryPanel.vue` sits below the fields in `RuleEditorSlideOver`, where a rule is read in full — not on the list row, where a history entry point would compete with the row's job. REUSE, DECIDED FIELD BY FIELD RATHER THAN ALL AT ONCE. DiffView.vue is reused unchanged: it takes DiffLine[] and nothing note-shaped. HistoryPanel.vue is NOT, and its props are the reason — noteId + currentBody, a NoteVersion carrying tags and pin columns, a fetch of /api/notes/…, a restore emit, pin/unpin buttons. Rules have no tags, no pins, and deliberately no restore, and a rule's text is EIGHT fields rather than one body, which changes the reader's question from "what changed" to "which fields moved". Recorded here rather than forked silently, per #3207. THE FORK THAT WAS ALREADY THERE. The LCS walk existed three times — privately in useAssist.ts, and again inside HistoryPanel.vue and VersionHistorySection.vue — character-identical apart from quote style, because computeDiff was never exported. Rather than add a fourth copy, it moves to utils/diff.ts and the three become imports; the extraction was verified equivalent to all three before anything was deleted. DiffLine is re-exported from useAssist so its existing importers are untouched. WHAT A ROW SHOWS: when, and which fields moved. A version holds the text the edit REPLACED, so the edit is the step from a row to the next NEWER state — the row above it, or, for the newest row, the rule as it stands now. Comparing against the row below would attribute every change to the wrong edit. A field nobody has fetched yet reads as neither changed nor unchanged. An edit that touched verify_with is badged "check reset", because that edit silently cleared verified_at (milestone 312) and put the rule back at the top of the staleness sweep — a moment visible nowhere else. The badge is a 12% color-mix TINT, not solid `--fs-warning`. `--fs-warning-fg` is defined in theme.css as "warning TEXT on a warning tint", so painting it over the solid token is exactly the same-hue contrast failure #3141 records. Every var() the component references resolves against theme.css, checked before pushing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
/**
|
|
* Line diff — one copy, for every surface that shows what changed.
|
|
*
|
|
* WHY THIS FILE EXISTS. The same LCS walk was written out three times:
|
|
* privately in `useAssist.ts`, and again inside `HistoryPanel.vue` and
|
|
* `VersionHistorySection.vue`. The three were character-identical apart from
|
|
* quote style — nobody had diverged them on purpose, they were simply copied
|
|
* because `computeDiff` was never exported. Milestone 323 needed a fourth
|
|
* consumer (a rule's edit history), and a fourth copy is the cost #3207
|
|
* records: a fix or an improvement now has to be found in N places by someone
|
|
* who does not know N.
|
|
*/
|
|
|
|
export interface DiffLine {
|
|
type: "equal" | "delete" | "insert";
|
|
text: string;
|
|
}
|
|
|
|
/**
|
|
* Diff `a` against `b`, line by line.
|
|
*
|
|
* `delete` lines come from `a`, `insert` lines from `b` — so the caller
|
|
* decides which side reads as "before" by which argument it passes. Every
|
|
* caller here passes the CURRENT text as `a` and the older text as `b`, so a
|
|
* deletion is what the old version had and an insertion is what replaced it.
|
|
*
|
|
* O(m·n) in time and memory: fine for a note or a rule, and deliberately not
|
|
* generalised further, since nothing here diffs a file of thousands of lines.
|
|
*/
|
|
export function computeDiff(a: string, b: string): DiffLine[] {
|
|
const aLines = a.split("\n");
|
|
const bLines = b.split("\n");
|
|
const m = aLines.length, n = bLines.length;
|
|
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
for (let i = m - 1; i >= 0; i--)
|
|
for (let j = n - 1; j >= 0; j--)
|
|
dp[i][j] = aLines[i] === bLines[j]
|
|
? dp[i + 1][j + 1] + 1
|
|
: Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
const result: DiffLine[] = [];
|
|
let i = 0, j = 0;
|
|
while (i < m && j < n) {
|
|
if (aLines[i] === bLines[j]) { result.push({ type: "equal", text: aLines[i++] }); j++; }
|
|
else if (dp[i + 1][j] >= dp[i][j + 1]) result.push({ type: "delete", text: aLines[i++] });
|
|
else result.push({ type: "insert", text: bLines[j++] });
|
|
}
|
|
while (i < m) result.push({ type: "delete", text: aLines[i++] });
|
|
while (j < n) result.push({ type: "insert", text: bLines[j++] });
|
|
return result;
|
|
}
|