FAILURE MAP
← Case archive

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

End-of-file newline diff: a terminated file gains an empty unterminated last line · case 01

Every newline-terminated file shows a phantom empty line with a missing-newline marker.

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

ROOT CAUSE

The final piece after the last newline is always tokenized, even when it is empty.

VERIFIED REPAIR

Emit a final unterminated token only when the text after the last newline is non-empty.

Unsuccessful approach: Using splitlines marks every line as terminated and loses the missing-newline information.

Case contract

Split each text on "\n" into [line, terminated] tokens; a final empty piece produces no token, a final non-empty piece is an unterminated token. Diff the token lists with an LCS walk (tokens equal only if text and terminator flag both match, deletions before insertions on ties) and render each op as tag + text, followed by "\ No newline at end of file" after any unterminated token.

Why this case matters

Unified diffs must show a change that only adds or removes the final newline, and mark which side lacks it.

1 / The failure

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

N = 1
observations = []
def solve(old, new):
    def toks(s):
        parts = s.split('\n')
        res = [[p, True] for p in parts[:-1]]
        res.append([parts[-1], False])
        return res
    def same(x, y):
        return x == y
    A, B = toks(old), toks(new)
    n, m = len(A), len(B)
    T = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(n - 1, -1, -1):
        for j in range(m - 1, -1, -1):
            T[i][j] = T[i + 1][j + 1] + 1 if same(A[i], B[j]) else max(T[i + 1][j], T[i][j + 1])
    ops = []
    i = j = 0
    while i < n or j < m:
        if i < n and j < m and same(A[i], B[j]):
            ops.append((' ', A[i])); i += 1; j += 1
        elif i < n and (j >= m or T[i + 1][j] >= T[i][j + 1]):
            ops.append(('-', A[i])); i += 1
        else:
            ops.append(('+', B[j])); j += 1
    out = []
    for tag, (text, nl) in ops:
        out.append(tag + text)
        if not nl:
            out.append('\\ No newline at end of file')
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only1'], ['+only1', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nold', 'k\nnew'], [' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    2: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only2'], ['+only2', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nold', 'k\nk\nnew'], [' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    3: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only3'], ['+only3', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nk\nold', 'k\nk\nk\nnew'], [' k', ' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    4: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only4'], ['+only4', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nk\nk\nold', 'k\nk\nk\nk\nnew'], [' k', ' k', ' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    5: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only5'], ['+only5', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nk\nk\nk\nold', 'k\nk\nk\nk\nk\nnew'], [' k', ' k', ' k', ' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
}[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
newline added at end of file[' a', '-b', '\\ No newline at end of file', '+b', '+', '\\ No newline at end of file'][' a', '-b', '\\ No newline at end of file', '+b']Failed
newline removed at end of file[' a', '-b', '-', '\\ No newline at end of file', '+b', '\\ No newline at end of file'][' a', '-b', '+b', '\\ No newline at end of file']Failed
unchanged last line without newline['-x', '+X', ' y', '\\ No newline at end of file']['-x', '+X', ' y', '\\ No newline at end of file']Passed
both terminated[' p', '-q', '+r', ' ', '\\ No newline at end of file'][' p', '-q', '+r']Failed
empty to one unterminated line['-', '\\ No newline at end of file', '+only1', '\\ No newline at end of file']['+only1', '\\ No newline at end of file']Failed
last line changed and unterminated[' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file'][' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']Passed
trailing blank line[' a', '-', ' ', '\\ No newline at end of file'][' a', '-']Failed
identical texts[' s', ' t', ' ', '\\ No newline at end of file'][' s', ' t']Failed

SHA-256 / ef30a368ca09ccdc3a58c5d78b003540f110dfc0a394571bf661715fa29163b3

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(old, new):
    def toks(s):
        parts = s.split('\n')
        res = [[p, True] for p in s.splitlines()]
        return res
    def same(x, y):
        return x == y
    A, B = toks(old), toks(new)
    n, m = len(A), len(B)
    T = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(n - 1, -1, -1):
        for j in range(m - 1, -1, -1):
            T[i][j] = T[i + 1][j + 1] + 1 if same(A[i], B[j]) else max(T[i + 1][j], T[i][j + 1])
    ops = []
    i = j = 0
    while i < n or j < m:
        if i < n and j < m and same(A[i], B[j]):
            ops.append((' ', A[i])); i += 1; j += 1
        elif i < n and (j >= m or T[i + 1][j] >= T[i][j + 1]):
            ops.append(('-', A[i])); i += 1
        else:
            ops.append(('+', B[j])); j += 1
    out = []
    for tag, (text, nl) in ops:
        out.append(tag + text)
        if not nl:
            out.append('\\ No newline at end of file')
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only1'], ['+only1', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nold', 'k\nnew'], [' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    2: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only2'], ['+only2', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nold', 'k\nk\nnew'], [' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    3: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only3'], ['+only3', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nk\nold', 'k\nk\nk\nnew'], [' k', ' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    4: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only4'], ['+only4', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nk\nk\nold', 'k\nk\nk\nk\nnew'], [' k', ' k', ' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    5: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only5'], ['+only5', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nk\nk\nk\nold', 'k\nk\nk\nk\nk\nnew'], [' k', ' k', ' k', ' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
}[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
newline added at end of file[' a', ' b'][' a', '-b', '\\ No newline at end of file', '+b']Failed
newline removed at end of file[' a', ' b'][' a', '-b', '+b', '\\ No newline at end of file']Failed
unchanged last line without newline['-x', '+X', ' y']['-x', '+X', ' y', '\\ No newline at end of file']Failed
both terminated[' p', '-q', '+r'][' p', '-q', '+r']Passed
empty to one unterminated line['+only1']['+only1', '\\ No newline at end of file']Failed
last line changed and unterminated[' k', '-old', '+new'][' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']Failed
trailing blank line[' a', '-'][' a', '-']Passed
identical texts[' s', ' t'][' s', ' t']Passed

SHA-256 / 854b278c1ef7115fe18c55bbe68c12ad4c43d24808074f399a0e420559a9c84e

3 / The verified repair

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

N = 1
observations = []
def solve(old, new):
    def toks(s):
        parts = s.split('\n')
        res = [[p, True] for p in parts[:-1]]
        if parts[-1] != '':
            res.append([parts[-1], False])
        return res
    def same(x, y):
        return x == y
    A, B = toks(old), toks(new)
    n, m = len(A), len(B)
    T = [[0] * (m + 1) for _ in range(n + 1)]
    for i in range(n - 1, -1, -1):
        for j in range(m - 1, -1, -1):
            T[i][j] = T[i + 1][j + 1] + 1 if same(A[i], B[j]) else max(T[i + 1][j], T[i][j + 1])
    ops = []
    i = j = 0
    while i < n or j < m:
        if i < n and j < m and same(A[i], B[j]):
            ops.append((' ', A[i])); i += 1; j += 1
        elif i < n and (j >= m or T[i + 1][j] >= T[i][j + 1]):
            ops.append(('-', A[i])); i += 1
        else:
            ops.append(('+', B[j])); j += 1
    out = []
    for tag, (text, nl) in ops:
        out.append(tag + text)
        if not nl:
            out.append('\\ No newline at end of file')
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only1'], ['+only1', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nold', 'k\nnew'], [' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    2: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only2'], ['+only2', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nold', 'k\nk\nnew'], [' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    3: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only3'], ['+only3', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nk\nold', 'k\nk\nk\nnew'], [' k', ' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    4: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only4'], ['+only4', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nk\nk\nold', 'k\nk\nk\nk\nnew'], [' k', ' k', ' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
    5: [('newline added at end of file', ['a\nb', 'a\nb\n'], [' a', '-b', '\\ No newline at end of file', '+b']), ('newline removed at end of file', ['a\nb\n', 'a\nb'], [' a', '-b', '+b', '\\ No newline at end of file']), ('unchanged last line without newline', ['x\ny', 'X\ny'], ['-x', '+X', ' y', '\\ No newline at end of file']), ('both terminated', ['p\nq\n', 'p\nr\n'], [' p', '-q', '+r']), ('empty to one unterminated line', ['', 'only5'], ['+only5', '\\ No newline at end of file']), ('last line changed and unterminated', ['k\nk\nk\nk\nk\nold', 'k\nk\nk\nk\nk\nnew'], [' k', ' k', ' k', ' k', ' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']), ('trailing blank line', ['a\n\n', 'a\n'], [' a', '-']), ('identical texts', ['s\nt\n', 's\nt\n'], [' s', ' t'])],
}[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
newline added at end of file[' a', '-b', '\\ No newline at end of file', '+b'][' a', '-b', '\\ No newline at end of file', '+b']Passed
newline removed at end of file[' a', '-b', '+b', '\\ No newline at end of file'][' a', '-b', '+b', '\\ No newline at end of file']Passed
unchanged last line without newline['-x', '+X', ' y', '\\ No newline at end of file']['-x', '+X', ' y', '\\ No newline at end of file']Passed
both terminated[' p', '-q', '+r'][' p', '-q', '+r']Passed
empty to one unterminated line['+only1', '\\ No newline at end of file']['+only1', '\\ No newline at end of file']Passed
last line changed and unterminated[' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file'][' k', '-old', '\\ No newline at end of file', '+new', '\\ No newline at end of file']Passed
trailing blank line[' a', '-'][' a', '-']Passed
identical texts[' s', ' t'][' s', ' t']Passed

SHA-256 / 3a8d8a863b6d5480c8e26c56d9572c3e18ed3e622d909e232de99c8798f60f72

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:07.030183+00:00.

Case digest / d59cf38f20eecf4ad639ab73a348067ce475340522785327e31a0690f8bff5d3