FA-72351 / Check-digit algorithms / Open access
Luhn normalization keeps hyphen group separators · case 01
Hyphen-grouped card numbers are rejected as malformed.
ROOT CAUSE
Only spaces are removed before validation; hyphens remain and fail the digit gate.
VERIFIED REPAIR
Remove both spaces and hyphens, and nothing else, before the digit gate.
Unsuccessful approach: Keeping every digit character silently accepts letters and other junk as if they were separators.
Case contract
Input a card-style number that may contain spaces or hyphens as group separators. After removing them the text must be 2..19 ASCII digits, otherwise "malformed". Doubling applies to every second digit counting from the rightmost (check) digit, doubled values above 9 lose 9, and the number is "valid" when the sum is a multiple of 10, else "invalid".
Why this case matters
Payment-card, loyalty and account numbers are screened with the Luhn mod-10 check before any lookup.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(s):
t = s.replace(' ', '')
if not t.isascii() or not t.isdigit() or len(t) < 2 or len(t) > 19:
return 'malformed'
total = 0
for i, ch in enumerate(reversed(t)):
d = ord(ch) - 48
if i % 2 == 1:
d *= 2
if d > 9:
d -= 9
total += d
return 'valid' if total % 10 == 0 else 'invalid'
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["59"]', ['59'], 'valid'], ['control ["182"]', ['182'], 'valid'], ['control ["97956"]', ['97956'], 'valid'], ['control ["60866456"]', ['60866456'], 'valid'], ['control ["49717735275"]', ['49717735275'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["9207919074163"]', ['9207919074163'], 'valid'], ['control ["470663048070284"]', ['470663048070284'], 'valid'], ['control ["2300017738893848"]', ['2300017738893848'], 'valid'], ['control ["7985145013991721"]', ['7985145013991721'], 'valid'], ['control ["067122965057139178"]', ['067122965057139178'], 'valid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["1513156399001929628"]', ['1513156399001929628'], 'valid'], ['control ["9505094299844733505"]', ['9505094299844733505'], 'valid'], ['control ["0906050370970"]', ['0906050370970'], 'valid'], ['control ["3165356977"]', ['3165356977'], 'valid'], ['control ["91"]', ['91'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["992"]', ['992'], 'valid'], ['control ["5553"]', ['5553'], 'valid'], ['control ["77777779"]', ['77777779'], 'valid'], ['control ["52"]', ['52'], 'invalid'], ['control ["185"]', ['185'], 'invalid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["97959"]', ['97959'], 'invalid'], ['control ["60866459"]', ['60866459'], 'invalid'], ['control ["49717735278"]', ['49717735278'], 'invalid'], ['control ["9207919074166"]', ['9207919074166'], 'invalid'], ['control ["470663048070287"]', ['470663048070287'], 'invalid']]]
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 |
|---|---|---|---|
| regression ["4111-1111-1111-1111"] | malformed | valid | Failed |
| regression ["4111-1111 1111-1112"] | malformed | invalid | Failed |
| partial-repair ["4111a1111"] | malformed | malformed | Passed |
| control ["59"] | valid | valid | Passed |
| control ["182"] | valid | valid | Passed |
| control ["97956"] | valid | valid | Passed |
| control ["60866456"] | valid | valid | Passed |
| control ["49717735275"] | valid | valid | Passed |
SHA-256 / ce869e502a48ebbf66e770e919b43b1aa3c4ffbba28843d1fae7a6a15bc3f0dc
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(s):
t = ''.join(c for c in s if c.isdigit())
if not t.isascii() or not t.isdigit() or len(t) < 2 or len(t) > 19:
return 'malformed'
total = 0
for i, ch in enumerate(reversed(t)):
d = ord(ch) - 48
if i % 2 == 1:
d *= 2
if d > 9:
d -= 9
total += d
return 'valid' if total % 10 == 0 else 'invalid'
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["59"]', ['59'], 'valid'], ['control ["182"]', ['182'], 'valid'], ['control ["97956"]', ['97956'], 'valid'], ['control ["60866456"]', ['60866456'], 'valid'], ['control ["49717735275"]', ['49717735275'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["9207919074163"]', ['9207919074163'], 'valid'], ['control ["470663048070284"]', ['470663048070284'], 'valid'], ['control ["2300017738893848"]', ['2300017738893848'], 'valid'], ['control ["7985145013991721"]', ['7985145013991721'], 'valid'], ['control ["067122965057139178"]', ['067122965057139178'], 'valid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["1513156399001929628"]', ['1513156399001929628'], 'valid'], ['control ["9505094299844733505"]', ['9505094299844733505'], 'valid'], ['control ["0906050370970"]', ['0906050370970'], 'valid'], ['control ["3165356977"]', ['3165356977'], 'valid'], ['control ["91"]', ['91'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["992"]', ['992'], 'valid'], ['control ["5553"]', ['5553'], 'valid'], ['control ["77777779"]', ['77777779'], 'valid'], ['control ["52"]', ['52'], 'invalid'], ['control ["185"]', ['185'], 'invalid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["97959"]', ['97959'], 'invalid'], ['control ["60866459"]', ['60866459'], 'invalid'], ['control ["49717735278"]', ['49717735278'], 'invalid'], ['control ["9207919074166"]', ['9207919074166'], 'invalid'], ['control ["470663048070287"]', ['470663048070287'], 'invalid']]]
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 |
|---|---|---|---|
| regression ["4111-1111-1111-1111"] | valid | valid | Passed |
| regression ["4111-1111 1111-1112"] | invalid | invalid | Passed |
| partial-repair ["4111a1111"] | invalid | malformed | Failed |
| control ["59"] | valid | valid | Passed |
| control ["182"] | valid | valid | Passed |
| control ["97956"] | valid | valid | Passed |
| control ["60866456"] | valid | valid | Passed |
| control ["49717735275"] | valid | valid | Passed |
SHA-256 / a1909b0d5e7f482133005eef921710ae3b73619610b715746bf023dd86a79f61
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(s):
t = s.replace(' ', '').replace('-', '')
if not t.isascii() or not t.isdigit() or len(t) < 2 or len(t) > 19:
return 'malformed'
total = 0
for i, ch in enumerate(reversed(t)):
d = ord(ch) - 48
if i % 2 == 1:
d *= 2
if d > 9:
d -= 9
total += d
return 'valid' if total % 10 == 0 else 'invalid'
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["59"]', ['59'], 'valid'], ['control ["182"]', ['182'], 'valid'], ['control ["97956"]', ['97956'], 'valid'], ['control ["60866456"]', ['60866456'], 'valid'], ['control ["49717735275"]', ['49717735275'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["9207919074163"]', ['9207919074163'], 'valid'], ['control ["470663048070284"]', ['470663048070284'], 'valid'], ['control ["2300017738893848"]', ['2300017738893848'], 'valid'], ['control ["7985145013991721"]', ['7985145013991721'], 'valid'], ['control ["067122965057139178"]', ['067122965057139178'], 'valid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["1513156399001929628"]', ['1513156399001929628'], 'valid'], ['control ["9505094299844733505"]', ['9505094299844733505'], 'valid'], ['control ["0906050370970"]', ['0906050370970'], 'valid'], ['control ["3165356977"]', ['3165356977'], 'valid'], ['control ["91"]', ['91'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["992"]', ['992'], 'valid'], ['control ["5553"]', ['5553'], 'valid'], ['control ["77777779"]', ['77777779'], 'valid'], ['control ["52"]', ['52'], 'invalid'], ['control ["185"]', ['185'], 'invalid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["97959"]', ['97959'], 'invalid'], ['control ["60866459"]', ['60866459'], 'invalid'], ['control ["49717735278"]', ['49717735278'], 'invalid'], ['control ["9207919074166"]', ['9207919074166'], 'invalid'], ['control ["470663048070287"]', ['470663048070287'], 'invalid']]]
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 |
|---|---|---|---|
| regression ["4111-1111-1111-1111"] | valid | valid | Passed |
| regression ["4111-1111 1111-1112"] | invalid | invalid | Passed |
| partial-repair ["4111a1111"] | malformed | malformed | Passed |
| control ["59"] | valid | valid | Passed |
| control ["182"] | valid | valid | Passed |
| control ["97956"] | valid | valid | Passed |
| control ["60866456"] | valid | valid | Passed |
| control ["49717735275"] | valid | valid | Passed |
SHA-256 / 79518d70bb9bbdc3f3c866b0200ab91a77cd25aa3ba6e8bf898b3dc11ed7fb2d
Verification & scope
A deterministic, bounded teaching model of the named scheme under the stated contract; not a certified validator. 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:48:37.804136+00:00.
Case digest / 54d43de9ee7ad688b0b430a9996fe878b2c900e96fcc0d29a8300cec18ec7b8e