FAILURE MAP
← Case archive

FA-72561 / Check-digit algorithms / Open access

ORCID accepts misplaced hyphens · case 01

Identifiers with shifted or missing group separators are accepted.

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

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 fixtureActualExpectedOutcome
regression ["00000-002-1825-0097"]['7', True]malformedFailed
regression ["0000-00021825-0097"]['7', True]malformedFailed
partial-repair ["000-00002-1825-0097"]['7', True]malformedFailed
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 fixtureActualExpectedOutcome
regression ["00000-002-1825-0097"]['7', True]malformedFailed
regression ["0000-00021825-0097"]malformedmalformedPassed
partial-repair ["000-00002-1825-0097"]['7', True]malformedFailed
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 fixtureActualExpectedOutcome
regression ["00000-002-1825-0097"]malformedmalformedPassed
regression ["0000-00021825-0097"]malformedmalformedPassed
partial-repair ["000-00002-1825-0097"]malformedmalformedPassed
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