FAILURE MAP
← Case archive

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

Diffstat histogram: each bar is scaled to its own file total · case 01

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

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

ROOT CAUSE

Scaling divides by the file's own total instead of the largest total.

VERIFIED REPAIR

Scale all bars by the largest file total, flooring the result.

Unsuccessful approach: Rounding instead of flooring draws bars one mark longer than the stipulated scale.

Case contract

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.

Why this case matters

Diffstat output is how reviewers size a change; scaling and pluralization rules are easy to get subtly wrong.

1 / The failure

Exit 1
"""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)
Boundary fixtureActualExpectedOutcome
small changes without scaling['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']Passed
scaled bar keeps small counts visible['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 ++++++++++----------', '2 files changed, 201 insertions(+), 1 deletion(-)']['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']Failed
binary file['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']Passed
single insertion grammar['one | 1 +', '1 file changed, 1 insertion(+)']['one | 1 +', '1 file changed, 1 insertion(+)']Passed
only deletions['gone | 5 -----', '1 file changed, 5 deletions(-)']['gone | 5 -----', '1 file changed, 5 deletions(-)']Passed
scale uses the largest file['f1 | 40 +++++++--', 'f2 | 10 ++++++++++', '2 files changed, 40 insertions(+), 10 deletions(-)']['f1 | 40 +++++++--', 'f2 | 10 ++', '2 files changed, 40 insertions(+), 10 deletions(-)']Failed
several files['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']Passed
unscaled wide change['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']Passed
floor not rounding when scaling['r | 7 ++++++++++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']Failed
binary-only change['img1.png | Bin', '1 file changed']['img1.png | Bin', '1 file changed']Passed

SHA-256 / 194e75c4bf6658f1d6a1eb8a2f855bd1ca853accad8c162faf2cbe0fe7eb9d6b

2 / The unsuccessful fix

Exit 1
"""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)
Boundary fixtureActualExpectedOutcome
small changes without scaling['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']Passed
scaled bar keeps small counts visible['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']Passed
binary file['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']Passed
single insertion grammar['one | 1 +', '1 file changed, 1 insertion(+)']['one | 1 +', '1 file changed, 1 insertion(+)']Passed
only deletions['gone | 5 -----', '1 file changed, 5 deletions(-)']['gone | 5 -----', '1 file changed, 5 deletions(-)']Passed
scale uses the largest file['f1 | 40 ++++++++--', 'f2 | 10 ++', '2 files changed, 40 insertions(+), 10 deletions(-)']['f1 | 40 +++++++--', 'f2 | 10 ++', '2 files changed, 40 insertions(+), 10 deletions(-)']Failed
several files['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']Passed
unscaled wide change['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']Passed
floor not rounding when scaling['r | 7 ++++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']Failed
binary-only change['img1.png | Bin', '1 file changed']['img1.png | Bin', '1 file changed']Passed

SHA-256 / 22ee2f338b72e0a92d8f1fb7e22072bbf7cb8fe13bc298dea0b26c8e92a5808f

3 / The verified repair

Exit 0
"""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 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)
Boundary fixtureActualExpectedOutcome
small changes without scaling['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']['a.txt | 4 +++-', '1 file changed, 3 insertions(+), 1 deletion(-)']Passed
scaled bar keeps small counts visible['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']['big.c | 200 ++++++++++++++++++++', 'tiny.c | 2 +-', '2 files changed, 201 insertions(+), 1 deletion(-)']Passed
binary file['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']['logo.png | Bin', 'x | 2 ++', '2 files changed, 2 insertions(+)']Passed
single insertion grammar['one | 1 +', '1 file changed, 1 insertion(+)']['one | 1 +', '1 file changed, 1 insertion(+)']Passed
only deletions['gone | 5 -----', '1 file changed, 5 deletions(-)']['gone | 5 -----', '1 file changed, 5 deletions(-)']Passed
scale uses the largest file['f1 | 40 +++++++--', 'f2 | 10 ++', '2 files changed, 40 insertions(+), 10 deletions(-)']['f1 | 40 +++++++--', 'f2 | 10 ++', '2 files changed, 40 insertions(+), 10 deletions(-)']Passed
several files['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']['p | 1 +', 'q | 1 -', 'r | 2 +-', '3 files changed, 2 insertions(+), 2 deletions(-)']Passed
unscaled wide change['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']['w | 15 ++++++++++++---', '1 file changed, 12 insertions(+), 3 deletions(-)']Passed
floor not rounding when scaling['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']['r | 7 +++', 's | 20 ++++++++++', '2 files changed, 27 insertions(+)']Passed
binary-only change['img1.png | Bin', '1 file changed']['img1.png | Bin', '1 file changed']Passed

SHA-256 / cc4e38e4974d95ba0eb5d04a1640bdc1140232fe488f8ebe72b7c0e091d098b7

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

Case digest / 34e6ea9996d6497ea492b27b5aaa2799d5ce4ff89fd80a8cf9ffb186e66627d1