FAILURE MAP
← Case archive

FA-75641 / Text diff and three-way merge / Open access

Line mapping through hunks: the line after a changed range is reported as removed · case 01

A comment on the first unchanged line after a hunk is dropped.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The in-range test includes the exclusive end of the old range.

VERIFIED REPAIR

An old line is inside the hunk when start <= line < start + length.

Unsuccessful approach: Excluding the last line of the range maps a deleted line as if it survived.

Case contract

Hunks [old_start, old_len, new_start, new_len] use unified conventions (start is the line before when the length is 0) and are sorted. An old line maps to None if it lies in a hunk's old range; otherwise it moves by the sum of new_len - old_len over hunks entirely before it. A zero-length hunk after line s affects lines from s+1 on. Return the mapped line numbers.

Why this case matters

Review tools re-anchor comments on changed files by mapping old line numbers through the diff.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(hunks, lines):
    res = []
    for ln in lines:
        shift = 0
        out = ln
        for os_, ol, ns, nl in hunks:
            s = os_ if ol > 0 else os_ + 1
            if ln < s:
                break
            if ln <= s + ol:
                out = None
                break
            shift += nl - ol
        res.append(None if out is None else ln + shift)
    return res
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 1]], [5, 6, 7]], [5, 7, 8]), ('pure deletion', [[[3, 1, 2, 0]], [2, 3, 3, 4, 10]], [2, None, None, 3, 9]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 1]], [1, 1])],
    2: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 2]], [5, 6, 7]], [5, 8, 9]), ('pure deletion', [[[3, 2, 2, 0]], [2, 3, 4, 5, 10]], [2, None, None, 3, 8]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 2]], [1, 2])],
    3: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 3]], [5, 6, 7]], [5, 9, 10]), ('pure deletion', [[[3, 3, 2, 0]], [2, 3, 5, 6, 10]], [2, None, None, 3, 7]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 3]], [1, 3])],
    4: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 4]], [5, 6, 7]], [5, 10, 11]), ('pure deletion', [[[3, 4, 2, 0]], [2, 3, 6, 7, 10]], [2, None, None, 3, 6]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 4]], [1, 4])],
    5: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 5]], [5, 6, 7]], [5, 11, 12]), ('pure deletion', [[[3, 5, 2, 0]], [2, 3, 7, 8, 10]], [2, None, None, 3, 5]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 5]], [1, 5])],
}[N]
for label, args, expected in cases:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
lines before and after a replacement[4, None, None, None, 9][4, None, None, 8, 9]Failed
pure insertion after line five[5, None, 8][5, 7, 8]Failed
pure deletion[2, None, None, None, 9][2, None, None, 3, 9]Failed
insertion at the top of the file[None, 4][3, 4]Failed
several hunks accumulate[1, None, None, 10, None, None, 19, None][1, None, 4, 10, None, 12, 19, 24]Failed
line just after a hunk[None, None, 13][None, 12, 13]Failed
last line of a deletion[None, None][None, 4]Failed
no hunks[1, 1][1, 1]Passed

SHA-256 / 721a30f51284aeccced13abfd0456e74320849bf716bab9e2b9a6a8b41d1c52c

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(hunks, lines):
    res = []
    for ln in lines:
        shift = 0
        out = ln
        for os_, ol, ns, nl in hunks:
            s = os_ if ol > 0 else os_ + 1
            if ln < s:
                break
            if ln < s + ol - 1:
                out = None
                break
            shift += nl - ol
        res.append(None if out is None else ln + shift)
    return res
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 1]], [5, 6, 7]], [5, 7, 8]), ('pure deletion', [[[3, 1, 2, 0]], [2, 3, 3, 4, 10]], [2, None, None, 3, 9]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 1]], [1, 1])],
    2: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 2]], [5, 6, 7]], [5, 8, 9]), ('pure deletion', [[[3, 2, 2, 0]], [2, 3, 4, 5, 10]], [2, None, None, 3, 8]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 2]], [1, 2])],
    3: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 3]], [5, 6, 7]], [5, 9, 10]), ('pure deletion', [[[3, 3, 2, 0]], [2, 3, 5, 6, 10]], [2, None, None, 3, 7]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 3]], [1, 3])],
    4: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 4]], [5, 6, 7]], [5, 10, 11]), ('pure deletion', [[[3, 4, 2, 0]], [2, 3, 6, 7, 10]], [2, None, None, 3, 6]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 4]], [1, 4])],
    5: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 5]], [5, 6, 7]], [5, 11, 12]), ('pure deletion', [[[3, 5, 2, 0]], [2, 3, 7, 8, 10]], [2, None, None, 3, 5]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 5]], [1, 5])],
}[N]
for label, args, expected in cases:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
lines before and after a replacement[4, None, 7, 8, 9][4, None, None, 8, 9]Failed
pure insertion after line five[5, 7, 8][5, 7, 8]Passed
pure deletion[2, 2, 2, 3, 9][2, None, None, 3, 9]Failed
insertion at the top of the file[3, 4][3, 4]Passed
several hunks accumulate[1, 3, 4, 10, None, 12, 19, 24][1, None, 4, 10, None, 12, 19, 24]Failed
line just after a hunk[11, 12, 13][None, 12, 13]Failed
last line of a deletion[3, 4][None, 4]Failed
no hunks[1, 1][1, 1]Passed

SHA-256 / 6a16f3b55e8a77392f78b8499b8435d1567a73dadaed2d0c5f0d7f5be50b7c17

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(hunks, lines):
    res = []
    for ln in lines:
        shift = 0
        out = ln
        for os_, ol, ns, nl in hunks:
            s = os_ if ol > 0 else os_ + 1
            if ln < s:
                break
            if ln < s + ol:
                out = None
                break
            shift += nl - ol
        res.append(None if out is None else ln + shift)
    return res
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 1]], [5, 6, 7]], [5, 7, 8]), ('pure deletion', [[[3, 1, 2, 0]], [2, 3, 3, 4, 10]], [2, None, None, 3, 9]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 1]], [1, 1])],
    2: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 2]], [5, 6, 7]], [5, 8, 9]), ('pure deletion', [[[3, 2, 2, 0]], [2, 3, 4, 5, 10]], [2, None, None, 3, 8]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 2]], [1, 2])],
    3: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 3]], [5, 6, 7]], [5, 9, 10]), ('pure deletion', [[[3, 3, 2, 0]], [2, 3, 5, 6, 10]], [2, None, None, 3, 7]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 3]], [1, 3])],
    4: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 4]], [5, 6, 7]], [5, 10, 11]), ('pure deletion', [[[3, 4, 2, 0]], [2, 3, 6, 7, 10]], [2, None, None, 3, 6]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 4]], [1, 4])],
    5: [('lines before and after a replacement', [[[5, 2, 5, 3]], [4, 5, 6, 7, 8]], [4, None, None, 8, 9]), ('pure insertion after line five', [[[5, 0, 6, 5]], [5, 6, 7]], [5, 11, 12]), ('pure deletion', [[[3, 5, 2, 0]], [2, 3, 7, 8, 10]], [2, None, None, 3, 5]), ('insertion at the top of the file', [[[0, 0, 1, 2]], [1, 2]], [3, 4]), ('several hunks accumulate', [[[2, 1, 2, 2], [10, 3, 11, 1], [20, 0, 19, 4]], [1, 2, 3, 9, 10, 13, 20, 21]], [1, None, 4, 10, None, 12, 19, 24]), ('line just after a hunk', [[[7, 2, 7, 5]], [8, 9, 10]], [None, 12, 13]), ('last line of a deletion', [[[4, 3, 3, 0]], [6, 7]], [None, 4]), ('no hunks', [[], [1, 5]], [1, 5])],
}[N]
for label, args, expected in cases:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
lines before and after a replacement[4, None, None, 8, 9][4, None, None, 8, 9]Passed
pure insertion after line five[5, 7, 8][5, 7, 8]Passed
pure deletion[2, None, None, 3, 9][2, None, None, 3, 9]Passed
insertion at the top of the file[3, 4][3, 4]Passed
several hunks accumulate[1, None, 4, 10, None, 12, 19, 24][1, None, 4, 10, None, 12, 19, 24]Passed
line just after a hunk[None, 12, 13][None, 12, 13]Passed
last line of a deletion[None, 4][None, 4]Passed
no hunks[1, 1][1, 1]Passed

SHA-256 / aab39124f240e0d5b7f21f67c12b75c82c38c65ca3b9144132e6026cc1d781f7

Verification & scope

A deterministic, bounded teaching model of one diff, patch or merge rule with stipulated conventions; it is not a production diff or version-control implementation and makes no claim of conformance to any specific tool. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:49:08.856868+00:00.

Case digest / 720fc15d82ab898a3fa190e72ba5d0b3fef96c37e50bfc461038244117a4652d