resolve() accepts a single exact occurrence without a context check — a duplicated block can hijack any anchor consumer #74
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Surfaced by the #70 branch review (session 0066, PR #73) and CONFIRMED against the code; pre-existing (pre-dates #70), affects accept and reject alike.
Behavior (
src/anchorer.tsresolve()): when the fingerprint text occurs exactly once in the document,resolvereturns that occurrence without checking thebefore/aftercontext — context disambiguation only runs for multiple occurrences (if (occ.length === 1) return rangeAt(occ[0], fp.text);).Failure scenario: the writer copy-pastes a block containing an applied proposal (or any anchored text), then edits the ORIGINAL site so it no longer matches. The fingerprint now has exactly one exact occurrence — the untouched duplicate — so every anchor consumer (accept, reject, thread/attribution re-anchoring) resolves to the WRONG site: e.g. ✗ Reject replaces the duplicate paragraph with the proposal original while the real applied-and-tweaked text stays, and the proposal record is removed, so the corruption is unflagged.
Mitigations in place: #70 made the tracked-span bookkeeping (
appliedSpans) ground truth while present —renderAllonly rebuilds an absent entry from a resolve — so the reject fallback path is insulated in-session. The primary resolved paths are not.Fix direction to evaluate: require
contextMatcheseven for a single occurrence (fall back to lineHint/orphaned when the context disagrees), weighed against re-anchoring tolerance for legitimately moved text (INV-1/INV-6 external-move re-anchor cases intest/anchorer.test.tsandproposals.test.ts).