FA-60381 / Loyalty points ledgers / Open access
Expiring point lots: sweep cutoff · case 01
The nightly sweep removes points on their last valid day.
ROOT CAUSE
The sweep treats lots expiring on the sweep day as expired.
VERIFIED REPAIR
Sweep only lots whose expiry is before the sweep day.
Unsuccessful approach: Lagging the sweep by an extra day leaves expired points in balances for a day.
Case contract
Events ["earn",day,pts,ttl] create a lot expiring at day+ttl (usable through that day); ["redeem",day,pts] draws only unexpired lots, soonest expiry first (ties by earn day), rejecting with "insufficient" when unexpired points are short; ["sweep",day] removes lots whose expiry is before day and reports the points removed. Emptied lots are dropped. Return {results, lots:[[expiry, earn_day, pts]]}.
Why this case matters
Member point balances, statements and the program liability depend on this ledger rule.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
lots = []
out = []
for ev in x['events']:
kind, day = ev[0], ev[1]
if kind == 'earn':
exp = day + ev[3]
lots.append([exp, day, ev[2]])
out.append(exp)
elif kind == 'sweep':
gone = sum(l[2] for l in lots if l[0] <= day)
lots = [l for l in lots if l[0] > day]
out.append(gone)
else:
need = ev[2]
live = sorted([l for l in lots if l[0] >= day], key=lambda l: (l[0], l[1]))
if need > sum(l[2] for l in live):
out.append('insufficient')
continue
used = []
for l in live:
t = min(need, l[2])
if t > 0:
l[2] -= t
need -= t
used.append([l[0], t])
lots = [l for l in lots if l[2] > 0]
out.append(used)
return {'results': out, 'lots': lots}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 31], ['redeem', 20, 400]]}, {'results': [366, 41, [[41, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 151]]}, {'results': [30, [[30, 151]]], 'lots': [[30, 0, 49]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 101]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 81, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 201], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 11], ['sweep', 11], ['sweep', 12]]}, {'results': [11, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 31]]}, {'results': [50, 61, [[50, 100]], [[61, 31]]], 'lots': [[61, 1, 69]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 151], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 51]]}, {'results': [25, 25, [[25, 40], [25, 11]]], 'lots': [[25, 5, 29]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 32], ['redeem', 20, 400]]}, {'results': [366, 42, [[42, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 152]]}, {'results': [30, [[30, 152]]], 'lots': [[30, 0, 48]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 102]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 82, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 202], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 12], ['sweep', 12], ['sweep', 13]]}, {'results': [12, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 32]]}, {'results': [50, 61, [[50, 100]], [[61, 32]]], 'lots': [[61, 1, 68]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 152], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 52]]}, {'results': [25, 25, [[25, 40], [25, 12]]], 'lots': [[25, 5, 28]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 33], ['redeem', 20, 400]]}, {'results': [366, 43, [[43, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 153]]}, {'results': [30, [[30, 153]]], 'lots': [[30, 0, 47]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 103]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 83, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 203], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 13], ['sweep', 13], ['sweep', 14]]}, {'results': [13, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 33]]}, {'results': [50, 61, [[50, 100]], [[61, 33]]], 'lots': [[61, 1, 67]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 153], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 53]]}, {'results': [25, 25, [[25, 40], [25, 13]]], 'lots': [[25, 5, 27]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 34], ['redeem', 20, 400]]}, {'results': [366, 44, [[44, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 154]]}, {'results': [30, [[30, 154]]], 'lots': [[30, 0, 46]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 104]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 84, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 204], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 14], ['sweep', 14], ['sweep', 15]]}, {'results': [14, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 34]]}, {'results': [50, 61, [[50, 100]], [[61, 34]]], 'lots': [[61, 1, 66]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 154], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 54]]}, {'results': [25, 25, [[25, 40], [25, 14]]], 'lots': [[25, 5, 26]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 35], ['redeem', 20, 400]]}, {'results': [366, 45, [[45, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 155]]}, {'results': [30, [[30, 155]]], 'lots': [[30, 0, 45]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 105]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 85, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 205], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 15], ['sweep', 15], ['sweep', 16]]}, {'results': [15, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 35]]}, {'results': [50, 61, [[50, 100]], [[61, 35]]], 'lots': [[61, 1, 65]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 155], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 55]]}, {'results': [25, 25, [[25, 40], [25, 15]]], 'lots': [[25, 5, 25]]}]]]
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| soonest expiry first | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | Passed |
| use on expiry day | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | Passed |
| expired unusable | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | Passed |
| sweep reports points | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | Passed |
| sweep boundary | {'lots': [], 'results': [11, 70, 0]} | {'lots': [], 'results': [11, 0, 70]} | Failed |
| partial lot cleanup | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | Passed |
| insufficient | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | Passed |
| ties by earn day | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | Passed |
SHA-256 / 6647e83745ed166cd94192ecdacf0ccbfd76b7a4a24d06fcf4e3394d3ffceeb7
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
lots = []
out = []
for ev in x['events']:
kind, day = ev[0], ev[1]
if kind == 'earn':
exp = day + ev[3]
lots.append([exp, day, ev[2]])
out.append(exp)
elif kind == 'sweep':
gone = sum(l[2] for l in lots if l[0] + 1 < day)
lots = [l for l in lots if l[0] + 1 >= day]
out.append(gone)
else:
need = ev[2]
live = sorted([l for l in lots if l[0] >= day], key=lambda l: (l[0], l[1]))
if need > sum(l[2] for l in live):
out.append('insufficient')
continue
used = []
for l in live:
t = min(need, l[2])
if t > 0:
l[2] -= t
need -= t
used.append([l[0], t])
lots = [l for l in lots if l[2] > 0]
out.append(used)
return {'results': out, 'lots': lots}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 31], ['redeem', 20, 400]]}, {'results': [366, 41, [[41, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 151]]}, {'results': [30, [[30, 151]]], 'lots': [[30, 0, 49]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 101]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 81, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 201], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 11], ['sweep', 11], ['sweep', 12]]}, {'results': [11, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 31]]}, {'results': [50, 61, [[50, 100]], [[61, 31]]], 'lots': [[61, 1, 69]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 151], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 51]]}, {'results': [25, 25, [[25, 40], [25, 11]]], 'lots': [[25, 5, 29]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 32], ['redeem', 20, 400]]}, {'results': [366, 42, [[42, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 152]]}, {'results': [30, [[30, 152]]], 'lots': [[30, 0, 48]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 102]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 82, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 202], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 12], ['sweep', 12], ['sweep', 13]]}, {'results': [12, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 32]]}, {'results': [50, 61, [[50, 100]], [[61, 32]]], 'lots': [[61, 1, 68]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 152], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 52]]}, {'results': [25, 25, [[25, 40], [25, 12]]], 'lots': [[25, 5, 28]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 33], ['redeem', 20, 400]]}, {'results': [366, 43, [[43, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 153]]}, {'results': [30, [[30, 153]]], 'lots': [[30, 0, 47]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 103]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 83, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 203], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 13], ['sweep', 13], ['sweep', 14]]}, {'results': [13, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 33]]}, {'results': [50, 61, [[50, 100]], [[61, 33]]], 'lots': [[61, 1, 67]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 153], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 53]]}, {'results': [25, 25, [[25, 40], [25, 13]]], 'lots': [[25, 5, 27]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 34], ['redeem', 20, 400]]}, {'results': [366, 44, [[44, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 154]]}, {'results': [30, [[30, 154]]], 'lots': [[30, 0, 46]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 104]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 84, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 204], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 14], ['sweep', 14], ['sweep', 15]]}, {'results': [14, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 34]]}, {'results': [50, 61, [[50, 100]], [[61, 34]]], 'lots': [[61, 1, 66]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 154], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 54]]}, {'results': [25, 25, [[25, 40], [25, 14]]], 'lots': [[25, 5, 26]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 35], ['redeem', 20, 400]]}, {'results': [366, 45, [[45, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 155]]}, {'results': [30, [[30, 155]]], 'lots': [[30, 0, 45]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 105]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 85, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 205], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 15], ['sweep', 15], ['sweep', 16]]}, {'results': [15, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 35]]}, {'results': [50, 61, [[50, 100]], [[61, 35]]], 'lots': [[61, 1, 65]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 155], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 55]]}, {'results': [25, 25, [[25, 40], [25, 15]]], 'lots': [[25, 5, 25]]}]]]
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| soonest expiry first | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | Passed |
| use on expiry day | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | Passed |
| expired unusable | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | Passed |
| sweep reports points | {'lots': [[10, 0, 120], [10, 0, 81], [103, 3, 60]], 'results': [10, 10, 103, 0]} | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | Failed |
| sweep boundary | {'lots': [[11, 0, 70]], 'results': [11, 0, 0]} | {'lots': [], 'results': [11, 0, 70]} | Failed |
| partial lot cleanup | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | Passed |
| insufficient | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | Passed |
| ties by earn day | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | Passed |
SHA-256 / 745a2cc4ad0b838d4b94501a7ed8f6f367595bb3f614f44238eac7471b882463
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
lots = []
out = []
for ev in x['events']:
kind, day = ev[0], ev[1]
if kind == 'earn':
exp = day + ev[3]
lots.append([exp, day, ev[2]])
out.append(exp)
elif kind == 'sweep':
gone = sum(l[2] for l in lots if l[0] < day)
lots = [l for l in lots if l[0] >= day]
out.append(gone)
else:
need = ev[2]
live = sorted([l for l in lots if l[0] >= day], key=lambda l: (l[0], l[1]))
if need > sum(l[2] for l in live):
out.append('insufficient')
continue
used = []
for l in live:
t = min(need, l[2])
if t > 0:
l[2] -= t
need -= t
used.append([l[0], t])
lots = [l for l in lots if l[2] > 0]
out.append(used)
return {'results': out, 'lots': lots}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 31], ['redeem', 20, 400]]}, {'results': [366, 41, [[41, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 151]]}, {'results': [30, [[30, 151]]], 'lots': [[30, 0, 49]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 101]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 81, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 201], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 11], ['sweep', 11], ['sweep', 12]]}, {'results': [11, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 31]]}, {'results': [50, 61, [[50, 100]], [[61, 31]]], 'lots': [[61, 1, 69]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 151], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 51]]}, {'results': [25, 25, [[25, 40], [25, 11]]], 'lots': [[25, 5, 29]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 32], ['redeem', 20, 400]]}, {'results': [366, 42, [[42, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 152]]}, {'results': [30, [[30, 152]]], 'lots': [[30, 0, 48]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 102]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 82, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 202], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 12], ['sweep', 12], ['sweep', 13]]}, {'results': [12, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 32]]}, {'results': [50, 61, [[50, 100]], [[61, 32]]], 'lots': [[61, 1, 68]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 152], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 52]]}, {'results': [25, 25, [[25, 40], [25, 12]]], 'lots': [[25, 5, 28]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 33], ['redeem', 20, 400]]}, {'results': [366, 43, [[43, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 153]]}, {'results': [30, [[30, 153]]], 'lots': [[30, 0, 47]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 103]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 83, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 203], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 13], ['sweep', 13], ['sweep', 14]]}, {'results': [13, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 33]]}, {'results': [50, 61, [[50, 100]], [[61, 33]]], 'lots': [[61, 1, 67]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 153], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 53]]}, {'results': [25, 25, [[25, 40], [25, 13]]], 'lots': [[25, 5, 27]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 34], ['redeem', 20, 400]]}, {'results': [366, 44, [[44, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 154]]}, {'results': [30, [[30, 154]]], 'lots': [[30, 0, 46]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 104]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 84, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 204], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 14], ['sweep', 14], ['sweep', 15]]}, {'results': [14, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 34]]}, {'results': [50, 61, [[50, 100]], [[61, 34]]], 'lots': [[61, 1, 66]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 154], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 54]]}, {'results': [25, 25, [[25, 40], [25, 14]]], 'lots': [[25, 5, 26]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 35], ['redeem', 20, 400]]}, {'results': [366, 45, [[45, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 155]]}, {'results': [30, [[30, 155]]], 'lots': [[30, 0, 45]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 105]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 85, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 205], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 15], ['sweep', 15], ['sweep', 16]]}, {'results': [15, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 35]]}, {'results': [50, 61, [[50, 100]], [[61, 35]]], 'lots': [[61, 1, 65]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 155], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 55]]}, {'results': [25, 25, [[25, 40], [25, 15]]], 'lots': [[25, 5, 25]]}]]]
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| soonest expiry first | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | Passed |
| use on expiry day | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | Passed |
| expired unusable | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | Passed |
| sweep reports points | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | Passed |
| sweep boundary | {'lots': [], 'results': [11, 0, 70]} | {'lots': [], 'results': [11, 0, 70]} | Passed |
| partial lot cleanup | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | Passed |
| insufficient | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | Passed |
| ties by earn day | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | Passed |
SHA-256 / 5bc68b951670e50c9a94821866bb25d5150737754da5f6fdee108df25fde5473
Verification & scope
Stipulated bounded loyalty-program model with explicit toy rules; not any real program terms. 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:46:45.051668+00:00.
Case digest / 0b93a1abc94777ea818123444cb9673f918fd016bca24e0052c2d1c35347d8ab