FAILURE MAP
← Case archive

FA-74121 / Feature flag rollout bucketing / Open access

Semantic version clause: Build metadata makes versions invalid · case 01

Versions such as 1.0.0+build.5 fail every version clause.

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

ROOT CAUSE

The "+build" suffix is not stripped before parsing the numeric core.

VERIFIED REPAIR

Drop everything after the first "+" before parsing.

Unsuccessful approach: Stripping build metadata only from the prerelease part leaves release versions with build tags invalid.

Case contract

Versions are MAJOR[.MINOR[.PATCH]][-prerelease][+build]; missing components are 0, build metadata is ignored, anything else is invalid (return None). A release outranks its prereleases; prerelease identifiers compare numerically when all digits, lexically otherwise, numeric below alphanumeric, and a shorter identifier list ranks lower when it is a prefix. op is semVerLessThan, semVerEqual or semVerGreaterThan; unknown ops return None.

Why this case matters

Targeting by app version gates features to clients that can render them.

1 / The failure

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

N = 1
observations = []
def solve(version, op, target):
    def parse(s):
        core, _, pre = s.partition('-')
        parts = core.split('.')
        if not 1 <= len(parts) <= 3 or not all(p.isdigit() for p in parts):
            return None
        nums = [int(p) for p in parts] + [0] * (3 - len(parts))
        ids = pre.split('.') if pre else []
        return nums, ids
    def key_id(i):
        return (0, int(i), '') if i.isdigit() else (1, 0, i)
    def cmp(a, b):
        if a[0] != b[0]:
            return -1 if a[0] < b[0] else 1
        x, y = a[1], b[1]
        if not x or not y:
            return (len(x) == 0) - (len(y) == 0) if (x or y) else 0
        for p, q in zip(x, y):
            kp, kq = key_id(p), key_id(q)
            if kp != kq:
                return -1 if kp < kq else 1
        return (len(x) > len(y)) - (len(x) < len(y))
    a, b = parse(version), parse(target)
    if a is None or b is None:
        return None
    c = cmp(a, b)
    return {'semVerLessThan': c < 0, 'semVerEqual': c == 0, 'semVerGreaterThan': c > 0}.get(op)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('numeric prerelease identifiers compare as numbers',
   ['1.0.0-beta.11', 'semVerGreaterThan', '1.0.0-beta.2'],
   True),
  ('numeric identifier ranks below alphanumeric', ['1.0.0-1', 'semVerLessThan', '1.0.0-alpha'], True),
  ('release outranks its prerelease', ['1.0.0', 'semVerGreaterThan', '1.0.0-rc.1'], True),
  ('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('version pair sample 1', ['1.0.0-beta.11', 'semVerEqual', '1.0.0-alpha'], False),
  ('version pair sample 2', ['1.0.0-alpha', 'semVerEqual', '1.0.0'], False)],
 [('numeric identifier ranks below alphanumeric', ['1.0.0-1', 'semVerLessThan', '1.0.0-alpha'], True),
  ('release outranks its prerelease', ['1.0.0', 'semVerGreaterThan', '1.0.0-rc.1'], True),
  ('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('version pair sample 6', ['1.0.0.0', 'semVerEqual', '2.1'], None),
  ('version pair sample 32', ['1.0.0-beta.11', 'semVerEqual', '1.0.0+build.5'], False),
  ('version pair sample 36', ['1.0.0-beta.11', 'semVerLessThan', '1.0.0+build.5'], True)],
 [('release outranks its prerelease', ['1.0.0', 'semVerGreaterThan', '1.0.0-rc.1'], True),
  ('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('longer prerelease list ranks higher', ['1.0.0-alpha.1', 'semVerGreaterThan', '1.0.0-alpha'], True),
  ('version pair sample 10', ['1.0.0-alpha', 'semVerEqual', '1.0.0+build.5'], False),
  ('version pair sample 11', ['1.0.0+build.5', 'semVerLessThan', '1.0.0.0'], None),
  ('version pair sample 22', ['1.0.0-2', 'semVerEqual', '1.0.0+build.5'], False)],
 [('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('longer prerelease list ranks higher', ['1.0.0-alpha.1', 'semVerGreaterThan', '1.0.0-alpha'], True),
  ('minor ten beats minor nine', ['2.10.0', 'semVerGreaterThan', '2.9.0'], True),
  ('version pair sample 3', ['1.0.0-alpha.10', 'semVerLessThan', '1.0.0+build.5'], True),
  ('version pair sample 16', ['1.0.0-alpha.10', 'semVerEqual', '1.0.0'], False),
  ('version pair sample 44', ['1.0.0+build.5', 'semVerLessThan', '1.0.0-beta.2'], False)],
 [('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('longer prerelease list ranks higher', ['1.0.0-alpha.1', 'semVerGreaterThan', '1.0.0-alpha'], True),
  ('minor ten beats minor nine', ['2.10.0', 'semVerGreaterThan', '2.9.0'], True),
  ('four components are invalid', ['1.0.0.0', 'semVerEqual', '1.0.0'], None),
  ('version pair sample 21', ['2.1.0', 'semVerGreaterThan', '2.1'], False),
  ('version pair sample 27', ['1.0.0-10', 'semVerLessThan', '1.0.0+build.5'], True),
  ('version pair sample 42', ['1.0.0-beta.2', 'semVerGreaterThan', '1.0.0+build.5'], False)]]
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
numeric prerelease identifiers compare as numbersTrueTruePassed
numeric identifier ranks below alphanumericTrueTruePassed
release outranks its prereleaseTrueTruePassed
two-component version pads patchTrueTruePassed
build metadata is ignoredNoneTrueFailed
build metadata after prerelease is ignoredFalseTrueFailed
version pair sample 1FalseFalsePassed
version pair sample 2FalseFalsePassed

SHA-256 / 4a6633caf8c40818de77db68504e52e354285979a0e58bf8551abdc056d128f3

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(version, op, target):
    def parse(s):
        core, _, pre = s.partition('-')
        parts = core.split('.')
        if not 1 <= len(parts) <= 3 or not all(p.isdigit() for p in parts):
            return None
        nums = [int(p) for p in parts] + [0] * (3 - len(parts))
        ids = pre.split('+')[0].split('.') if pre else []
        return nums, ids
    def key_id(i):
        return (0, int(i), '') if i.isdigit() else (1, 0, i)
    def cmp(a, b):
        if a[0] != b[0]:
            return -1 if a[0] < b[0] else 1
        x, y = a[1], b[1]
        if not x or not y:
            return (len(x) == 0) - (len(y) == 0) if (x or y) else 0
        for p, q in zip(x, y):
            kp, kq = key_id(p), key_id(q)
            if kp != kq:
                return -1 if kp < kq else 1
        return (len(x) > len(y)) - (len(x) < len(y))
    a, b = parse(version), parse(target)
    if a is None or b is None:
        return None
    c = cmp(a, b)
    return {'semVerLessThan': c < 0, 'semVerEqual': c == 0, 'semVerGreaterThan': c > 0}.get(op)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('numeric prerelease identifiers compare as numbers',
   ['1.0.0-beta.11', 'semVerGreaterThan', '1.0.0-beta.2'],
   True),
  ('numeric identifier ranks below alphanumeric', ['1.0.0-1', 'semVerLessThan', '1.0.0-alpha'], True),
  ('release outranks its prerelease', ['1.0.0', 'semVerGreaterThan', '1.0.0-rc.1'], True),
  ('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('version pair sample 1', ['1.0.0-beta.11', 'semVerEqual', '1.0.0-alpha'], False),
  ('version pair sample 2', ['1.0.0-alpha', 'semVerEqual', '1.0.0'], False)],
 [('numeric identifier ranks below alphanumeric', ['1.0.0-1', 'semVerLessThan', '1.0.0-alpha'], True),
  ('release outranks its prerelease', ['1.0.0', 'semVerGreaterThan', '1.0.0-rc.1'], True),
  ('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('version pair sample 6', ['1.0.0.0', 'semVerEqual', '2.1'], None),
  ('version pair sample 32', ['1.0.0-beta.11', 'semVerEqual', '1.0.0+build.5'], False),
  ('version pair sample 36', ['1.0.0-beta.11', 'semVerLessThan', '1.0.0+build.5'], True)],
 [('release outranks its prerelease', ['1.0.0', 'semVerGreaterThan', '1.0.0-rc.1'], True),
  ('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('longer prerelease list ranks higher', ['1.0.0-alpha.1', 'semVerGreaterThan', '1.0.0-alpha'], True),
  ('version pair sample 10', ['1.0.0-alpha', 'semVerEqual', '1.0.0+build.5'], False),
  ('version pair sample 11', ['1.0.0+build.5', 'semVerLessThan', '1.0.0.0'], None),
  ('version pair sample 22', ['1.0.0-2', 'semVerEqual', '1.0.0+build.5'], False)],
 [('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('longer prerelease list ranks higher', ['1.0.0-alpha.1', 'semVerGreaterThan', '1.0.0-alpha'], True),
  ('minor ten beats minor nine', ['2.10.0', 'semVerGreaterThan', '2.9.0'], True),
  ('version pair sample 3', ['1.0.0-alpha.10', 'semVerLessThan', '1.0.0+build.5'], True),
  ('version pair sample 16', ['1.0.0-alpha.10', 'semVerEqual', '1.0.0'], False),
  ('version pair sample 44', ['1.0.0+build.5', 'semVerLessThan', '1.0.0-beta.2'], False)],
 [('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('longer prerelease list ranks higher', ['1.0.0-alpha.1', 'semVerGreaterThan', '1.0.0-alpha'], True),
  ('minor ten beats minor nine', ['2.10.0', 'semVerGreaterThan', '2.9.0'], True),
  ('four components are invalid', ['1.0.0.0', 'semVerEqual', '1.0.0'], None),
  ('version pair sample 21', ['2.1.0', 'semVerGreaterThan', '2.1'], False),
  ('version pair sample 27', ['1.0.0-10', 'semVerLessThan', '1.0.0+build.5'], True),
  ('version pair sample 42', ['1.0.0-beta.2', 'semVerGreaterThan', '1.0.0+build.5'], False)]]
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
numeric prerelease identifiers compare as numbersTrueTruePassed
numeric identifier ranks below alphanumericTrueTruePassed
release outranks its prereleaseTrueTruePassed
two-component version pads patchTrueTruePassed
build metadata is ignoredNoneTrueFailed
build metadata after prerelease is ignoredTrueTruePassed
version pair sample 1FalseFalsePassed
version pair sample 2FalseFalsePassed

SHA-256 / ed4bd32ce508de08c416238cdab1c2b9c38c86e260f5e9be6aedec3d6256e602

3 / The verified repair

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

N = 1
observations = []
def solve(version, op, target):
    def parse(s):
        s = s.split('+', 1)[0]
        core, _, pre = s.partition('-')
        parts = core.split('.')
        if not 1 <= len(parts) <= 3 or not all(p.isdigit() for p in parts):
            return None
        nums = [int(p) for p in parts] + [0] * (3 - len(parts))
        ids = pre.split('.') if pre else []
        return nums, ids
    def key_id(i):
        return (0, int(i), '') if i.isdigit() else (1, 0, i)
    def cmp(a, b):
        if a[0] != b[0]:
            return -1 if a[0] < b[0] else 1
        x, y = a[1], b[1]
        if not x or not y:
            return (len(x) == 0) - (len(y) == 0) if (x or y) else 0
        for p, q in zip(x, y):
            kp, kq = key_id(p), key_id(q)
            if kp != kq:
                return -1 if kp < kq else 1
        return (len(x) > len(y)) - (len(x) < len(y))
    a, b = parse(version), parse(target)
    if a is None or b is None:
        return None
    c = cmp(a, b)
    return {'semVerLessThan': c < 0, 'semVerEqual': c == 0, 'semVerGreaterThan': c > 0}.get(op)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('numeric prerelease identifiers compare as numbers',
   ['1.0.0-beta.11', 'semVerGreaterThan', '1.0.0-beta.2'],
   True),
  ('numeric identifier ranks below alphanumeric', ['1.0.0-1', 'semVerLessThan', '1.0.0-alpha'], True),
  ('release outranks its prerelease', ['1.0.0', 'semVerGreaterThan', '1.0.0-rc.1'], True),
  ('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('version pair sample 1', ['1.0.0-beta.11', 'semVerEqual', '1.0.0-alpha'], False),
  ('version pair sample 2', ['1.0.0-alpha', 'semVerEqual', '1.0.0'], False)],
 [('numeric identifier ranks below alphanumeric', ['1.0.0-1', 'semVerLessThan', '1.0.0-alpha'], True),
  ('release outranks its prerelease', ['1.0.0', 'semVerGreaterThan', '1.0.0-rc.1'], True),
  ('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('version pair sample 6', ['1.0.0.0', 'semVerEqual', '2.1'], None),
  ('version pair sample 32', ['1.0.0-beta.11', 'semVerEqual', '1.0.0+build.5'], False),
  ('version pair sample 36', ['1.0.0-beta.11', 'semVerLessThan', '1.0.0+build.5'], True)],
 [('release outranks its prerelease', ['1.0.0', 'semVerGreaterThan', '1.0.0-rc.1'], True),
  ('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('longer prerelease list ranks higher', ['1.0.0-alpha.1', 'semVerGreaterThan', '1.0.0-alpha'], True),
  ('version pair sample 10', ['1.0.0-alpha', 'semVerEqual', '1.0.0+build.5'], False),
  ('version pair sample 11', ['1.0.0+build.5', 'semVerLessThan', '1.0.0.0'], None),
  ('version pair sample 22', ['1.0.0-2', 'semVerEqual', '1.0.0+build.5'], False)],
 [('two-component version pads patch', ['2.1', 'semVerEqual', '2.1.0'], True),
  ('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('longer prerelease list ranks higher', ['1.0.0-alpha.1', 'semVerGreaterThan', '1.0.0-alpha'], True),
  ('minor ten beats minor nine', ['2.10.0', 'semVerGreaterThan', '2.9.0'], True),
  ('version pair sample 3', ['1.0.0-alpha.10', 'semVerLessThan', '1.0.0+build.5'], True),
  ('version pair sample 16', ['1.0.0-alpha.10', 'semVerEqual', '1.0.0'], False),
  ('version pair sample 44', ['1.0.0+build.5', 'semVerLessThan', '1.0.0-beta.2'], False)],
 [('build metadata is ignored', ['1.0.0+build.5', 'semVerEqual', '1.0.0'], True),
  ('build metadata after prerelease is ignored', ['1.0.0-rc.1+b7', 'semVerEqual', '1.0.0-rc.1'], True),
  ('longer prerelease list ranks higher', ['1.0.0-alpha.1', 'semVerGreaterThan', '1.0.0-alpha'], True),
  ('minor ten beats minor nine', ['2.10.0', 'semVerGreaterThan', '2.9.0'], True),
  ('four components are invalid', ['1.0.0.0', 'semVerEqual', '1.0.0'], None),
  ('version pair sample 21', ['2.1.0', 'semVerGreaterThan', '2.1'], False),
  ('version pair sample 27', ['1.0.0-10', 'semVerLessThan', '1.0.0+build.5'], True),
  ('version pair sample 42', ['1.0.0-beta.2', 'semVerGreaterThan', '1.0.0+build.5'], False)]]
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
numeric prerelease identifiers compare as numbersTrueTruePassed
numeric identifier ranks below alphanumericTrueTruePassed
release outranks its prereleaseTrueTruePassed
two-component version pads patchTrueTruePassed
build metadata is ignoredTrueTruePassed
build metadata after prerelease is ignoredTrueTruePassed
version pair sample 1FalseFalsePassed
version pair sample 2FalseFalsePassed

SHA-256 / 178763786c3835c478a40ae60d17cd48bea0bad7a26bbfd4008d0ed65cf3a01d

Verification & scope

A deterministic toy flag-evaluation model with a stipulated contract; it does not reproduce any vendor SDK byte for byte. 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:53.823905+00:00.

Case digest / 3ec51b34f863d2b5c8c8eb2ae4c82cbd7c9480f0c38e9d602e358ef187450948