FAILURE MAP
← Case archive

FA-82656 / Chess move legality / Open access

Ambiguous SAN silently resolved to one piece · case 01

"Nd2" with knights on b1 and f3 returns one origin instead of "ambiguous".

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

ROOT CAUSE

The first candidate is returned whenever any exists.

THE FAILURE

The first candidate is returned whenever any exists.

Unsuccessful approach: Sorting before choosing is deterministic but still hides the ambiguity.

Case contract

Input: {"board", "white": side to move, "san"} for a non-castling move. Check suffixes + and # are ignored. Resolve the SAN to the unique origin square of a pseudo-legal move (sliders need a clear path; pawns push one or, from their start rank through an empty square, two; pawns capture one step diagonally forward). "x" requires an enemy piece on the target and its absence requires an empty target. File/rank disambiguators filter candidates. Return the origin, "ambiguous" or "illegal".

Why this case matters

PGN import and move entry depend on resolving SAN to exactly one move.

1 / The failure

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

N = 1
observations = []
def solve(x):
    b=x['board']; white=x['white']; s=x['san'].rstrip('+#')
    def c(q): return ord(q[0])-97,int(q[1])-1
    def n(f,r): return chr(97+f)+str(r+1)
    piece=s[0] if s[0] in 'NBRQK' else 'P'
    body=s[1:] if piece!='P' else s
    capture='x' in body
    body=body.replace('x','')
    to=body[-2:]; dis=body[:-2]
    tf,tr=c(to)
    occ=b.get(to)
    if capture and (occ is None or occ.isupper()==white): return 'illegal'
    if not capture and occ is not None: return 'illegal'
    letter=piece if white else piece.lower()
    cands=[]
    for q,v in b.items():
        if v!=letter: continue
        f,r=c(q)
        if piece=='P':
            d=1 if white else -1
            if capture:
                ok=abs(tf-f)==1 and tr-r==d
            else:
                ok=tf==f and (tr-r==d or (tr-r==2*d and r==(1 if white else 6) and n(f,r+d) not in b))
        elif piece=='N':
            ok=(abs(tf-f),abs(tr-r)) in ((1,2),(2,1))
        elif piece=='K':
            ok=max(abs(tf-f),abs(tr-r))==1
        else:
            df,dr=tf-f,tr-r
            orth=(df==0)!=(dr==0); diag=abs(df)==abs(dr) and df!=0
            ok=(piece=='R' and orth) or (piece=='B' and diag) or (piece=='Q' and (orth or diag))
            if ok:
                sf=(df>0)-(df<0); sr=(dr>0)-(dr<0)
                cf,cr=f+sf,r+sr
                while (cf,cr)!=(tf,tr):
                    if n(cf,cr) in b: ok=False
                    cf+=sf; cr+=sr
        if ok and all((ch==q[0]) if ch.isalpha() else (ch==q[1]) for ch in dis):
            cands.append(q)
    if cands: return cands[0]
    return 'ambiguous' if cands else 'illegal'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('knight file disambiguation',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nbd2', 'white': True}, 'b1'),
  ('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('rook rank disambiguation',
   {'board': {'a1': 'R', 'a5': 'R', 'e1': 'K', 'e8': 'k'}, 'san': 'R1a3', 'white': True}, 'a1'),
  ('rook rank disambiguation upper',
   {'board': {'a1': 'R', 'a5': 'R', 'e1': 'K', 'e8': 'k'}, 'san': 'R5a3', 'white': True}, 'a5'),
  ('mate suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qf7#', 'white': True}, 'h5'),
  ('check suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qe5+', 'white': True},
   'h5'),
  ('quiet move onto enemy',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'p', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal'),
  ('quiet move onto own',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'P', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal')],
 [('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('mate suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qf7#', 'white': True}, 'h5'),
  ('check suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qe5+', 'white': True},
   'h5'),
  ('quiet move onto enemy',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'p', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal'),
  ('quiet move onto own',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'P', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal'),
  ('capture with x',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'p', 'g1': 'N'}, 'san': 'Nxf3', 'white': True}, 'g1'),
  ('black double push', {'board': {'d7': 'p', 'e1': 'K', 'e8': 'k'}, 'san': 'd5', 'white': False},
   'd7'),
  ('white double push', {'board': {'e1': 'K', 'e2': 'P', 'e8': 'k'}, 'san': 'e4', 'white': True},
   'e2')],
 [('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('quiet move onto own',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'P', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal'),
  ('capture with x',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'p', 'g1': 'N'}, 'san': 'Nxf3', 'white': True}, 'g1'),
  ('black double push', {'board': {'d7': 'p', 'e1': 'K', 'e8': 'k'}, 'san': 'd5', 'white': False},
   'd7'),
  ('white double push', {'board': {'e1': 'K', 'e2': 'P', 'e8': 'k'}, 'san': 'e4', 'white': True},
   'e2'),
  ('pawn capture file',
   {'board': {'c4': 'P', 'd5': 'p', 'e1': 'K', 'e4': 'P', 'e8': 'k'}, 'san': 'exd5', 'white': True},
   'e4'),
  ('backward pawn capture',
   {'board': {'d3': 'p', 'e1': 'K', 'e4': 'P', 'e8': 'k'}, 'san': 'exd3', 'white': True},
   'illegal'),
  ('black pawn capture',
   {'board': {'d4': 'P', 'e1': 'K', 'e5': 'p', 'e8': 'k'}, 'san': 'exd4', 'white': False}, 'e5')],
 [('knight file disambiguation',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nbd2', 'white': True}, 'b1'),
  ('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('white double push', {'board': {'e1': 'K', 'e2': 'P', 'e8': 'k'}, 'san': 'e4', 'white': True},
   'e2'),
  ('pawn capture file',
   {'board': {'c4': 'P', 'd5': 'p', 'e1': 'K', 'e4': 'P', 'e8': 'k'}, 'san': 'exd5', 'white': True},
   'e4'),
  ('backward pawn capture',
   {'board': {'d3': 'p', 'e1': 'K', 'e4': 'P', 'e8': 'k'}, 'san': 'exd3', 'white': True},
   'illegal'),
  ('black pawn capture',
   {'board': {'d4': 'P', 'e1': 'K', 'e5': 'p', 'e8': 'k'}, 'san': 'exd4', 'white': False}, 'e5'),
  ('rook blocked candidate',
   {'board': {'a1': 'R', 'c1': 'B', 'e2': 'K', 'e8': 'k', 'h1': 'R'}, 'san': 'Rd1', 'white': True},
   'h1'),
  ('bishop simple', {'board': {'c1': 'B', 'e1': 'K', 'e8': 'k'}, 'san': 'Bg5', 'white': True},
   'c1')],
 [('knight file disambiguation',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nbd2', 'white': True}, 'b1'),
  ('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('rook rank disambiguation',
   {'board': {'a1': 'R', 'a5': 'R', 'e1': 'K', 'e8': 'k'}, 'san': 'R1a3', 'white': True}, 'a1'),
  ('rook rank disambiguation upper',
   {'board': {'a1': 'R', 'a5': 'R', 'e1': 'K', 'e8': 'k'}, 'san': 'R5a3', 'white': True}, 'a5'),
  ('mate suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qf7#', 'white': True}, 'h5'),
  ('black pawn capture',
   {'board': {'d4': 'P', 'e1': 'K', 'e5': 'p', 'e8': 'k'}, 'san': 'exd4', 'white': False}, 'e5'),
  ('rook blocked candidate',
   {'board': {'a1': 'R', 'c1': 'B', 'e2': 'K', 'e8': 'k', 'h1': 'R'}, 'san': 'Rd1', 'white': True},
   'h1'),
  ('bishop simple', {'board': {'c1': 'B', 'e1': 'K', 'e8': 'k'}, 'san': 'Bg5', 'white': True},
   'c1')]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'error:' + type(exc).__name__
    check(label, actual, 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
knight file disambiguationb1b1Passed
knight ambiguousb1ambiguousFailed
rook rank disambiguationa1a1Passed
rook rank disambiguation uppera5a5Passed
mate suffixh5h5Passed
check suffixh5h5Passed
quiet move onto enemyillegalillegalPassed
quiet move onto ownillegalillegalPassed

SHA-256 / 003fbfc44f1dcbd6456a48946670dfc23c16caaa85358d690fbd0a475d5453a2

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(x):
    b=x['board']; white=x['white']; s=x['san'].rstrip('+#')
    def c(q): return ord(q[0])-97,int(q[1])-1
    def n(f,r): return chr(97+f)+str(r+1)
    piece=s[0] if s[0] in 'NBRQK' else 'P'
    body=s[1:] if piece!='P' else s
    capture='x' in body
    body=body.replace('x','')
    to=body[-2:]; dis=body[:-2]
    tf,tr=c(to)
    occ=b.get(to)
    if capture and (occ is None or occ.isupper()==white): return 'illegal'
    if not capture and occ is not None: return 'illegal'
    letter=piece if white else piece.lower()
    cands=[]
    for q,v in b.items():
        if v!=letter: continue
        f,r=c(q)
        if piece=='P':
            d=1 if white else -1
            if capture:
                ok=abs(tf-f)==1 and tr-r==d
            else:
                ok=tf==f and (tr-r==d or (tr-r==2*d and r==(1 if white else 6) and n(f,r+d) not in b))
        elif piece=='N':
            ok=(abs(tf-f),abs(tr-r)) in ((1,2),(2,1))
        elif piece=='K':
            ok=max(abs(tf-f),abs(tr-r))==1
        else:
            df,dr=tf-f,tr-r
            orth=(df==0)!=(dr==0); diag=abs(df)==abs(dr) and df!=0
            ok=(piece=='R' and orth) or (piece=='B' and diag) or (piece=='Q' and (orth or diag))
            if ok:
                sf=(df>0)-(df<0); sr=(dr>0)-(dr<0)
                cf,cr=f+sf,r+sr
                while (cf,cr)!=(tf,tr):
                    if n(cf,cr) in b: ok=False
                    cf+=sf; cr+=sr
        if ok and all((ch==q[0]) if ch.isalpha() else (ch==q[1]) for ch in dis):
            cands.append(q)
    if cands: return sorted(cands)[0]
    return 'ambiguous' if cands else 'illegal'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('knight file disambiguation',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nbd2', 'white': True}, 'b1'),
  ('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('rook rank disambiguation',
   {'board': {'a1': 'R', 'a5': 'R', 'e1': 'K', 'e8': 'k'}, 'san': 'R1a3', 'white': True}, 'a1'),
  ('rook rank disambiguation upper',
   {'board': {'a1': 'R', 'a5': 'R', 'e1': 'K', 'e8': 'k'}, 'san': 'R5a3', 'white': True}, 'a5'),
  ('mate suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qf7#', 'white': True}, 'h5'),
  ('check suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qe5+', 'white': True},
   'h5'),
  ('quiet move onto enemy',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'p', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal'),
  ('quiet move onto own',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'P', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal')],
 [('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('mate suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qf7#', 'white': True}, 'h5'),
  ('check suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qe5+', 'white': True},
   'h5'),
  ('quiet move onto enemy',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'p', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal'),
  ('quiet move onto own',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'P', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal'),
  ('capture with x',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'p', 'g1': 'N'}, 'san': 'Nxf3', 'white': True}, 'g1'),
  ('black double push', {'board': {'d7': 'p', 'e1': 'K', 'e8': 'k'}, 'san': 'd5', 'white': False},
   'd7'),
  ('white double push', {'board': {'e1': 'K', 'e2': 'P', 'e8': 'k'}, 'san': 'e4', 'white': True},
   'e2')],
 [('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('quiet move onto own',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'P', 'g1': 'N'}, 'san': 'Nf3', 'white': True},
   'illegal'),
  ('capture with x',
   {'board': {'e1': 'K', 'e8': 'k', 'f3': 'p', 'g1': 'N'}, 'san': 'Nxf3', 'white': True}, 'g1'),
  ('black double push', {'board': {'d7': 'p', 'e1': 'K', 'e8': 'k'}, 'san': 'd5', 'white': False},
   'd7'),
  ('white double push', {'board': {'e1': 'K', 'e2': 'P', 'e8': 'k'}, 'san': 'e4', 'white': True},
   'e2'),
  ('pawn capture file',
   {'board': {'c4': 'P', 'd5': 'p', 'e1': 'K', 'e4': 'P', 'e8': 'k'}, 'san': 'exd5', 'white': True},
   'e4'),
  ('backward pawn capture',
   {'board': {'d3': 'p', 'e1': 'K', 'e4': 'P', 'e8': 'k'}, 'san': 'exd3', 'white': True},
   'illegal'),
  ('black pawn capture',
   {'board': {'d4': 'P', 'e1': 'K', 'e5': 'p', 'e8': 'k'}, 'san': 'exd4', 'white': False}, 'e5')],
 [('knight file disambiguation',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nbd2', 'white': True}, 'b1'),
  ('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('white double push', {'board': {'e1': 'K', 'e2': 'P', 'e8': 'k'}, 'san': 'e4', 'white': True},
   'e2'),
  ('pawn capture file',
   {'board': {'c4': 'P', 'd5': 'p', 'e1': 'K', 'e4': 'P', 'e8': 'k'}, 'san': 'exd5', 'white': True},
   'e4'),
  ('backward pawn capture',
   {'board': {'d3': 'p', 'e1': 'K', 'e4': 'P', 'e8': 'k'}, 'san': 'exd3', 'white': True},
   'illegal'),
  ('black pawn capture',
   {'board': {'d4': 'P', 'e1': 'K', 'e5': 'p', 'e8': 'k'}, 'san': 'exd4', 'white': False}, 'e5'),
  ('rook blocked candidate',
   {'board': {'a1': 'R', 'c1': 'B', 'e2': 'K', 'e8': 'k', 'h1': 'R'}, 'san': 'Rd1', 'white': True},
   'h1'),
  ('bishop simple', {'board': {'c1': 'B', 'e1': 'K', 'e8': 'k'}, 'san': 'Bg5', 'white': True},
   'c1')],
 [('knight file disambiguation',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nbd2', 'white': True}, 'b1'),
  ('knight ambiguous',
   {'board': {'b1': 'N', 'e1': 'K', 'e8': 'k', 'f3': 'N'}, 'san': 'Nd2', 'white': True},
   'ambiguous'),
  ('rook rank disambiguation',
   {'board': {'a1': 'R', 'a5': 'R', 'e1': 'K', 'e8': 'k'}, 'san': 'R1a3', 'white': True}, 'a1'),
  ('rook rank disambiguation upper',
   {'board': {'a1': 'R', 'a5': 'R', 'e1': 'K', 'e8': 'k'}, 'san': 'R5a3', 'white': True}, 'a5'),
  ('mate suffix', {'board': {'e1': 'K', 'e8': 'k', 'h5': 'Q'}, 'san': 'Qf7#', 'white': True}, 'h5'),
  ('black pawn capture',
   {'board': {'d4': 'P', 'e1': 'K', 'e5': 'p', 'e8': 'k'}, 'san': 'exd4', 'white': False}, 'e5'),
  ('rook blocked candidate',
   {'board': {'a1': 'R', 'c1': 'B', 'e2': 'K', 'e8': 'k', 'h1': 'R'}, 'san': 'Rd1', 'white': True},
   'h1'),
  ('bishop simple', {'board': {'c1': 'B', 'e1': 'K', 'e8': 'k'}, 'san': 'Bg5', 'white': True},
   'c1')]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'error:' + type(exc).__name__
    check(label, actual, 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
knight file disambiguationb1b1Passed
knight ambiguousb1ambiguousFailed
rook rank disambiguationa1a1Passed
rook rank disambiguation uppera5a5Passed
mate suffixh5h5Passed
check suffixh5h5Passed
quiet move onto enemyillegalillegalPassed
quiet move onto ownillegalillegalPassed

SHA-256 / 866741e201bd031191c3ba4633f89cabb0d182974bb1374ade5a38c63ac74398

HELD IN THE MEMBER ARCHIVE

The verified repair and its recorded checks are member-only.

This mechanism has 8 recorded checks per implementation. The open-access tier publishes the failure and the unsuccessful fix; the repaired source that passes every check, and the observations that prove it, are available to members.

Every case sharing this mechanism uses the same contract and the same repair, so this one record is held back for all of them.

Member access is invitation-based. Sign in with your invited account to inspect the repair.

Sign in to the archive ↗

Verification & scope

A deterministic toy model with an explicitly stated contract; boards are {square: piece} with white upper-case and black lower-case pieces; no claim of full FIDE Laws conformance. 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:50:14.083211+00:00.

Case digest / 90fea83b1c3e66c558fb90e651534c005b1a701c9a1c9b01db591af7c23452c0