fix(hashline): validate unique stale anchor remaps

This commit is contained in:
can1357
2026-07-01 21:08:56 +02:00
parent 3e4ac085aa
commit 4c751c48a8
2 changed files with 38 additions and 1 deletions
+20 -1
View File
@@ -168,6 +168,20 @@ function validateDuplicateAnchorContext(
return checked;
}
function validateUniqueAnchorContext(
line: number,
mapped: number,
previousLines: readonly string[],
lineMap: ReadonlyMap<number, number>,
anchorLines: ReadonlySet<number>,
): boolean {
const offset = mapped - line;
const after = nearestContextLine(line, 1, anchorLines, previousLines.length);
if (after !== undefined) return lineMap.get(after) === after + offset;
const before = nearestContextLine(line, -1, anchorLines, previousLines.length);
return before !== undefined && lineMap.get(before) === before + offset;
}
function validateRemappedAnchorContext(
previousText: string,
currentText: string,
@@ -181,7 +195,12 @@ function validateRemappedAnchorContext(
for (const line of anchorLines) {
const mapped = lineMap.get(line);
if (mapped === undefined) return false;
if (!lineIsDuplicated(previousLines, line) && !lineIsDuplicated(currentLines, mapped)) continue;
if (!lineIsDuplicated(previousLines, line) && !lineIsDuplicated(currentLines, mapped)) {
if (!validateUniqueAnchorContext(line, mapped, previousLines, lineMap, anchorLines)) {
return false;
}
continue;
}
if (!validateDuplicateAnchorContext(line, mapped, previousLines, lineMap, anchorLines)) {
return false;
}
@@ -139,4 +139,22 @@ describe("Recovery — session-chain replay anchor-content gate", () => {
expect(recovered).toBeNull();
});
it("refuses unique-line remaps when following context no longer matches", () => {
const store = new InMemorySnapshotStore();
const v0Text = lines("L1", "L2", "L3", "L4", "T", "L6");
const h0 = store.record(PATH, v0Text);
const v1Text = lines("X", "L1", "L2", "L3", "L4", "T", "T_CHANGED", "L6");
store.record(PATH, v1Text);
const { edits } = parsePatch("SWAP 5.=5:\n+MODEL");
const recovered = new Recovery(store).tryRecover({
path: PATH,
currentText: v1Text,
fileHash: h0,
edits,
});
expect(recovered).toBeNull();
});
});