FA-72561 / Check-digit algorithms / Open access
ORCID accepts misplaced hyphens · case 01
Identifiers with shifted or missing group separators are accepted.
ROOT CAUSE
Hyphens are removed without checking that they separate four groups of four.
VERIFIED REPAIR
When hyphens are present require exactly four groups of four digits.
Unsuccessful approach: Counting three hyphens still admits groups of the wrong size.
Case contract
ORCID identifier check (ISO 7064 MOD 11-2). Input is 16 characters, either plain or as four hyphen-separated groups of four (other hyphen layouts "malformed"); fifteen digits then a digit or uppercase X. Fold total = (total + digit) * 2 over the fifteen digits; the check value is (12 - total % 11) % 11 with 10 written X. Return [check character, whether it matches].
Why this case matters
Research information systems verify ORCID iDs before linking publications to authors.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(s):
if False:
return 'malformed'
t = s.replace('-', '')
if len(t) != 16 or not t.isascii() or not t[:15].isdigit() or not (t[15].isdigit() or t[15] == 'X'):
return 'malformed'
total = 0
for ch in t[:15]:
total = (total + int(ch)) * 2
r = (12 - total % 11) % 11
check = 'X' if r == 10 else str(r)
return [check, check == t[15]]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-2284-2705-0317"]', ['0000-2284-2705-0317'], ['6', False]], ['control ["0000-6830-1348-0503"]', ['0000-6830-1348-0503'], ['5', False]], ['control ["0000-6511-5584-6923"]', ['0000-6511-5584-6923'], ['4', False]], ['control ["0000-4451-8984-5952"]', ['0000-4451-8984-5952'], ['7', False]], ['control ["0000-8150-6101-2864"]', ['0000-8150-6101-2864'], ['3', False]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-5056-4379-1736"]', ['0000-5056-4379-1736'], ['6', True]], ['control ["0000-8999-3438-1875"]', ['0000-8999-3438-1875'], ['0', False]], ['control ["0000-6263-5457-6118"]', ['0000-6263-5457-6118'], ['7', False]], ['control ["0000-2361-3351-5002"]', ['0000-2361-3351-5002'], ['X', False]], ['control ["0000-9095-7746-214X"]', ['0000-9095-7746-214X'], ['1', False]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-5390-9620-5628"]', ['0000-5390-9620-5628'], ['4', False]], ['control ["0000-4723-8762-7029"]', ['0000-4723-8762-7029'], ['2', False]], ['control ["0000-9157-2825-059X"]', ['0000-9157-2825-059X'], ['4', False]], ['control ["0000-9861-4470-6996"]', ['0000-9861-4470-6996'], ['8', False]], ['control ["7242310350419377"]', ['7242310350419377'], ['6', False]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["8604828079008568"]', ['8604828079008568'], ['1', False]], ['control ["2094476501967275"]', ['2094476501967275'], ['5', True]], ['control ["3417023077781379"]', ['3417023077781379'], ['2', False]], ['control ["0000-0002-1825-0097"]', ['0000-0002-1825-0097'], ['7', True]], ['control ["0000-0001-5109-3700"]', ['0000-0001-5109-3700'], ['0', True]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-0002-1694-233X"]', ['0000-0002-1694-233X'], ['X', True]], ['control ["0000000218250097"]', ['0000000218250097'], ['7', True]], ['control ["0000-0002-1825-0098"]', ['0000-0002-1825-0098'], ['7', False]], ['control ["0000-0002-1825-009"]', ['0000-0002-1825-009'], 'malformed'], ['control ["0000-0002-1694-233x"]', ['0000-0002-1694-233x'], 'malformed']]]
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 ["00000-002-1825-0097"] | ['7', True] | malformed | Failed |
| regression ["0000-00021825-0097"] | ['7', True] | malformed | Failed |
| partial-repair ["000-00002-1825-0097"] | ['7', True] | malformed | Failed |
| control ["0000-2284-2705-0317"] | ['6', False] | ['6', False] | Passed |
| control ["0000-6830-1348-0503"] | ['5', False] | ['5', False] | Passed |
| control ["0000-6511-5584-6923"] | ['4', False] | ['4', False] | Passed |
| control ["0000-4451-8984-5952"] | ['7', False] | ['7', False] | Passed |
| control ["0000-8150-6101-2864"] | ['3', False] | ['3', False] | Passed |
SHA-256 / 065f566d06b225d90f439274b95fced7d1d0f0b2a90785d2732ed589f52ef477
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(s):
if '-' in s and s.count('-') != 3:
return 'malformed'
t = s.replace('-', '')
if len(t) != 16 or not t.isascii() or not t[:15].isdigit() or not (t[15].isdigit() or t[15] == 'X'):
return 'malformed'
total = 0
for ch in t[:15]:
total = (total + int(ch)) * 2
r = (12 - total % 11) % 11
check = 'X' if r == 10 else str(r)
return [check, check == t[15]]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-2284-2705-0317"]', ['0000-2284-2705-0317'], ['6', False]], ['control ["0000-6830-1348-0503"]', ['0000-6830-1348-0503'], ['5', False]], ['control ["0000-6511-5584-6923"]', ['0000-6511-5584-6923'], ['4', False]], ['control ["0000-4451-8984-5952"]', ['0000-4451-8984-5952'], ['7', False]], ['control ["0000-8150-6101-2864"]', ['0000-8150-6101-2864'], ['3', False]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-5056-4379-1736"]', ['0000-5056-4379-1736'], ['6', True]], ['control ["0000-8999-3438-1875"]', ['0000-8999-3438-1875'], ['0', False]], ['control ["0000-6263-5457-6118"]', ['0000-6263-5457-6118'], ['7', False]], ['control ["0000-2361-3351-5002"]', ['0000-2361-3351-5002'], ['X', False]], ['control ["0000-9095-7746-214X"]', ['0000-9095-7746-214X'], ['1', False]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-5390-9620-5628"]', ['0000-5390-9620-5628'], ['4', False]], ['control ["0000-4723-8762-7029"]', ['0000-4723-8762-7029'], ['2', False]], ['control ["0000-9157-2825-059X"]', ['0000-9157-2825-059X'], ['4', False]], ['control ["0000-9861-4470-6996"]', ['0000-9861-4470-6996'], ['8', False]], ['control ["7242310350419377"]', ['7242310350419377'], ['6', False]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["8604828079008568"]', ['8604828079008568'], ['1', False]], ['control ["2094476501967275"]', ['2094476501967275'], ['5', True]], ['control ["3417023077781379"]', ['3417023077781379'], ['2', False]], ['control ["0000-0002-1825-0097"]', ['0000-0002-1825-0097'], ['7', True]], ['control ["0000-0001-5109-3700"]', ['0000-0001-5109-3700'], ['0', True]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-0002-1694-233X"]', ['0000-0002-1694-233X'], ['X', True]], ['control ["0000000218250097"]', ['0000000218250097'], ['7', True]], ['control ["0000-0002-1825-0098"]', ['0000-0002-1825-0098'], ['7', False]], ['control ["0000-0002-1825-009"]', ['0000-0002-1825-009'], 'malformed'], ['control ["0000-0002-1694-233x"]', ['0000-0002-1694-233x'], 'malformed']]]
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 ["00000-002-1825-0097"] | ['7', True] | malformed | Failed |
| regression ["0000-00021825-0097"] | malformed | malformed | Passed |
| partial-repair ["000-00002-1825-0097"] | ['7', True] | malformed | Failed |
| control ["0000-2284-2705-0317"] | ['6', False] | ['6', False] | Passed |
| control ["0000-6830-1348-0503"] | ['5', False] | ['5', False] | Passed |
| control ["0000-6511-5584-6923"] | ['4', False] | ['4', False] | Passed |
| control ["0000-4451-8984-5952"] | ['7', False] | ['7', False] | Passed |
| control ["0000-8150-6101-2864"] | ['3', False] | ['3', False] | Passed |
SHA-256 / e56c05683fc24d410db8ea229a84d9590459857870245e5324c60d8f6630f658
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(s):
if '-' in s and [len(g) for g in s.split('-')] != [4, 4, 4, 4]:
return 'malformed'
t = s.replace('-', '')
if len(t) != 16 or not t.isascii() or not t[:15].isdigit() or not (t[15].isdigit() or t[15] == 'X'):
return 'malformed'
total = 0
for ch in t[:15]:
total = (total + int(ch)) * 2
r = (12 - total % 11) % 11
check = 'X' if r == 10 else str(r)
return [check, check == t[15]]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-2284-2705-0317"]', ['0000-2284-2705-0317'], ['6', False]], ['control ["0000-6830-1348-0503"]', ['0000-6830-1348-0503'], ['5', False]], ['control ["0000-6511-5584-6923"]', ['0000-6511-5584-6923'], ['4', False]], ['control ["0000-4451-8984-5952"]', ['0000-4451-8984-5952'], ['7', False]], ['control ["0000-8150-6101-2864"]', ['0000-8150-6101-2864'], ['3', False]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-5056-4379-1736"]', ['0000-5056-4379-1736'], ['6', True]], ['control ["0000-8999-3438-1875"]', ['0000-8999-3438-1875'], ['0', False]], ['control ["0000-6263-5457-6118"]', ['0000-6263-5457-6118'], ['7', False]], ['control ["0000-2361-3351-5002"]', ['0000-2361-3351-5002'], ['X', False]], ['control ["0000-9095-7746-214X"]', ['0000-9095-7746-214X'], ['1', False]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-5390-9620-5628"]', ['0000-5390-9620-5628'], ['4', False]], ['control ["0000-4723-8762-7029"]', ['0000-4723-8762-7029'], ['2', False]], ['control ["0000-9157-2825-059X"]', ['0000-9157-2825-059X'], ['4', False]], ['control ["0000-9861-4470-6996"]', ['0000-9861-4470-6996'], ['8', False]], ['control ["7242310350419377"]', ['7242310350419377'], ['6', False]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["8604828079008568"]', ['8604828079008568'], ['1', False]], ['control ["2094476501967275"]', ['2094476501967275'], ['5', True]], ['control ["3417023077781379"]', ['3417023077781379'], ['2', False]], ['control ["0000-0002-1825-0097"]', ['0000-0002-1825-0097'], ['7', True]], ['control ["0000-0001-5109-3700"]', ['0000-0001-5109-3700'], ['0', True]]], [['regression ["00000-002-1825-0097"]', ['00000-002-1825-0097'], 'malformed'], ['regression ["0000-00021825-0097"]', ['0000-00021825-0097'], 'malformed'], ['partial-repair ["000-00002-1825-0097"]', ['000-00002-1825-0097'], 'malformed'], ['control ["0000-0002-1694-233X"]', ['0000-0002-1694-233X'], ['X', True]], ['control ["0000000218250097"]', ['0000000218250097'], ['7', True]], ['control ["0000-0002-1825-0098"]', ['0000-0002-1825-0098'], ['7', False]], ['control ["0000-0002-1825-009"]', ['0000-0002-1825-009'], 'malformed'], ['control ["0000-0002-1694-233x"]', ['0000-0002-1694-233x'], 'malformed']]]
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 ["00000-002-1825-0097"] | malformed | malformed | Passed |
| regression ["0000-00021825-0097"] | malformed | malformed | Passed |
| partial-repair ["000-00002-1825-0097"] | malformed | malformed | Passed |
| control ["0000-2284-2705-0317"] | ['6', False] | ['6', False] | Passed |
| control ["0000-6830-1348-0503"] | ['5', False] | ['5', False] | Passed |
| control ["0000-6511-5584-6923"] | ['4', False] | ['4', False] | Passed |
| control ["0000-4451-8984-5952"] | ['7', False] | ['7', False] | Passed |
| control ["0000-8150-6101-2864"] | ['3', False] | ['3', False] | Passed |
SHA-256 / d4a7972ea3370a75f36ce3d92186d1790a5ad9b2a8b241881934382fc5512cb0
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:39.649143+00:00.
Case digest / 7124531d6d64aa02934d82dd695e4e5dd13fe78945d2d62f2d4a428e57f727c7