FAILURE MAP
← Case archive

FA-95061 / Warehouse slotting and bin packing / Open access

FEFO putaway: zero-quantity labelled bin not empty · case 01

Bins that still carry a stale SKU label after being emptied are never reused.

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

ROOT CAUSE

Emptiness is determined only by a missing SKU label; zero quantity is ignored.

VERIFIED REPAIR

Treat a bin as empty when it has no SKU label or holds zero units.

Unsuccessful approach: Only reusing zero-quantity bins labelled with the same SKU still strands bins labelled for other SKUs.

Case contract

bins [[bin, sku|None, lot, expiry_day, qty, capacity, distance]]; receipt [sku, lot, expiry_day, qty]. A bin is empty when sku is None or qty is 0. Preference: (1) non-empty bin with same sku and lot and room (capacity-qty >= receipt qty), nearest then id; (2) same sku, different lot, expiry within 30 days either way, room; smallest expiry gap, nearest, id; (3) empty bin with capacity >= qty, nearest then id; else "HOLD".

Why this case matters

FEFO storage keeps lots with similar expiry together so the earliest expiring stock ships first.

1 / The failure

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

N = 1
observations = []
def solve(bins, receipt):
    sku, lot, exp, q = receipt
    def room(b):
        return b[5] - b[4] >= q
    def empty(b):
        return b[1] is None
    same_lot = [b for b in bins if not empty(b) and b[1] == sku and b[2] == lot and room(b)]
    if same_lot:
        return min(same_lot, key=lambda b: (b[6], b[0]))[0]
    mixed = [b for b in bins if not empty(b) and b[1] == sku and b[2] != lot and abs(b[3] - exp) <= 30 and room(b)]
    if mixed:
        return min(mixed, key=lambda b: (abs(b[3] - exp), b[6], b[0]))[0]
    free = [b for b in bins if empty(b) and b[5] >= q]
    if free:
        return min(free, key=lambda b: (b[6], b[0]))[0]
    return 'HOLD'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['generated mix 7', [[['B0', 'S1', 'L1', 130, 10, 40, 3], ['B1', 'S2', 'L3', 160, 30, 20, 2], ['B2', 'S1', 'L2', 145, 0, 20, 1]], ['S2', 'L4', 130, 20]], 'B2'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 6', [[['B0', 'S1', 'L1', 160, 40, 20, 6], ['B1', 'S2', 'L2', 131, 40, 20, 7]], ['S1', 'L4', 100, 30]], 'HOLD'], ['generated mix 15', [[['B0', 'S1', 'L2', 145, 40, 20, 2], ['B1', 'S2', 'L1', 145, 40, 60, 3], ['B2', 'S1', 'L1', 100, 30, 40, 3], ['B3', 'S1', 'L2', 145, 5, 60, 3], ['B4', 'S1', 'L1', 131, 0, 20, 6], ['B5', None, None, 0, 0, 40, 9]], ['S2', 'L4', 150, 20]], 'B1']], [['generated mix 28', [[['B0', 'S1', 'L3', 131, 10, 60, 9], ['B1', 'S2', 'L2', 131, 0, 20, 2], ['B2', 'S2', 'L2', 160, 40, 40, 5]], ['S2', 'L1', 100, 10]], 'B1'], ['generated mix 71', [[['B0', 'S1', 'L3', 160, 5, 40, 5], ['B1', 'S1', 'L1', 130, 0, 60, 7], ['B2', 'S1', 'L3', 120, 0, 60, 2]], ['S2', 'L1', 100, 10]], 'B2'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 9', [[['B0', 'S1', 'L2', 160, 40, 40, 1], ['B1', 'S1', 'L1', 131, 40, 40, 1], ['B2', 'S1', 'L3', 145, 0, 60, 1], ['B3', 'S1', 'L2', 100, 10, 20, 7], ['B4', 'S2', 'L1', 100, 30, 60, 5]], ['S1', 'L4', 150, 20]], 'B2'], ['generated mix 17', [[['B0', 'S2', 'L2', 120, 0, 20, 8], ['B1', 'S1', 'L2', 131, 5, 40, 3], ['B2', 'S2', 'L2', 100, 10, 20, 8], ['B3', 'S2', 'L2', 100, 10, 60, 6], ['B4', 'S1', 'L3', 100, 30, 40, 3], ['B5', 'S1', 'L1', 145, 10, 60, 5]], ['S1', 'L4', 150, 20]], 'B5']], [['generated mix 69', [[['B0', 'S1', 'L2', 160, 0, 60, 7], ['B1', 'S1', 'L3', 160, 0, 60, 8], ['B2', None, None, 0, 0, 60, 6], ['B3', 'S2', 'L3', 130, 0, 60, 7], ['B4', None, None, 0, 0, 60, 5], ['B5', 'S1', 'L2', 160, 10, 20, 4], ['B6', 'S2', 'L1', 145, 30, 20, 9]], ['S2', 'L4', 100, 10]], 'B4'], ['generated mix 22', [[['B0', 'S2', 'L3', 131, 40, 40, 2], ['B1', 'S2', 'L1', 160, 0, 20, 8], ['B2', 'S2', 'L1', 100, 40, 60, 8], ['B3', 'S2', 'L1', 131, 0, 60, 2], ['B4', 'S2', 'L1', 130, 5, 20, 7]], ['S1', 'L1', 150, 20]], 'B3'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 20', [[['B0', None, None, 0, 0, 20, 8], ['B1', 'S1', 'L1', 131, 30, 40, 6], ['B2', None, None, 0, 0, 20, 6], ['B3', 'S1', 'L3', 160, 0, 40, 6], ['B4', 'S1', 'L3', 160, 10, 20, 4], ['B5', 'S1', 'L3', 131, 30, 40, 9], ['B6', None, None, 0, 0, 60, 5]], ['S1', 'L4', 130, 20]], 'B6'], ['generated mix 29', [[['B0', None, None, 0, 0, 60, 5], ['B1', 'S1', 'L3', 100, 0, 60, 7], ['B2', 'S2', 'L2', 160, 40, 60, 7], ['B3', 'S1', 'L3', 131, 30, 60, 3], ['B4', 'S1', 'L1', 145, 10, 20, 4], ['B5', 'S2', 'L1', 130, 5, 40, 3], ['B6', 'S2', 'L2', 160, 40, 60, 5]], ['S1', 'L2', 100, 30]], 'B0']], [['generated mix 14', [[['B0', 'S2', 'L1', 130, 5, 60, 3], ['B1', 'S2', 'L1', 120, 0, 60, 8], ['B2', None, None, 0, 0, 60, 9]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 12', [[['B0', 'S1', 'L3', 145, 0, 60, 4], ['B1', 'S1', 'L3', 160, 30, 40, 7], ['B2', 'S1', 'L2', 100, 40, 40, 4], ['B3', 'S1', 'L1', 130, 0, 60, 3]], ['S2', 'L2', 150, 20]], 'B3'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 33', [[['B0', 'S1', 'L1', 130, 10, 40, 6], ['B1', 'S1', 'L3', 160, 40, 40, 3], ['B2', None, None, 0, 0, 20, 1], ['B3', 'S2', 'L2', 160, 5, 60, 8], ['B4', None, None, 0, 0, 40, 4], ['B5', 'S2', 'L2', 131, 5, 60, 8]], ['S1', 'L1', 130, 30]], 'B0'], ['generated mix 41', [[['B0', 'S1', 'L2', 160, 30, 60, 7], ['B1', 'S1', 'L2', 160, 40, 60, 3], ['B2', 'S1', 'L3', 100, 0, 60, 7], ['B3', 'S2', 'L2', 130, 10, 60, 9], ['B4', 'S1', 'L2', 160, 0, 20, 4], ['B5', 'S2', 'L2', 100, 30, 40, 4]], ['S2', 'L1', 130, 30]], 'B3']], [['generated mix 53', [[['B0', None, None, 0, 0, 40, 1], ['B1', 'S2', 'L1', 130, 0, 20, 4], ['B2', 'S1', 'L2', 100, 5, 60, 3], ['B3', None, None, 0, 0, 20, 1], ['B4', 'S1', 'L1', 100, 30, 20, 4], ['B5', 'S2', 'L3', 130, 5, 60, 7], ['B6', 'S2', 'L1', 120, 0, 60, 1]], ['S2', 'L1', 150, 10]], 'B5'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 42', [[['B0', 'S1', 'L2', 100, 30, 20, 3], ['B1', 'S1', 'L1', 131, 30, 40, 9], ['B2', 'S1', 'L2', 100, 10, 60, 1], ['B3', 'S1', 'L2', 131, 30, 20, 1]], ['S1', 'L1', 100, 20]], 'B2'], ['generated mix 50', [[['B0', 'S2', 'L3', 160, 0, 40, 6], ['B1', 'S1', 'L2', 130, 0, 40, 1], ['B2', 'S2', 'L1', 130, 10, 60, 1]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 59', [[['B0', 'S1', 'L3', 160, 10, 60, 4], ['B1', None, None, 0, 0, 60, 1], ['B2', 'S1', 'L3', 145, 5, 20, 4], ['B3', 'S1', 'L3', 100, 40, 60, 5], ['B4', None, None, 0, 0, 20, 8]], ['S2', 'L2', 130, 20]], 'B1']]]
for label, args, expected in fixtures[N-1]:
    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
residual label emptyERFailed
generated mix 7HOLDB2Failed
same lot preferredAAPassed
expiry 30 days laterAAPassed
lot code on other skuEEPassed
room exactAAPassed
generated mix 6HOLDHOLDPassed
generated mix 15B1B1Passed

SHA-256 / 8b152c8b88ba167e0e7223b3bde7e4dea73a5eabcbfe99b7d036013c1690639c

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(bins, receipt):
    sku, lot, exp, q = receipt
    def room(b):
        return b[5] - b[4] >= q
    def empty(b):
        return b[4] == 0 and b[1] in (None, sku)
    same_lot = [b for b in bins if not empty(b) and b[1] == sku and b[2] == lot and room(b)]
    if same_lot:
        return min(same_lot, key=lambda b: (b[6], b[0]))[0]
    mixed = [b for b in bins if not empty(b) and b[1] == sku and b[2] != lot and abs(b[3] - exp) <= 30 and room(b)]
    if mixed:
        return min(mixed, key=lambda b: (abs(b[3] - exp), b[6], b[0]))[0]
    free = [b for b in bins if empty(b) and b[5] >= q]
    if free:
        return min(free, key=lambda b: (b[6], b[0]))[0]
    return 'HOLD'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['generated mix 7', [[['B0', 'S1', 'L1', 130, 10, 40, 3], ['B1', 'S2', 'L3', 160, 30, 20, 2], ['B2', 'S1', 'L2', 145, 0, 20, 1]], ['S2', 'L4', 130, 20]], 'B2'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 6', [[['B0', 'S1', 'L1', 160, 40, 20, 6], ['B1', 'S2', 'L2', 131, 40, 20, 7]], ['S1', 'L4', 100, 30]], 'HOLD'], ['generated mix 15', [[['B0', 'S1', 'L2', 145, 40, 20, 2], ['B1', 'S2', 'L1', 145, 40, 60, 3], ['B2', 'S1', 'L1', 100, 30, 40, 3], ['B3', 'S1', 'L2', 145, 5, 60, 3], ['B4', 'S1', 'L1', 131, 0, 20, 6], ['B5', None, None, 0, 0, 40, 9]], ['S2', 'L4', 150, 20]], 'B1']], [['generated mix 28', [[['B0', 'S1', 'L3', 131, 10, 60, 9], ['B1', 'S2', 'L2', 131, 0, 20, 2], ['B2', 'S2', 'L2', 160, 40, 40, 5]], ['S2', 'L1', 100, 10]], 'B1'], ['generated mix 71', [[['B0', 'S1', 'L3', 160, 5, 40, 5], ['B1', 'S1', 'L1', 130, 0, 60, 7], ['B2', 'S1', 'L3', 120, 0, 60, 2]], ['S2', 'L1', 100, 10]], 'B2'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 9', [[['B0', 'S1', 'L2', 160, 40, 40, 1], ['B1', 'S1', 'L1', 131, 40, 40, 1], ['B2', 'S1', 'L3', 145, 0, 60, 1], ['B3', 'S1', 'L2', 100, 10, 20, 7], ['B4', 'S2', 'L1', 100, 30, 60, 5]], ['S1', 'L4', 150, 20]], 'B2'], ['generated mix 17', [[['B0', 'S2', 'L2', 120, 0, 20, 8], ['B1', 'S1', 'L2', 131, 5, 40, 3], ['B2', 'S2', 'L2', 100, 10, 20, 8], ['B3', 'S2', 'L2', 100, 10, 60, 6], ['B4', 'S1', 'L3', 100, 30, 40, 3], ['B5', 'S1', 'L1', 145, 10, 60, 5]], ['S1', 'L4', 150, 20]], 'B5']], [['generated mix 69', [[['B0', 'S1', 'L2', 160, 0, 60, 7], ['B1', 'S1', 'L3', 160, 0, 60, 8], ['B2', None, None, 0, 0, 60, 6], ['B3', 'S2', 'L3', 130, 0, 60, 7], ['B4', None, None, 0, 0, 60, 5], ['B5', 'S1', 'L2', 160, 10, 20, 4], ['B6', 'S2', 'L1', 145, 30, 20, 9]], ['S2', 'L4', 100, 10]], 'B4'], ['generated mix 22', [[['B0', 'S2', 'L3', 131, 40, 40, 2], ['B1', 'S2', 'L1', 160, 0, 20, 8], ['B2', 'S2', 'L1', 100, 40, 60, 8], ['B3', 'S2', 'L1', 131, 0, 60, 2], ['B4', 'S2', 'L1', 130, 5, 20, 7]], ['S1', 'L1', 150, 20]], 'B3'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 20', [[['B0', None, None, 0, 0, 20, 8], ['B1', 'S1', 'L1', 131, 30, 40, 6], ['B2', None, None, 0, 0, 20, 6], ['B3', 'S1', 'L3', 160, 0, 40, 6], ['B4', 'S1', 'L3', 160, 10, 20, 4], ['B5', 'S1', 'L3', 131, 30, 40, 9], ['B6', None, None, 0, 0, 60, 5]], ['S1', 'L4', 130, 20]], 'B6'], ['generated mix 29', [[['B0', None, None, 0, 0, 60, 5], ['B1', 'S1', 'L3', 100, 0, 60, 7], ['B2', 'S2', 'L2', 160, 40, 60, 7], ['B3', 'S1', 'L3', 131, 30, 60, 3], ['B4', 'S1', 'L1', 145, 10, 20, 4], ['B5', 'S2', 'L1', 130, 5, 40, 3], ['B6', 'S2', 'L2', 160, 40, 60, 5]], ['S1', 'L2', 100, 30]], 'B0']], [['generated mix 14', [[['B0', 'S2', 'L1', 130, 5, 60, 3], ['B1', 'S2', 'L1', 120, 0, 60, 8], ['B2', None, None, 0, 0, 60, 9]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 12', [[['B0', 'S1', 'L3', 145, 0, 60, 4], ['B1', 'S1', 'L3', 160, 30, 40, 7], ['B2', 'S1', 'L2', 100, 40, 40, 4], ['B3', 'S1', 'L1', 130, 0, 60, 3]], ['S2', 'L2', 150, 20]], 'B3'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 33', [[['B0', 'S1', 'L1', 130, 10, 40, 6], ['B1', 'S1', 'L3', 160, 40, 40, 3], ['B2', None, None, 0, 0, 20, 1], ['B3', 'S2', 'L2', 160, 5, 60, 8], ['B4', None, None, 0, 0, 40, 4], ['B5', 'S2', 'L2', 131, 5, 60, 8]], ['S1', 'L1', 130, 30]], 'B0'], ['generated mix 41', [[['B0', 'S1', 'L2', 160, 30, 60, 7], ['B1', 'S1', 'L2', 160, 40, 60, 3], ['B2', 'S1', 'L3', 100, 0, 60, 7], ['B3', 'S2', 'L2', 130, 10, 60, 9], ['B4', 'S1', 'L2', 160, 0, 20, 4], ['B5', 'S2', 'L2', 100, 30, 40, 4]], ['S2', 'L1', 130, 30]], 'B3']], [['generated mix 53', [[['B0', None, None, 0, 0, 40, 1], ['B1', 'S2', 'L1', 130, 0, 20, 4], ['B2', 'S1', 'L2', 100, 5, 60, 3], ['B3', None, None, 0, 0, 20, 1], ['B4', 'S1', 'L1', 100, 30, 20, 4], ['B5', 'S2', 'L3', 130, 5, 60, 7], ['B6', 'S2', 'L1', 120, 0, 60, 1]], ['S2', 'L1', 150, 10]], 'B5'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 42', [[['B0', 'S1', 'L2', 100, 30, 20, 3], ['B1', 'S1', 'L1', 131, 30, 40, 9], ['B2', 'S1', 'L2', 100, 10, 60, 1], ['B3', 'S1', 'L2', 131, 30, 20, 1]], ['S1', 'L1', 100, 20]], 'B2'], ['generated mix 50', [[['B0', 'S2', 'L3', 160, 0, 40, 6], ['B1', 'S1', 'L2', 130, 0, 40, 1], ['B2', 'S2', 'L1', 130, 10, 60, 1]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 59', [[['B0', 'S1', 'L3', 160, 10, 60, 4], ['B1', None, None, 0, 0, 60, 1], ['B2', 'S1', 'L3', 145, 5, 20, 4], ['B3', 'S1', 'L3', 100, 40, 60, 5], ['B4', None, None, 0, 0, 20, 8]], ['S2', 'L2', 130, 20]], 'B1']]]
for label, args, expected in fixtures[N-1]:
    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
residual label emptyERFailed
generated mix 7HOLDB2Failed
same lot preferredAAPassed
expiry 30 days laterAAPassed
lot code on other skuEEPassed
room exactAAPassed
generated mix 6HOLDHOLDPassed
generated mix 15B1B1Passed

SHA-256 / b42370c992e9bb5b57cd7e0b9f6a80ff92c8c268b2089c5aca16e745e63f4220

3 / The verified repair

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

N = 1
observations = []
def solve(bins, receipt):
    sku, lot, exp, q = receipt
    def room(b):
        return b[5] - b[4] >= q
    def empty(b):
        return b[1] is None or b[4] == 0
    same_lot = [b for b in bins if not empty(b) and b[1] == sku and b[2] == lot and room(b)]
    if same_lot:
        return min(same_lot, key=lambda b: (b[6], b[0]))[0]
    mixed = [b for b in bins if not empty(b) and b[1] == sku and b[2] != lot and abs(b[3] - exp) <= 30 and room(b)]
    if mixed:
        return min(mixed, key=lambda b: (abs(b[3] - exp), b[6], b[0]))[0]
    free = [b for b in bins if empty(b) and b[5] >= q]
    if free:
        return min(free, key=lambda b: (b[6], b[0]))[0]
    return 'HOLD'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['generated mix 7', [[['B0', 'S1', 'L1', 130, 10, 40, 3], ['B1', 'S2', 'L3', 160, 30, 20, 2], ['B2', 'S1', 'L2', 145, 0, 20, 1]], ['S2', 'L4', 130, 20]], 'B2'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 6', [[['B0', 'S1', 'L1', 160, 40, 20, 6], ['B1', 'S2', 'L2', 131, 40, 20, 7]], ['S1', 'L4', 100, 30]], 'HOLD'], ['generated mix 15', [[['B0', 'S1', 'L2', 145, 40, 20, 2], ['B1', 'S2', 'L1', 145, 40, 60, 3], ['B2', 'S1', 'L1', 100, 30, 40, 3], ['B3', 'S1', 'L2', 145, 5, 60, 3], ['B4', 'S1', 'L1', 131, 0, 20, 6], ['B5', None, None, 0, 0, 40, 9]], ['S2', 'L4', 150, 20]], 'B1']], [['generated mix 28', [[['B0', 'S1', 'L3', 131, 10, 60, 9], ['B1', 'S2', 'L2', 131, 0, 20, 2], ['B2', 'S2', 'L2', 160, 40, 40, 5]], ['S2', 'L1', 100, 10]], 'B1'], ['generated mix 71', [[['B0', 'S1', 'L3', 160, 5, 40, 5], ['B1', 'S1', 'L1', 130, 0, 60, 7], ['B2', 'S1', 'L3', 120, 0, 60, 2]], ['S2', 'L1', 100, 10]], 'B2'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 9', [[['B0', 'S1', 'L2', 160, 40, 40, 1], ['B1', 'S1', 'L1', 131, 40, 40, 1], ['B2', 'S1', 'L3', 145, 0, 60, 1], ['B3', 'S1', 'L2', 100, 10, 20, 7], ['B4', 'S2', 'L1', 100, 30, 60, 5]], ['S1', 'L4', 150, 20]], 'B2'], ['generated mix 17', [[['B0', 'S2', 'L2', 120, 0, 20, 8], ['B1', 'S1', 'L2', 131, 5, 40, 3], ['B2', 'S2', 'L2', 100, 10, 20, 8], ['B3', 'S2', 'L2', 100, 10, 60, 6], ['B4', 'S1', 'L3', 100, 30, 40, 3], ['B5', 'S1', 'L1', 145, 10, 60, 5]], ['S1', 'L4', 150, 20]], 'B5']], [['generated mix 69', [[['B0', 'S1', 'L2', 160, 0, 60, 7], ['B1', 'S1', 'L3', 160, 0, 60, 8], ['B2', None, None, 0, 0, 60, 6], ['B3', 'S2', 'L3', 130, 0, 60, 7], ['B4', None, None, 0, 0, 60, 5], ['B5', 'S1', 'L2', 160, 10, 20, 4], ['B6', 'S2', 'L1', 145, 30, 20, 9]], ['S2', 'L4', 100, 10]], 'B4'], ['generated mix 22', [[['B0', 'S2', 'L3', 131, 40, 40, 2], ['B1', 'S2', 'L1', 160, 0, 20, 8], ['B2', 'S2', 'L1', 100, 40, 60, 8], ['B3', 'S2', 'L1', 131, 0, 60, 2], ['B4', 'S2', 'L1', 130, 5, 20, 7]], ['S1', 'L1', 150, 20]], 'B3'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 20', [[['B0', None, None, 0, 0, 20, 8], ['B1', 'S1', 'L1', 131, 30, 40, 6], ['B2', None, None, 0, 0, 20, 6], ['B3', 'S1', 'L3', 160, 0, 40, 6], ['B4', 'S1', 'L3', 160, 10, 20, 4], ['B5', 'S1', 'L3', 131, 30, 40, 9], ['B6', None, None, 0, 0, 60, 5]], ['S1', 'L4', 130, 20]], 'B6'], ['generated mix 29', [[['B0', None, None, 0, 0, 60, 5], ['B1', 'S1', 'L3', 100, 0, 60, 7], ['B2', 'S2', 'L2', 160, 40, 60, 7], ['B3', 'S1', 'L3', 131, 30, 60, 3], ['B4', 'S1', 'L1', 145, 10, 20, 4], ['B5', 'S2', 'L1', 130, 5, 40, 3], ['B6', 'S2', 'L2', 160, 40, 60, 5]], ['S1', 'L2', 100, 30]], 'B0']], [['generated mix 14', [[['B0', 'S2', 'L1', 130, 5, 60, 3], ['B1', 'S2', 'L1', 120, 0, 60, 8], ['B2', None, None, 0, 0, 60, 9]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 12', [[['B0', 'S1', 'L3', 145, 0, 60, 4], ['B1', 'S1', 'L3', 160, 30, 40, 7], ['B2', 'S1', 'L2', 100, 40, 40, 4], ['B3', 'S1', 'L1', 130, 0, 60, 3]], ['S2', 'L2', 150, 20]], 'B3'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 33', [[['B0', 'S1', 'L1', 130, 10, 40, 6], ['B1', 'S1', 'L3', 160, 40, 40, 3], ['B2', None, None, 0, 0, 20, 1], ['B3', 'S2', 'L2', 160, 5, 60, 8], ['B4', None, None, 0, 0, 40, 4], ['B5', 'S2', 'L2', 131, 5, 60, 8]], ['S1', 'L1', 130, 30]], 'B0'], ['generated mix 41', [[['B0', 'S1', 'L2', 160, 30, 60, 7], ['B1', 'S1', 'L2', 160, 40, 60, 3], ['B2', 'S1', 'L3', 100, 0, 60, 7], ['B3', 'S2', 'L2', 130, 10, 60, 9], ['B4', 'S1', 'L2', 160, 0, 20, 4], ['B5', 'S2', 'L2', 100, 30, 40, 4]], ['S2', 'L1', 130, 30]], 'B3']], [['generated mix 53', [[['B0', None, None, 0, 0, 40, 1], ['B1', 'S2', 'L1', 130, 0, 20, 4], ['B2', 'S1', 'L2', 100, 5, 60, 3], ['B3', None, None, 0, 0, 20, 1], ['B4', 'S1', 'L1', 100, 30, 20, 4], ['B5', 'S2', 'L3', 130, 5, 60, 7], ['B6', 'S2', 'L1', 120, 0, 60, 1]], ['S2', 'L1', 150, 10]], 'B5'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 42', [[['B0', 'S1', 'L2', 100, 30, 20, 3], ['B1', 'S1', 'L1', 131, 30, 40, 9], ['B2', 'S1', 'L2', 100, 10, 60, 1], ['B3', 'S1', 'L2', 131, 30, 20, 1]], ['S1', 'L1', 100, 20]], 'B2'], ['generated mix 50', [[['B0', 'S2', 'L3', 160, 0, 40, 6], ['B1', 'S1', 'L2', 130, 0, 40, 1], ['B2', 'S2', 'L1', 130, 10, 60, 1]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 59', [[['B0', 'S1', 'L3', 160, 10, 60, 4], ['B1', None, None, 0, 0, 60, 1], ['B2', 'S1', 'L3', 145, 5, 20, 4], ['B3', 'S1', 'L3', 100, 40, 60, 5], ['B4', None, None, 0, 0, 20, 8]], ['S2', 'L2', 130, 20]], 'B1']]]
for label, args, expected in fixtures[N-1]:
    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
residual label emptyRRPassed
generated mix 7B2B2Passed
same lot preferredAAPassed
expiry 30 days laterAAPassed
lot code on other skuEEPassed
room exactAAPassed
generated mix 6HOLDHOLDPassed
generated mix 15B1B1Passed

SHA-256 / 1d84939cb18fa932b7de06bf8d18d2333013c8a89df540d728c191e14e2a6bbe

Verification & scope

Stipulated day-number expiries and a single receipt; no quarantine or multi-bin splits. 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:52:10.266741+00:00.

Case digest / 4caccece90c34840c4648dadddf2480ea939bb183e0e8ef7a727896b77005278