FAILURE MAP
← Case archive

FA-96611 / Phone number formatting / Open access

tel URI builder: global numbers lose their plus sign · case 01

"+42 555..." becomes "tel:42-555...", which reads as a local number.

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

ROOT CAUSE

The plus sign is never written.

VERIFIED REPAIR

Write "+" for global numbers.

Unsuccessful approach: Writing "+" when a context is present marks local numbers as global and global ones as local.

Case contract

Input {"number", optional "ext", optional "context"}. A number starting with "+" is global. Digits are grouped at runs of visual separators (- . space parentheses); any other character returns None. Build "tel:" + ("+" if global) + groups joined by "-", then ";ext=" + digits when an extension is given, then for local numbers ";phone-context=" + context (required).

Why this case matters

tel URIs are exchanged between softphones, CRMs and SIP gateways; a missing context or misordered parameter makes local numbers undialable.

1 / The failure

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

N = 1
observations = []
def solve(x):
    num, ext, ctx = x['number'].strip(), x.get('ext', ''), x.get('context', '')
    glob = num.startswith('+')
    body = num[1:] if glob else num
    parts, cur = [], ''
    for c in body:
        if c.isdigit():
            cur += c
        elif c in '-. ()':
            if cur:
                parts.append(cur)
                cur = ''
        else:
            return None
    if cur:
        parts.append(cur)
    if not parts:
        return None
    uri = 'tel:' + '' + '-'.join(parts)
    if ext:
        if not ext.isdigit():
            return None
        uri += ';ext=' + ext
    if not glob:
        if not ctx:
            return None
        uri += ';phone-context=' + ctx
    return uri
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['global number', {'number': '+42 555-010-1211'}, 'tel:+42-555-010-1211'], ['local number with context', {'number': '555-0114', 'context': '+42'}, 'tel:555-0114;phone-context=+42'], ['local number with extension', {'number': '555-0114', 'ext': '12', 'context': '+42'}, 'tel:555-0114;ext=12;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0114', 'ext': '1'}, 'tel:+42-555-0114;ext=1'], ['local without context', {'number': '5550114'}, None], ['run of separators', {'number': '+42 555 - 0114'}, 'tel:+42-555-0114'], ['slash is not a visual separator', {'number': '+42 555/0114'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0114', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1222'}, 'tel:+42-555-010-1222'], ['local number with context', {'number': '555-0125', 'context': '+42'}, 'tel:555-0125;phone-context=+42'], ['local number with extension', {'number': '555-0125', 'ext': '22', 'context': '+42'}, 'tel:555-0125;ext=22;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0125', 'ext': '2'}, 'tel:+42-555-0125;ext=2'], ['local without context', {'number': '5550125'}, None], ['run of separators', {'number': '+42 555 - 0125'}, 'tel:+42-555-0125'], ['slash is not a visual separator', {'number': '+42 555/0125'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0125', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1233'}, 'tel:+42-555-010-1233'], ['local number with context', {'number': '555-0136', 'context': '+42'}, 'tel:555-0136;phone-context=+42'], ['local number with extension', {'number': '555-0136', 'ext': '32', 'context': '+42'}, 'tel:555-0136;ext=32;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0136', 'ext': '3'}, 'tel:+42-555-0136;ext=3'], ['local without context', {'number': '5550136'}, None], ['run of separators', {'number': '+42 555 - 0136'}, 'tel:+42-555-0136'], ['slash is not a visual separator', {'number': '+42 555/0136'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0136', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1244'}, 'tel:+42-555-010-1244'], ['local number with context', {'number': '555-0147', 'context': '+42'}, 'tel:555-0147;phone-context=+42'], ['local number with extension', {'number': '555-0147', 'ext': '42', 'context': '+42'}, 'tel:555-0147;ext=42;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0147', 'ext': '4'}, 'tel:+42-555-0147;ext=4'], ['local without context', {'number': '5550147'}, None], ['run of separators', {'number': '+42 555 - 0147'}, 'tel:+42-555-0147'], ['slash is not a visual separator', {'number': '+42 555/0147'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0147', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1255'}, 'tel:+42-555-010-1255'], ['local number with context', {'number': '555-0158', 'context': '+42'}, 'tel:555-0158;phone-context=+42'], ['local number with extension', {'number': '555-0158', 'ext': '52', 'context': '+42'}, 'tel:555-0158;ext=52;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0158', 'ext': '5'}, 'tel:+42-555-0158;ext=5'], ['local without context', {'number': '5550158'}, None], ['run of separators', {'number': '+42 555 - 0158'}, 'tel:+42-555-0158'], ['slash is not a visual separator', {'number': '+42 555/0158'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0158', 'ext': '1a'}, None]]]
for label, arg, expected in fixtures[N - 1]:
    check(label, solve(arg), 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
global numbertel:42-555-010-1211tel:+42-555-010-1211Failed
local number with contexttel:555-0114;phone-context=+42tel:555-0114;phone-context=+42Passed
local number with extensiontel:555-0114;ext=12;phone-context=+42tel:555-0114;ext=12;phone-context=+42Passed
global with extensiontel:42-555-0114;ext=1tel:+42-555-0114;ext=1Failed
local without contextNoneNonePassed
run of separatorstel:42-555-0114tel:+42-555-0114Failed
slash is not a visual separatorNoneNonePassed
letters rejectedNoneNonePassed
non-numeric extensionNoneNonePassed

SHA-256 / 177d9616f320b4c45fa9f611d592685b3ee2e11043604fc4dfab79b8cbab2064

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(x):
    num, ext, ctx = x['number'].strip(), x.get('ext', ''), x.get('context', '')
    glob = num.startswith('+')
    body = num[1:] if glob else num
    parts, cur = [], ''
    for c in body:
        if c.isdigit():
            cur += c
        elif c in '-. ()':
            if cur:
                parts.append(cur)
                cur = ''
        else:
            return None
    if cur:
        parts.append(cur)
    if not parts:
        return None
    uri = 'tel:' + ('+' if ctx else '') + '-'.join(parts)
    if ext:
        if not ext.isdigit():
            return None
        uri += ';ext=' + ext
    if not glob:
        if not ctx:
            return None
        uri += ';phone-context=' + ctx
    return uri
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['global number', {'number': '+42 555-010-1211'}, 'tel:+42-555-010-1211'], ['local number with context', {'number': '555-0114', 'context': '+42'}, 'tel:555-0114;phone-context=+42'], ['local number with extension', {'number': '555-0114', 'ext': '12', 'context': '+42'}, 'tel:555-0114;ext=12;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0114', 'ext': '1'}, 'tel:+42-555-0114;ext=1'], ['local without context', {'number': '5550114'}, None], ['run of separators', {'number': '+42 555 - 0114'}, 'tel:+42-555-0114'], ['slash is not a visual separator', {'number': '+42 555/0114'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0114', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1222'}, 'tel:+42-555-010-1222'], ['local number with context', {'number': '555-0125', 'context': '+42'}, 'tel:555-0125;phone-context=+42'], ['local number with extension', {'number': '555-0125', 'ext': '22', 'context': '+42'}, 'tel:555-0125;ext=22;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0125', 'ext': '2'}, 'tel:+42-555-0125;ext=2'], ['local without context', {'number': '5550125'}, None], ['run of separators', {'number': '+42 555 - 0125'}, 'tel:+42-555-0125'], ['slash is not a visual separator', {'number': '+42 555/0125'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0125', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1233'}, 'tel:+42-555-010-1233'], ['local number with context', {'number': '555-0136', 'context': '+42'}, 'tel:555-0136;phone-context=+42'], ['local number with extension', {'number': '555-0136', 'ext': '32', 'context': '+42'}, 'tel:555-0136;ext=32;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0136', 'ext': '3'}, 'tel:+42-555-0136;ext=3'], ['local without context', {'number': '5550136'}, None], ['run of separators', {'number': '+42 555 - 0136'}, 'tel:+42-555-0136'], ['slash is not a visual separator', {'number': '+42 555/0136'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0136', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1244'}, 'tel:+42-555-010-1244'], ['local number with context', {'number': '555-0147', 'context': '+42'}, 'tel:555-0147;phone-context=+42'], ['local number with extension', {'number': '555-0147', 'ext': '42', 'context': '+42'}, 'tel:555-0147;ext=42;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0147', 'ext': '4'}, 'tel:+42-555-0147;ext=4'], ['local without context', {'number': '5550147'}, None], ['run of separators', {'number': '+42 555 - 0147'}, 'tel:+42-555-0147'], ['slash is not a visual separator', {'number': '+42 555/0147'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0147', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1255'}, 'tel:+42-555-010-1255'], ['local number with context', {'number': '555-0158', 'context': '+42'}, 'tel:555-0158;phone-context=+42'], ['local number with extension', {'number': '555-0158', 'ext': '52', 'context': '+42'}, 'tel:555-0158;ext=52;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0158', 'ext': '5'}, 'tel:+42-555-0158;ext=5'], ['local without context', {'number': '5550158'}, None], ['run of separators', {'number': '+42 555 - 0158'}, 'tel:+42-555-0158'], ['slash is not a visual separator', {'number': '+42 555/0158'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0158', 'ext': '1a'}, None]]]
for label, arg, expected in fixtures[N - 1]:
    check(label, solve(arg), 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
global numbertel:42-555-010-1211tel:+42-555-010-1211Failed
local number with contexttel:+555-0114;phone-context=+42tel:555-0114;phone-context=+42Failed
local number with extensiontel:+555-0114;ext=12;phone-context=+42tel:555-0114;ext=12;phone-context=+42Failed
global with extensiontel:42-555-0114;ext=1tel:+42-555-0114;ext=1Failed
local without contextNoneNonePassed
run of separatorstel:42-555-0114tel:+42-555-0114Failed
slash is not a visual separatorNoneNonePassed
letters rejectedNoneNonePassed
non-numeric extensionNoneNonePassed

SHA-256 / 7e5a0987facbb130197cb57500ddfcdd0094578a5055fa327a3b85a5bcf5a1d6

3 / The verified repair

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

N = 1
observations = []
def solve(x):
    num, ext, ctx = x['number'].strip(), x.get('ext', ''), x.get('context', '')
    glob = num.startswith('+')
    body = num[1:] if glob else num
    parts, cur = [], ''
    for c in body:
        if c.isdigit():
            cur += c
        elif c in '-. ()':
            if cur:
                parts.append(cur)
                cur = ''
        else:
            return None
    if cur:
        parts.append(cur)
    if not parts:
        return None
    uri = 'tel:' + ('+' if glob else '') + '-'.join(parts)
    if ext:
        if not ext.isdigit():
            return None
        uri += ';ext=' + ext
    if not glob:
        if not ctx:
            return None
        uri += ';phone-context=' + ctx
    return uri
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['global number', {'number': '+42 555-010-1211'}, 'tel:+42-555-010-1211'], ['local number with context', {'number': '555-0114', 'context': '+42'}, 'tel:555-0114;phone-context=+42'], ['local number with extension', {'number': '555-0114', 'ext': '12', 'context': '+42'}, 'tel:555-0114;ext=12;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0114', 'ext': '1'}, 'tel:+42-555-0114;ext=1'], ['local without context', {'number': '5550114'}, None], ['run of separators', {'number': '+42 555 - 0114'}, 'tel:+42-555-0114'], ['slash is not a visual separator', {'number': '+42 555/0114'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0114', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1222'}, 'tel:+42-555-010-1222'], ['local number with context', {'number': '555-0125', 'context': '+42'}, 'tel:555-0125;phone-context=+42'], ['local number with extension', {'number': '555-0125', 'ext': '22', 'context': '+42'}, 'tel:555-0125;ext=22;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0125', 'ext': '2'}, 'tel:+42-555-0125;ext=2'], ['local without context', {'number': '5550125'}, None], ['run of separators', {'number': '+42 555 - 0125'}, 'tel:+42-555-0125'], ['slash is not a visual separator', {'number': '+42 555/0125'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0125', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1233'}, 'tel:+42-555-010-1233'], ['local number with context', {'number': '555-0136', 'context': '+42'}, 'tel:555-0136;phone-context=+42'], ['local number with extension', {'number': '555-0136', 'ext': '32', 'context': '+42'}, 'tel:555-0136;ext=32;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0136', 'ext': '3'}, 'tel:+42-555-0136;ext=3'], ['local without context', {'number': '5550136'}, None], ['run of separators', {'number': '+42 555 - 0136'}, 'tel:+42-555-0136'], ['slash is not a visual separator', {'number': '+42 555/0136'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0136', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1244'}, 'tel:+42-555-010-1244'], ['local number with context', {'number': '555-0147', 'context': '+42'}, 'tel:555-0147;phone-context=+42'], ['local number with extension', {'number': '555-0147', 'ext': '42', 'context': '+42'}, 'tel:555-0147;ext=42;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0147', 'ext': '4'}, 'tel:+42-555-0147;ext=4'], ['local without context', {'number': '5550147'}, None], ['run of separators', {'number': '+42 555 - 0147'}, 'tel:+42-555-0147'], ['slash is not a visual separator', {'number': '+42 555/0147'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0147', 'ext': '1a'}, None]], [['global number', {'number': '+42 555-010-1255'}, 'tel:+42-555-010-1255'], ['local number with context', {'number': '555-0158', 'context': '+42'}, 'tel:555-0158;phone-context=+42'], ['local number with extension', {'number': '555-0158', 'ext': '52', 'context': '+42'}, 'tel:555-0158;ext=52;phone-context=+42'], ['global with extension', {'number': '+42 (555) 0158', 'ext': '5'}, 'tel:+42-555-0158;ext=5'], ['local without context', {'number': '5550158'}, None], ['run of separators', {'number': '+42 555 - 0158'}, 'tel:+42-555-0158'], ['slash is not a visual separator', {'number': '+42 555/0158'}, None], ['letters rejected', {'number': '+42 555 FAKE'}, None], ['non-numeric extension', {'number': '+42 555 0158', 'ext': '1a'}, None]]]
for label, arg, expected in fixtures[N - 1]:
    check(label, solve(arg), 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
global numbertel:+42-555-010-1211tel:+42-555-010-1211Passed
local number with contexttel:555-0114;phone-context=+42tel:555-0114;phone-context=+42Passed
local number with extensiontel:555-0114;ext=12;phone-context=+42tel:555-0114;ext=12;phone-context=+42Passed
global with extensiontel:+42-555-0114;ext=1tel:+42-555-0114;ext=1Passed
local without contextNoneNonePassed
run of separatorstel:+42-555-0114tel:+42-555-0114Passed
slash is not a visual separatorNoneNonePassed
letters rejectedNoneNonePassed
non-numeric extensionNoneNonePassed

SHA-256 / 4d179db71c8c01f24d360bd5410491f19a8f7777909b0f89af55d69b0a3d4707

Verification & scope

A deterministic toy contract over synthetic, obviously fictitious telephone numbers and toy country tables. The rules are stipulated for this model only and make no claim of conformance with any numbering plan or telecommunications standard. 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:24.338374+00:00.

Case digest / f13940b2f240d75356a4901d4d4bd698e277ef9b43f2fd2359d582baa07a6be1