Marketing

A Re-Anchor Fallback Ladder for Edits That Moved

Antonio Evans10 min read

A three-rung ladder drawn from bottom to top.
Key points

Key points

  1. 1A stored finding anchor should first be matched by normalized string similarity against the current file, because it costs no tokens and resolves most small edits.
  2. 2Re-asking the model is worth exactly one attempt, with the old snippet, the file text and surrounding lines in the prompt, and an exact-match check before the returned offset is trusted.
  3. 3Everything that survives both steps goes to a human reviewer with the old snippet, the best candidate window and the similarity score attached.
  4. 4Measuring your own drift fixture (how many re-anchors each step resolves, and at what token cost) lets you set the thresholds instead of copying someone else's.

Why anchors break, and what "moved" actually means

A finding is a claim plus a pointer: "line 412 says the retry loop has no backoff." The claim survives edits; the pointer does not. A formatter reflows the file, a rename touches every call site, a block is cut and pasted three screens down. The diff tools most teams already use know about this. Review Board detects a moved block and shows a jump indicator rather than a delete-plus-insert pair Review Board documentation, and the Rich Diff Checker separates move, update, copy and find/replace operations so a reviewer sees what actually changed the Rich Diff Checker. IntelliJ's diff viewer color-codes added, modified and deleted lines and lets you collapse unchanged fragments to focus on the change itself the IntelliJ diff viewer.

Your re-anchor problem is the inverse of the diff problem. You are not asking "what changed between two versions?" You are asking "given a snippet from version N, where does it live in version N+7?" That matters because the answer has three possible shapes, and they need different handling:

  1. The text is identical and moved. Similarity will find it, usually with a score of 1.0. Assign it and stop.
  2. The text is edited but recognizable. Similarity finds the neighborhood; you need a threshold to decide whether the match is good enough.
  3. The text is gone or unrecognizable. Similarity returns noise, and you need a second opinion or a person.

The third case is where naive pipelines quietly corrupt data. A fuzzy match that lands on the wrong function is worse than an unanchored finding, because it looks resolved.

The fallback ladder, cheapest step first

Order the steps by marginal cost, not by accuracy. Similarity scoring runs in your own process on strings you already hold. A model call costs input tokens and latency. A reviewer costs minutes of a person's attention and, eventually, your credibility if you waste it.

The ladder:

Step 1: Score every window in the current file against the stored snippet. Normalize both sides first (collapse whitespace, trim trailing punctuation, optionally lowercase), then compute a similarity ratio. Levenshtein-family edit distance is the usual workhorse; n-gram and Bitap variants trade exactness for speed on short patterns a survey of fuzzy matching algorithms. Accept the best match if it clears your threshold, which the short answer sets at 0.85. Stop.

Step 2: Ask the model once. Send the old snippet, the full current file (or the relevant section), and two or three lines of context from before and after the original location. Ask for a revised snippet and an offset. Accept only if the returned snippet appears in the file at the claimed offset, character for character. One attempt, no retries. A loop of model calls on a single finding is how a modest repair job becomes a budget line.

Step 3: Route to a reviewer. The reviewer gets three things: the old snippet, the top candidate window with its score, and the finding's original claim. Their job is a yes/no on the candidate or a manual placement, not a research task.

The reason similarity comes first is a property of text, not of your system. Neil Fraser's analysis of pattern repetition found that in English prose a fifteen-character pattern has less than a 2% chance of occurring twice in the same chapter, while in the Linux kernel source the same length pattern is likely to occur about 250 times in the same file, mostly because of decorative ASCII separator lines Fuzzy Patch. That single number tells you how the ladder should be tuned: prose anchors are safe to auto-accept at a moderate threshold because ambiguity is rare, but code anchors need a higher threshold and a uniqueness check, because duplicate windows are common.

What the three strategies cost and where they land

The comparison below follows the fixture described in the brief: 50 findings whose anchors no longer exist, run through a three-step ladder. The numbers are worked examples you can reproduce on your own fixture, not measurements from a published study, and the cost column assumes a model that bills input and output tokens separately. The estimates assume a mid-sized file (about 1,500 lines, roughly 12,000 tokens) and a short answer (about 60 tokens).

StrategyWhat it returnsCost per findingStrengthsFailure mode
String similarityBest window and a score, in-processNo tokens, milliseconds of CPUCheap, deterministic, no networkPicks a wrong duplicate window when the file repeats itself
Model re-ask (one attempt)New snippet plus offsetRoughly 12,000 input tokens plus 60 output tokens per callHandles rewording, renamed identifiers, reordered blocksConfidently returns a plausible offset that does not match
Human reviewerManual placement or rejectTwo to four minutes of attentionCorrect on genuinely ambiguous casesCost scales with volume; slow feedback loop

A worked example on the 50-finding fixture, using plausible per-step resolution rates:

  • Step 1 (similarity at 0.85 or above) resolves 31 findings. Token cost: zero.
  • Step 2 (one model re-ask) runs on the remaining 19. It returns an exact-match offset for 11 of them. Token cost: 19 calls at roughly 12,000 input tokens, call it 228,000 input tokens, plus about 660 output tokens.
  • Step 3 sends the last 8 to a reviewer. At three minutes each, that is 24 minutes of review.

Sixty-two percent of the drift was repaired for no tokens at all, and the model handled most of what remained. If you flipped the order and asked the model first, you would spend 50 calls (about 600,000 input tokens) to resolve 42 findings, and similarity would still be the cheaper way to validate the answer. Skip the validation and you inherit the model's false matches.

For teams running this at scale, where the file text is stable across many re-anchor attempts on the same revision, prompt caching changes the arithmetic: cached prefix reads are billed at a fraction of the normal input rate, with reported reductions of up to 90% on input cost for repeated context prompt caching for AI agents. That makes Step 2 cheaper, not unnecessary, because the exact-match check is what protects you, not the price.

Building the similarity step correctly

The similarity step is where most implementations go wrong, and almost never because the scoring function is bad. It goes wrong in the windowing and the normalization.

Normalize before you compare. Collapse runs of whitespace to a single space, strip trailing whitespace per line, and decide once whether case matters. In code, case usually matters (identifiers), in prose it usually does not.

Window the current file, not the stored snippet. Slide a window the length of the normalized snippet across the current text, one character or one line at a time, and score each position. Scoring the snippet against the whole file as one string gives you a number that means nothing.

Check uniqueness. Keep the top two non-overlapping scores. If the best score is 0.91 and the runner-up is 0.88, you have a coin flip dressed as a match. A useful rule: accept automatically only when the best clears 0.85 and beats the runner-up by at least 0.10. Everything else drops to Step 2.

Cap the work. A character-by-character slide over a large file with an edit-distance calculation at every position is O(n·m) per position and gets slow fast. Slide by line instead, score line-chunks with a token ratio, then refine character positions only inside the winning chunk. Bitap-style approximate substring search exists precisely to avoid scoring every position independently fuzzy matching algorithms.

A minimal implementation of the accept rule:

from difflib import SequenceMatcher

def normalize(s):
    return " ".join(s.split())

def best_windows(snippet, text, top_k=2):
    snip, text = normalize(snippet), normalize(text)
    n = len(snip)
    scored = []
    for i in range(0, max(len(text) - n, 0) + 1, 40):
        window = text[i:i + n]
        scored.append((SequenceMatcher(None, snip, window).ratio(), i))
    scored.sort(reverse=True)
    return scored[:top_k]

def reanchor(stored, current, accept=0.85, margin=0.10):
    top = best_windows(stored, current)
    best = top[0] if top else (0.0, 0)
    runner_up = top[1][0] if len(top) > 1 else 0.0
    if best[0] >= accept and (best[0] - runner_up) >= margin:
        return {"status": "accepted", "score": best[0], "offset": best[1]}
    return {"status": "escalate", "score": best[0]}

The step size of 40 in the loop is a deliberate speed/sensitivity tradeoff: smaller steps find more matches and cost more time. Tune it against your own drift fixture, not by feel.

Writing the one-shot re-ask prompt

If Step 1 escalates, the model's job is narrow: find where this text went. Narrow jobs are what prompt structuring is for. Clear delimiters, an explicit output format and a stated role are the structural basics that make responses parseable LaunchDarkly's prompt engineering guide. Being specific about the desired output beats being clever: ask for the exact shape you intend to parse, and nothing else the Prompt Engineering Guide.

A workable template, with labeled sections:

<role>You locate a previously stored code snippet inside a current file.</role>
<task>Return the snippet as it now appears, or the string NOT_FOUND.</task>
<instructions>
1. Read the old snippet and the surrounding context.
2. Find the corresponding text in the current file, which may be edited.
3. Return JSON: {"snippet": "...", "offset": <int>} or {"snippet": "NOT_FOUND"}.
4. The snippet must be copied verbatim from the current file. Do not paraphrase.
</instructions>
<old_snippet>...</old_snippet>
<context_before>...</context_before>
<context_after>...</context_after>
<current_file>...</current_file>

Then verify mechanically. Look up current_file[offset:offset+len(snippet)] and reject if it does not equal snippet. This check is what makes the model step safe, and it is the same discipline behind structured outputs generally: enforce a schema, then validate the parsed result before it touches your data a guide to structured outputs. If you rely on JSON mode from a provider, keep the validation anyway, because JSON mode guarantees syntax, not truth structured output and JSON mode.

Two prompt details pay for themselves. First, put the static instructions and the file text early so that repeated calls on the same revision share a cacheable prefix; prefix matching is how caching works, so variable content goes last prompt caching for AI agents. Second, ask for NOT_FOUND explicitly so the model has an honest exit. Remove that option and you get a fabricated location instead.

When the team running this pipeline is not the team that owns the model, a single API across text, image, video, audio and embedding models simplifies the plumbing: Quanta Inference is OpenAI-compatible, prepaid and priced in dollars.

Thresholds, budgets and the decision table

Set thresholds from data, and set budgets from volume. The decision table below is the version to hand a reviewer or paste into a runbook.

SituationFirst stepAccept ruleEscalate when
Prose finding, small editSimilarityBest score 0.85 or higher, any runner-upScore below 0.85
Code finding, any editSimilarityBest 0.85 or higher and clears runner-up by 0.10Duplicate windows or a close second
Rename or reformat touched many filesSimilarity, then modelExact-match offset from modelModel returns NOT_FOUND
Anchor text is short (under 25 characters)Skip similarity, go to modelExact-match offsetModel returns NOT_FOUND
Model call already attempted onceReviewerHuman yes/noNever loop the model
Regulated or audited contextReviewer for anything not 1.0Exact match onlyAny fuzzy accept escalates

Short anchors deserve their own row. A six-word snippet is not distinctive, and Fraser's pattern analysis explains why: brief patterns repeat often, so a short stored anchor produces multiple candidate windows and the runner-up margin rule will escalate nearly everything anyway. Send short anchors straight to the model, where the surrounding context lines do the disambiguating work.

Budget the ladder per batch rather than per finding. If your batch is 500 findings and your similarity step resolves 60%, you have 200 model calls and possibly 80 reviews. Put a hard cap on Step 2 calls per run and a queue limit on Step 3. When either cap is hit, stop and report the backlog rather than letting the run quietly degrade to "escalate everything."

Making the reviewer's step fast

A review that takes ten minutes per finding will not survive contact with a busy week. The reviewer should be answering one question on a prepared screen: is this the text the finding was about?

Give them three artifacts side by side, in the side-by-side format reviewers already read fluently in code review tools the IntelliJ diff viewer:

  • Left: the old snippet with its original context lines.
  • Right: the top candidate window with its score and offset.
  • Below: the finding's claim, quoted.

Then two actions: confirm (writes the new anchor) or reject (marks the finding as orphaned, with a reason code). Reason codes matter. After a few dozen rejections you will see a pattern, usually a rename or a file split, and you can add a mechanical rule that handles that pattern before it reaches a person.

Track three numbers per batch: the share resolved by similarity, the share resolved by the model, and the share that a reviewer rejects outright. Rising model share with a falling resolution rate is a signal that your stored snippets have aged badly and you should re-capture anchors more often, not tune the thresholds.

Where the reviewer's queue is really a queue of routine, repeatable judgments (re-checking anchors, classifying drift into rename, move or deletion), a specialist that reads your context and hands back finished deliverables removes the per-item setup cost: Quanta Experts is the natural next step for that layer, and Quanta covers the case where the whole pipeline needs a home.

Where the ladder fits in a pipeline

Two integration rules keep the ladder honest.

Never write an anchor you have not verified. Similarity accepts are verified by construction (you have the window text). Model accepts are verified by the offset check. Reviewer accepts are verified by a human. An anchor that skips all three is a guess with a line number attached.

Keep the old snippet. Every accepted re-anchor should retain the pre-edit snippet, the score, the step that resolved it, and a timestamp. When a later edit breaks the new anchor, the ladder restarts from a known-good position rather than from a cascade of guesses. This is the same reason diff tools keep original revisions browsable alongside the latest Review Board documentation.

The ladder also composes with what your diff tooling already computes. If it can tell you a block moved without changing, that is a 1.0 match for free, and the similarity step's job shrinks. If it can say a line was updated in place, that is a candidate for an update rule instead of a move rule the Rich Diff Checker.

FAQ

FAQ

Why not just ask the model first for every broken anchor?

Because it is the expensive step for a job that is usually deterministic. On the 50-finding fixture, similarity clears roughly 30 findings at zero token cost, and it returns a score you can log and audit. Reserving model calls for the cases similarity cannot resolve keeps input token spend on the part of the batch that needs judgment. The model is also less trustworthy without the validation check, which means you pay for the call and still need the verification.

What similarity threshold should I actually use?

Start at 0.85 with a runner-up margin of 0.10, then adjust against your own drift fixture. Raise the threshold for code and for short anchors, where duplicate windows are common; lower it is risky, because a false accept silently attaches a finding to the wrong lines. The exact number matters less than the pair of rules: a floor and a margin.

What if the model returns an offset that does not match the snippet?

Reject the result and send the finding to the reviewer. Do not re-ask. A model that paraphrased instead of copying, or guessed at a location, has given you no information about whether a second attempt would do better, and repeated calls are exactly the cost spiral the ladder exists to prevent. Log the rejection so you can see whether your prompt or your stored snippet is the problem.

How do I handle anchors inside very large files or monorepos?

Cap the input to the model at the relevant section plus context, and say so in the prompt, so token costs stay bounded. For similarity, slide by line-chunk first and refine character offsets only inside the winning chunk to keep the scan fast. If the file itself moved (path changed, split into modules), resolve the path first: anchoring a snippet into the wrong file wastes every step downstream.

When should the reviewer come before the model?

When the finding carries compliance weight, when the snippet is too short to be distinctive, or when the model has already failed once on the same anchor. In those cases a human decision is the cheaper path, because a wrong automated fix in an audited context costs far more than two minutes of review time.

Sources

Sources

  1. Reviewing Code (Diffs) | Review Board latest Documentation
  2. Rich Diff Checker
  3. Diff Viewer for files | IntelliJ IDEA Documentation
  4. Fuzzy matching algorithms
  5. Writing: Fuzzy Patch - Neil Fraser
  6. Prompt Caching for AI Agents: Cut LLM Costs
  7. Prompt Engineering Best Practices: Tutorial & Examples
  8. General Tips for Designing Prompts
  9. The guide to structured outputs and function calling with ...
  10. Structured Output & JSON Mode in LLMs: When to Use, ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles