broken_source:
"""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[0] == y[0]
    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)

source:
"""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[0] == y[0] and x[1] >= y[1]
    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)

prompt:
Adding or removing the final newline produces an empty diff.

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.

split: open-access


broken_source:
"""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 and tag == '-':
            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)

meaning: Fixture-count band, not measured model difficulty

deps: stdlib

source:
"""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 and tag != ' ':
            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)

prompt:
An added last line without newline is shown as if it had one.

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.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import bisect
import collections
N = 1
observations = []
def solve(a, b):
    ca = collections.Counter(a)
    cb = collections.Counter(b)
    posb = {x: j for j, x in enumerate(b)}
    cand = [(i, posb[x]) for i, x in enumerate(a) if ca[x] == 1 and x in cb]
    tails = []
    back = {}
    for idx, (i, j) in enumerate(cand):
        lo = bisect.bisect_left([cand[t][1] for t in tails], j)
        if lo == len(tails):
            tails.append(idx)
        else:
            tails[lo] = idx
        back[idx] = tails[lo - 1] if lo > 0 else None
    res = []
    t = tails[-1] if tails else None
    while t is not None:
        res.append(list(cand[t]))
        t = back[t]
    return res[::-1]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'k'], ['k', 'm0', 'm1', 'm2']], [[0, 1], [1, 2], [2, 3]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    2: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'k'], ['k', 'm0', 'm1', 'm2', 'm3']], [[0, 1], [1, 2], [2, 3], [3, 4]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    3: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    4: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    5: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
}[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)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import bisect
import collections
N = 1
observations = []
def solve(a, b):
    ca = collections.Counter(a)
    cb = collections.Counter(b)
    posb = {x: j for j, x in enumerate(b)}
    cand = [(i, posb[x]) for i, x in enumerate(a) if cb.get(x, 0) == 1]
    tails = []
    back = {}
    for idx, (i, j) in enumerate(cand):
        lo = bisect.bisect_left([cand[t][1] for t in tails], j)
        if lo == len(tails):
            tails.append(idx)
        else:
            tails[lo] = idx
        back[idx] = tails[lo - 1] if lo > 0 else None
    res = []
    t = tails[-1] if tails else None
    while t is not None:
        res.append(list(cand[t]))
        t = back[t]
    return res[::-1]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'k'], ['k', 'm0', 'm1', 'm2']], [[0, 1], [1, 2], [2, 3]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    2: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'k'], ['k', 'm0', 'm1', 'm2', 'm3']], [[0, 1], [1, 2], [2, 3], [3, 4]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    3: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    4: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    5: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
}[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)

prompt:
A line duplicated in the new file anchors to its last copy and misaligns the diff.

Candidates are lines occurring exactly once in the old side and exactly once in the new side, as (old index, new index) pairs in old order. Keep the longest chain with increasing new index, found with patience piles (bisect on pile tops by new index, back pointers to the previous pile top) and reconstructed from the top of the last pile. Return the anchor pairs.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import bisect
import collections
N = 1
observations = []
def solve(a, b):
    ca = collections.Counter(a)
    cb = collections.Counter(b)
    posb = {x: j for j, x in enumerate(b)}
    cand = [(i, posb[x]) for i, x in enumerate(a) if ca[x] == 1 and cb.get(x, 0) == 1]
    tails = []
    back = {}
    for idx, (i, j) in enumerate(cand):
        lo = bisect.bisect_left([cand[t][0] for t in tails], i)
        if lo == len(tails):
            tails.append(idx)
        else:
            tails[lo] = idx
        back[idx] = tails[lo - 1] if lo > 0 else None
    res = []
    t = tails[-1] if tails else None
    while t is not None:
        res.append(list(cand[t]))
        t = back[t]
    return res[::-1]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'k'], ['k', 'm0', 'm1', 'm2']], [[0, 1], [1, 2], [2, 3]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    2: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'k'], ['k', 'm0', 'm1', 'm2', 'm3']], [[0, 1], [1, 2], [2, 3], [3, 4]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    3: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    4: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    5: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
}[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)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import bisect
import collections
N = 1
observations = []
def solve(a, b):
    ca = collections.Counter(a)
    cb = collections.Counter(b)
    posb = {x: j for j, x in enumerate(b)}
    cand = [(i, posb[x]) for i, x in enumerate(a) if ca[x] == 1 and cb.get(x, 0) == 1]
    tails = []
    back = {}
    for idx, (i, j) in enumerate(cand):
        lo = bisect.bisect_left([cand[t][0] for t in tails], j)
        if lo == len(tails):
            tails.append(idx)
        else:
            tails[lo] = idx
        back[idx] = tails[lo - 1] if lo > 0 else None
    res = []
    t = tails[-1] if tails else None
    while t is not None:
        res.append(list(cand[t]))
        t = back[t]
    return res[::-1]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'k'], ['k', 'm0', 'm1', 'm2']], [[0, 1], [1, 2], [2, 3]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    2: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'k'], ['k', 'm0', 'm1', 'm2', 'm3']], [[0, 1], [1, 2], [2, 3], [3, 4]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    3: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    4: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    5: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
}[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)

prompt:
Crossing unique lines are all kept as anchors, producing an inconsistent alignment.

Candidates are lines occurring exactly once in the old side and exactly once in the new side, as (old index, new index) pairs in old order. Keep the longest chain with increasing new index, found with patience piles (bisect on pile tops by new index, back pointers to the previous pile top) and reconstructed from the top of the last pile. Return the anchor pairs.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import bisect
import collections
N = 1
observations = []
def solve(a, b):
    ca = collections.Counter(a)
    cb = collections.Counter(b)
    posb = {x: j for j, x in enumerate(b)}
    cand = [(i, posb[x]) for i, x in enumerate(a) if ca[x] == 1 and cb.get(x, 0) == 1]
    tails = []
    back = {}
    for idx, (i, j) in enumerate(cand):
        lo = bisect.bisect_left([cand[t][1] for t in tails], j)
        if lo == len(tails):
            tails.append(idx)
        else:
            tails[lo] = idx
        back[idx] = tails[lo - 1] if lo > 0 else None
    res = []
    t = tails[0] if tails else None
    while t is not None:
        res.append(list(cand[t]))
        t = back[t]
    return res[::-1]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'k'], ['k', 'm0', 'm1', 'm2']], [[0, 1], [1, 2], [2, 3]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    2: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'k'], ['k', 'm0', 'm1', 'm2', 'm3']], [[0, 1], [1, 2], [2, 3], [3, 4]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    3: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    4: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    5: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
}[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)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import bisect
import collections
N = 1
observations = []
def solve(a, b):
    ca = collections.Counter(a)
    cb = collections.Counter(b)
    posb = {x: j for j, x in enumerate(b)}
    cand = [(i, posb[x]) for i, x in enumerate(a) if ca[x] == 1 and cb.get(x, 0) == 1]
    tails = []
    back = {}
    for idx, (i, j) in enumerate(cand):
        lo = bisect.bisect_left([cand[t][1] for t in tails], j)
        if lo == len(tails):
            tails.append(idx)
        else:
            tails[lo] = idx
        back[idx] = tails[lo - 1] if lo > 0 else None
    res = []
    t = len(cand) - 1 if cand else None
    while t is not None:
        res.append(list(cand[t]))
        t = back[t]
    return res[::-1]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'k'], ['k', 'm0', 'm1', 'm2']], [[0, 1], [1, 2], [2, 3]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    2: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'k'], ['k', 'm0', 'm1', 'm2', 'm3']], [[0, 1], [1, 2], [2, 3], [3, 4]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    3: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    4: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['u', 'v', 'w', 'p']], [[0, 0], [1, 1], [2, 2], [3, 3]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
    5: [('unique lines in order', [['a', 'b', 'c', 'd'], ['a', 'x', 'c', 'd']], [[0, 0], [2, 2], [3, 3]]), ('crossing unique lines keep the longest chain', [['f1', 'f2', 'f3', 'g'], ['f2', 'f3', 'f1', 'g']], [[1, 0], [2, 1], [3, 3]]), ('duplicated in new side are not anchors', [['x', 'y', 'z'], ['y', 'x', 'y', 'z']], [[0, 1], [2, 3]]), ('duplicated in old side are not anchors', [['}', 'a', '}', 'b'], ['}', 'b', 'a']], [[3, 1]]), ('no unique common lines', [['r', 'r'], ['s', 's']], []), ('chain ends before the last candidate', [['u', 'v', 'w', 'p'], ['v', 'w', 'p', 'u']], [[1, 0], [2, 1], [3, 2]]), ('later short chain', [['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'k'], ['k', 'm0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6']], [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7]]), ('single candidate', [['q', 'z'], ['z', 'q', 'q']], [[1, 0]])],
}[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)

prompt:
Only a single anchor is returned even when a long chain exists.

Candidates are lines occurring exactly once in the old side and exactly once in the new side, as (old index, new index) pairs in old order. Keep the longest chain with increasing new index, found with patience piles (bisect on pile tops by new index, back pointers to the previous pile top) and reconstructed from the top of the last pile. Return the anchor pairs.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(files, width):
    mx = max([f[1] + f[2] for f in files if f[1] is not None] + [0])
    lines = []
    ins = dels = changed = 0
    for path, add, rem in files:
        changed += 1
        if add is None:
            lines.append('%s | Bin' % path)
            continue
        ins += add
        dels += rem
        if mx > width:
            pa = add * width // mx
            pr = rem * width // mx
        else:
            pa, pr = add, rem
        lines.append('%s | %d %s' % (path, add + rem, '+' * pa + '-' * pr))
    summary = '%d file%s changed' % (changed, '' if changed == 1 else 's')
    if ins:
        summary += ', %d insertion%s(+)' % (ins, '' if ins == 1 else 's')
    if dels:
        summary += ', %d deletion%s(-)' % (dels, '' if dels == 1 else 's')
    lines.append(summary)
    return lines
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 5]], 40], ['gone | 5 -----', '1 file changed, 5 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 10, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 10 ++', '2 files changed, 40 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 1, 1]], 5], ['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img1.png', None, None]], 40], ['img1.png | Bin', '1 file changed'])],
    2: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 6]], 40], ['gone | 6 ------', '1 file changed, 6 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 20, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 20 +++++', '2 files changed, 50 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 2, 2]], 5], ['p | 1 +', 'q | 1 -', 'r | 4 ++--', '3 files changed, 3 insertions(+), 3 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img2.png', None, None]], 40], ['img2.png | Bin', '1 file changed'])],
    3: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 7]], 40], ['gone | 7 -------', '1 file changed, 7 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 30, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 30 +++++++', '2 files changed, 60 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 3, 3]], 5], ['p | 1 +', 'q | 1 -', 'r | 6 ++--', '3 files changed, 4 insertions(+), 4 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img3.png', None, None]], 40], ['img3.png | Bin', '1 file changed'])],
    4: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 8]], 40], ['gone | 8 --------', '1 file changed, 8 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 40, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 40 ++++++++++', '2 files changed, 70 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 4, 4]], 5], ['p | 1 +', 'q | 1 -', 'r | 8 ++--', '3 files changed, 5 insertions(+), 5 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img4.png', None, None]], 40], ['img4.png | Bin', '1 file changed'])],
    5: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 9]], 40], ['gone | 9 ---------', '1 file changed, 9 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 50, 0]], 10], ['f1 | 40 ++++++--', 'f2 | 50 ++++++++++', '2 files changed, 80 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 5, 5]], 5], ['p | 1 +', 'q | 1 -', 'r | 10 ++--', '3 files changed, 6 insertions(+), 6 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img5.png', None, None]], 40], ['img5.png | Bin', '1 file changed'])],
}[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)

source:
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(files, width):
    mx = max([f[1] + f[2] for f in files if f[1] is not None] + [0])
    lines = []
    ins = dels = changed = 0
    for path, add, rem in files:
        changed += 1
        if add is None:
            lines.append('%s | Bin' % path)
            continue
        ins += add
        dels += rem
        if mx > width:
            pa = add * width // mx
            pr = rem * width // mx
            if not pa:
                pa = 1
            if not pr:
                pr = 1
        else:
            pa, pr = add, rem
        lines.append('%s | %d %s' % (path, add + rem, '+' * pa + '-' * pr))
    summary = '%d file%s changed' % (changed, '' if changed == 1 else 's')
    if ins:
        summary += ', %d insertion%s(+)' % (ins, '' if ins == 1 else 's')
    if dels:
        summary += ', %d deletion%s(-)' % (dels, '' if dels == 1 else 's')
    lines.append(summary)
    return lines
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 5]], 40], ['gone | 5 -----', '1 file changed, 5 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 10, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 10 ++', '2 files changed, 40 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 1, 1]], 5], ['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img1.png', None, None]], 40], ['img1.png | Bin', '1 file changed'])],
    2: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 6]], 40], ['gone | 6 ------', '1 file changed, 6 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 20, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 20 +++++', '2 files changed, 50 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 2, 2]], 5], ['p | 1 +', 'q | 1 -', 'r | 4 ++--', '3 files changed, 3 insertions(+), 3 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img2.png', None, None]], 40], ['img2.png | Bin', '1 file changed'])],
    3: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 7]], 40], ['gone | 7 -------', '1 file changed, 7 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 30, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 30 +++++++', '2 files changed, 60 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 3, 3]], 5], ['p | 1 +', 'q | 1 -', 'r | 6 ++--', '3 files changed, 4 insertions(+), 4 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img3.png', None, None]], 40], ['img3.png | Bin', '1 file changed'])],
    4: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 8]], 40], ['gone | 8 --------', '1 file changed, 8 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 40, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 40 ++++++++++', '2 files changed, 70 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 4, 4]], 5], ['p | 1 +', 'q | 1 -', 'r | 8 ++--', '3 files changed, 5 insertions(+), 5 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img4.png', None, None]], 40], ['img4.png | Bin', '1 file changed'])],
    5: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 9]], 40], ['gone | 9 ---------', '1 file changed, 9 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 50, 0]], 10], ['f1 | 40 ++++++--', 'f2 | 50 ++++++++++', '2 files changed, 80 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 5, 5]], 5], ['p | 1 +', 'q | 1 -', 'r | 10 ++--', '3 files changed, 6 insertions(+), 6 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img5.png', None, None]], 40], ['img5.png | Bin', '1 file changed'])],
}[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)

prompt:
A file with one changed line shows no marks next to a large file.

Files are [path, added, removed], with None counts for binary files. Each text file prints "path | total +++---"; when the largest total exceeds width, both counts scale by width/max with floor, but a non-zero count keeps at least one mark. Binary files print "path | Bin". The summary counts files and, when non-zero, insertions and deletions with singular/plural wording.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(files, width):
    mx = max([f[1] + f[2] for f in files if f[1] is not None] + [0])
    lines = []
    ins = dels = changed = 0
    for path, add, rem in files:
        changed += 1
        if add is None:
            lines.append('%s | Bin' % path)
            continue
        ins += add
        dels += rem
        if mx > width:
            pa = add * width // (add + rem)
            pr = rem * width // (add + rem)
            if add and not pa:
                pa = 1
            if rem and not pr:
                pr = 1
        else:
            pa, pr = add, rem
        lines.append('%s | %d %s' % (path, add + rem, '+' * pa + '-' * pr))
    summary = '%d file%s changed' % (changed, '' if changed == 1 else 's')
    if ins:
        summary += ', %d insertion%s(+)' % (ins, '' if ins == 1 else 's')
    if dels:
        summary += ', %d deletion%s(-)' % (dels, '' if dels == 1 else 's')
    lines.append(summary)
    return lines
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 5]], 40], ['gone | 5 -----', '1 file changed, 5 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 10, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 10 ++', '2 files changed, 40 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 1, 1]], 5], ['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img1.png', None, None]], 40], ['img1.png | Bin', '1 file changed'])],
    2: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 6]], 40], ['gone | 6 ------', '1 file changed, 6 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 20, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 20 +++++', '2 files changed, 50 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 2, 2]], 5], ['p | 1 +', 'q | 1 -', 'r | 4 ++--', '3 files changed, 3 insertions(+), 3 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img2.png', None, None]], 40], ['img2.png | Bin', '1 file changed'])],
    3: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 7]], 40], ['gone | 7 -------', '1 file changed, 7 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 30, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 30 +++++++', '2 files changed, 60 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 3, 3]], 5], ['p | 1 +', 'q | 1 -', 'r | 6 ++--', '3 files changed, 4 insertions(+), 4 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img3.png', None, None]], 40], ['img3.png | Bin', '1 file changed'])],
    4: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 8]], 40], ['gone | 8 --------', '1 file changed, 8 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 40, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 40 ++++++++++', '2 files changed, 70 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 4, 4]], 5], ['p | 1 +', 'q | 1 -', 'r | 8 ++--', '3 files changed, 5 insertions(+), 5 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img4.png', None, None]], 40], ['img4.png | Bin', '1 file changed'])],
    5: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 9]], 40], ['gone | 9 ---------', '1 file changed, 9 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 50, 0]], 10], ['f1 | 40 ++++++--', 'f2 | 50 ++++++++++', '2 files changed, 80 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 5, 5]], 5], ['p | 1 +', 'q | 1 -', 'r | 10 ++--', '3 files changed, 6 insertions(+), 6 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img5.png', None, None]], 40], ['img5.png | Bin', '1 file changed'])],
}[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)

source:
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(files, width):
    mx = max([f[1] + f[2] for f in files if f[1] is not None] + [0])
    lines = []
    ins = dels = changed = 0
    for path, add, rem in files:
        changed += 1
        if add is None:
            lines.append('%s | Bin' % path)
            continue
        ins += add
        dels += rem
        if mx > width:
            pa = round(add * width / mx)
            pr = round(rem * width / mx)
            if add and not pa:
                pa = 1
            if rem and not pr:
                pr = 1
        else:
            pa, pr = add, rem
        lines.append('%s | %d %s' % (path, add + rem, '+' * pa + '-' * pr))
    summary = '%d file%s changed' % (changed, '' if changed == 1 else 's')
    if ins:
        summary += ', %d insertion%s(+)' % (ins, '' if ins == 1 else 's')
    if dels:
        summary += ', %d deletion%s(-)' % (dels, '' if dels == 1 else 's')
    lines.append(summary)
    return lines
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = {
    1: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 5]], 40], ['gone | 5 -----', '1 file changed, 5 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 10, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 10 ++', '2 files changed, 40 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 1, 1]], 5], ['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img1.png', None, None]], 40], ['img1.png | Bin', '1 file changed'])],
    2: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 6]], 40], ['gone | 6 ------', '1 file changed, 6 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 20, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 20 +++++', '2 files changed, 50 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 2, 2]], 5], ['p | 1 +', 'q | 1 -', 'r | 4 ++--', '3 files changed, 3 insertions(+), 3 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img2.png', None, None]], 40], ['img2.png | Bin', '1 file changed'])],
    3: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 7]], 40], ['gone | 7 -------', '1 file changed, 7 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 30, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 30 +++++++', '2 files changed, 60 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 3, 3]], 5], ['p | 1 +', 'q | 1 -', 'r | 6 ++--', '3 files changed, 4 insertions(+), 4 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img3.png', None, None]], 40], ['img3.png | Bin', '1 file changed'])],
    4: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 8]], 40], ['gone | 8 --------', '1 file changed, 8 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 40, 0]], 10], ['f1 | 40 +++++++--', 'f2 | 40 ++++++++++', '2 files changed, 70 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 4, 4]], 5], ['p | 1 +', 'q | 1 -', 'r | 8 ++--', '3 files changed, 5 insertions(+), 5 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img4.png', None, None]], 40], ['img4.png | Bin', '1 file changed'])],
    5: [('small changes without scaling', [[['a.txt', 3, 1]], 40], ['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']), ('scaled bar keeps small counts visible', [[['big.c', 200, 0], ['tiny.c', 1, 1]], 20], ['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']), ('binary file', [[['logo.png', None, None], ['x', 2, 0]], 40], ['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']), ('single insertion grammar', [[['one', 1, 0]], 40], ['one | 1 +', '1 file changed, 1 insertion(+)']), ('only deletions', [[['gone', 0, 9]], 40], ['gone | 9 ---------', '1 file changed, 9 deletions(-)']), ('scale uses the largest file', [[['f1', 30, 10], ['f2', 50, 0]], 10], ['f1 | 40 ++++++--', 'f2 | 50 ++++++++++', '2 files changed, 80 insertions(+), 10 deletions(-)']), ('several files', [[['p', 1, 0], ['q', 0, 1], ['r', 5, 5]], 5], ['p | 1 +', 'q | 1 -', 'r | 10 ++--', '3 files changed, 6 insertions(+), 6 deletions(-)']), ('unscaled wide change', [[['w', 12, 3]], 15], ['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']), ('floor not rounding when scaling', [[['r', 7, 0], ['s', 20, 0]], 10], ['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']), ('binary-only change', [[['img5.png', None, None]], 40], ['img5.png | Bin', '1 file changed'])],
}[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)

prompt:
Every changed file fills the whole width, so bars no longer compare sizes.

Files are [path, added, removed], with None counts for binary files. Each text file prints "path | total +++---"; when the largest total exceeds width, both counts scale by width/max with floor, but a non-zero count keeps at least one mark. Binary files print "path | Bin". The summary counts files and, when non-zero, insertions and deletions with singular/plural wording.

split: open-access