FA-54796 / Accessibility interaction semantics / Open access
Semantic hit testing forgets document scroll displacement · case 01
Semantic hit testing forgets document scroll displacement.
ROOT CAUSE
The scroll origin decision in the platform hit adapter adapter violates its declared contract.
VERIFIED REPAIR
Apply the exact contract at the scroll origin decision: point[0]-origin[0]+scroll[0],point[1]-origin[1]+scroll[1]
Unsuccessful approach: The local repair substitutes point[0]-origin[0]+scroll[0],point[1]-origin[1] but still mishandles the regression fixtures.
Case contract
Input [window_origin,scroll,point,records,expected_window]. Pointer is screen coordinates. Convert to document coordinates by subtracting window_origin and adding scroll. Records [id,window,rect,z,exposed,transparent,clip,proxy,enabled]; rect and optional clip are [x0,y0,x1,y1], half-open. Zero/reversed rectangles never hit. Reject other windows or unexposed records. Transparent records do not themselves hit. Reject points outside clip. Among hits largest z wins, ties retain first input. Disabled semantics remain inspectable. Return proxy ID when nonempty, else own ID, plus [document_x,document_y]. None when no hit. This is semantic hit selection, not pixel rendering or focus navigation.
Why this case matters
Offline accessibility bridge model with explicit policy; useful for testing semantic API adapters independently of browser implementations.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(data):
origin,scroll,point,records,window = data
x,y = point[0]-origin[0],point[1]-origin[1]
hits = []
for index,(ident,owner,rect,z,exposed,transparent,clip,proxy,enabled) in enumerate(records):
if owner != window or not exposed or transparent: continue
x0,y0,x1,y1 = rect
if not (x0 <= x < x1 and y0 <= y < y1): continue
if clip is not None and not (clip[0] <= x < clip[2] and clip[1] <= y < clip[3]): continue
hits.append((z,-index,proxy if proxy else ident))
if not hits: return None
return [max(hits,key=lambda hit:(hit[0],hit[1]))[2],[x,y]]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
check('contract fixture 1', solve([[N,10],[N+1,3],[N+1,11],[['a','w',[0,0,8,8],1,True,False,None,'',True]],'w'],), ['a',[N+2,4]])
check('contract fixture 2', solve([[0,0],[0,0],[1,1],[['back','w',[0,0,4,4],1,True,False,None,'',True],['front','w',[0,0,4,4],5,True,False,None,'proxy',False]],'w'],), ['proxy',[1,1]])
check('contract fixture 3', solve([[0,0],[0,0],[1,1],[['first','w',[0,0,4,4],2,True,False,None,'',True],['second','w',[0,0,4,4],2,True,False,None,'',True]],'w'],), ['first',[1,1]])
check('contract fixture 4', solve([[0,0],[0,0],[1,1],[['a','other',[0,0,4,4],1,True,False,None,'',True],['b','w',[0,0,4,4],2,False,False,None,'',True],['c','w',[0,0,4,4],3,True,True,None,'',True]],'w'],), None)
check('contract fixture 5', solve([[0,0],[0,0],[4,1],[['a','w',[0,0,4,4],1,True,False,None,'',True]],'w'],), None)
check('contract fixture 6', solve([[0,0],[0,0],[2,2],[['a','w',[0,0,4,4],1,True,False,[0,0,2,2],'',True]],'w'],), None)
check('contract fixture 7', solve([[0,0],[0,0],[0,0],[['a','w',[0,0,4,4],1,True,False,None,'',True]],'w'],), ['a',[0,0]])
check('contract fixture 8', solve([[0,0],[0,0],[1,1],[],'w'],), None)
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 |
|---|---|---|---|
| contract fixture 1 | ['a', [1, 1]] | ['a', [3, 4]] | Failed |
| contract fixture 2 | ['proxy', [1, 1]] | ['proxy', [1, 1]] | Passed |
| contract fixture 3 | ['first', [1, 1]] | ['first', [1, 1]] | Passed |
| contract fixture 4 | None | None | Passed |
| contract fixture 5 | None | None | Passed |
| contract fixture 6 | None | None | Passed |
| contract fixture 7 | ['a', [0, 0]] | ['a', [0, 0]] | Passed |
| contract fixture 8 | None | None | Passed |
SHA-256 / 09764e1129257ff4e0581478e63f39323c07b14ddb8bb00dd0fbeba51edec6c8
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(data):
origin,scroll,point,records,window = data
x,y = point[0]-origin[0]+scroll[0],point[1]-origin[1]
hits = []
for index,(ident,owner,rect,z,exposed,transparent,clip,proxy,enabled) in enumerate(records):
if owner != window or not exposed or transparent: continue
x0,y0,x1,y1 = rect
if not (x0 <= x < x1 and y0 <= y < y1): continue
if clip is not None and not (clip[0] <= x < clip[2] and clip[1] <= y < clip[3]): continue
hits.append((z,-index,proxy if proxy else ident))
if not hits: return None
return [max(hits,key=lambda hit:(hit[0],hit[1]))[2],[x,y]]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
check('contract fixture 1', solve([[N,10],[N+1,3],[N+1,11],[['a','w',[0,0,8,8],1,True,False,None,'',True]],'w'],), ['a',[N+2,4]])
check('contract fixture 2', solve([[0,0],[0,0],[1,1],[['back','w',[0,0,4,4],1,True,False,None,'',True],['front','w',[0,0,4,4],5,True,False,None,'proxy',False]],'w'],), ['proxy',[1,1]])
check('contract fixture 3', solve([[0,0],[0,0],[1,1],[['first','w',[0,0,4,4],2,True,False,None,'',True],['second','w',[0,0,4,4],2,True,False,None,'',True]],'w'],), ['first',[1,1]])
check('contract fixture 4', solve([[0,0],[0,0],[1,1],[['a','other',[0,0,4,4],1,True,False,None,'',True],['b','w',[0,0,4,4],2,False,False,None,'',True],['c','w',[0,0,4,4],3,True,True,None,'',True]],'w'],), None)
check('contract fixture 5', solve([[0,0],[0,0],[4,1],[['a','w',[0,0,4,4],1,True,False,None,'',True]],'w'],), None)
check('contract fixture 6', solve([[0,0],[0,0],[2,2],[['a','w',[0,0,4,4],1,True,False,[0,0,2,2],'',True]],'w'],), None)
check('contract fixture 7', solve([[0,0],[0,0],[0,0],[['a','w',[0,0,4,4],1,True,False,None,'',True]],'w'],), ['a',[0,0]])
check('contract fixture 8', solve([[0,0],[0,0],[1,1],[],'w'],), None)
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 |
|---|---|---|---|
| contract fixture 1 | ['a', [3, 1]] | ['a', [3, 4]] | Failed |
| contract fixture 2 | ['proxy', [1, 1]] | ['proxy', [1, 1]] | Passed |
| contract fixture 3 | ['first', [1, 1]] | ['first', [1, 1]] | Passed |
| contract fixture 4 | None | None | Passed |
| contract fixture 5 | None | None | Passed |
| contract fixture 6 | None | None | Passed |
| contract fixture 7 | ['a', [0, 0]] | ['a', [0, 0]] | Passed |
| contract fixture 8 | None | None | Passed |
SHA-256 / 3d8892d673ca3676af6f4c61a6cd64d5f74086fcb1bb2bfcee80bca21102b8ca
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(data):
origin,scroll,point,records,window = data
x,y = point[0]-origin[0]+scroll[0],point[1]-origin[1]+scroll[1]
hits = []
for index,(ident,owner,rect,z,exposed,transparent,clip,proxy,enabled) in enumerate(records):
if owner != window or not exposed or transparent: continue
x0,y0,x1,y1 = rect
if not (x0 <= x < x1 and y0 <= y < y1): continue
if clip is not None and not (clip[0] <= x < clip[2] and clip[1] <= y < clip[3]): continue
hits.append((z,-index,proxy if proxy else ident))
if not hits: return None
return [max(hits,key=lambda hit:(hit[0],hit[1]))[2],[x,y]]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
check('contract fixture 1', solve([[N,10],[N+1,3],[N+1,11],[['a','w',[0,0,8,8],1,True,False,None,'',True]],'w'],), ['a',[N+2,4]])
check('contract fixture 2', solve([[0,0],[0,0],[1,1],[['back','w',[0,0,4,4],1,True,False,None,'',True],['front','w',[0,0,4,4],5,True,False,None,'proxy',False]],'w'],), ['proxy',[1,1]])
check('contract fixture 3', solve([[0,0],[0,0],[1,1],[['first','w',[0,0,4,4],2,True,False,None,'',True],['second','w',[0,0,4,4],2,True,False,None,'',True]],'w'],), ['first',[1,1]])
check('contract fixture 4', solve([[0,0],[0,0],[1,1],[['a','other',[0,0,4,4],1,True,False,None,'',True],['b','w',[0,0,4,4],2,False,False,None,'',True],['c','w',[0,0,4,4],3,True,True,None,'',True]],'w'],), None)
check('contract fixture 5', solve([[0,0],[0,0],[4,1],[['a','w',[0,0,4,4],1,True,False,None,'',True]],'w'],), None)
check('contract fixture 6', solve([[0,0],[0,0],[2,2],[['a','w',[0,0,4,4],1,True,False,[0,0,2,2],'',True]],'w'],), None)
check('contract fixture 7', solve([[0,0],[0,0],[0,0],[['a','w',[0,0,4,4],1,True,False,None,'',True]],'w'],), ['a',[0,0]])
check('contract fixture 8', solve([[0,0],[0,0],[1,1],[],'w'],), None)
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 |
|---|---|---|---|
| contract fixture 1 | ['a', [3, 4]] | ['a', [3, 4]] | Passed |
| contract fixture 2 | ['proxy', [1, 1]] | ['proxy', [1, 1]] | Passed |
| contract fixture 3 | ['first', [1, 1]] | ['first', [1, 1]] | Passed |
| contract fixture 4 | None | None | Passed |
| contract fixture 5 | None | None | Passed |
| contract fixture 6 | None | None | Passed |
| contract fixture 7 | ['a', [0, 0]] | ['a', [0, 0]] | Passed |
| contract fixture 8 | None | None | Passed |
SHA-256 / f0de5cfd5d853c2425745a98bc1f6541813d8fa765e0fca7e82b5dd47d52ed6a
Verification & scope
Stipulated finite adapter contract, not a claim of platform accessibility or ARIA 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:45:51.704130+00:00.
Case digest / cf8807298ec159f538f61e3d4e0fc4f8868ecf8733cfc29276399d6971193251