Summary
The Windows shell has two independent descriptions of the UI Automation tree, and they have silently diverged:
graphcode-windows/src/Accessibility.zig - defaultContract(), instantiated at runtime in App.init and wired to WM_GETOBJECT.
graphcode-windows/src/AccessibilityProvider.cpp - the hand-rolled native fixed element table that the live UIA tree is actually traversed from.
Several ids declared in the Zig contract have no counterpart in the C++ table, so they cannot be found by a live UIA client no matter what the Zig-side unit tests assert.
Evidence
This was first discovered empirically during PR #408. A live-gate assertion for move-project-unavailable failed in CI (uia-live-gate.ps1:283, job 106845146915) with "project Move action was not exposed as a disabled UIA menu item" - because that id exists only in the Zig contract. #408 was fixed by replacing the assertion with a closed-set omission proof over the actions menu's real children, and that fix is on main.
However, the underlying divergence was never addressed, and more affected ids landed afterwards via PR #409:
Declared in Accessibility.zig but absent from AccessibilityProvider.cpp:
workspaces (Accessibility.zig:295)
workspace-new (:304)
workspace-rename (:311)
workspace-delete (:318)
Verified on main (4e88dfa): each of these appears only in Accessibility.zig and its own tests. The sole other match for workspace-new is workspace-new-tab-, an unrelated dynamic prefix emitted by AccessibilityProvider.cpp:812. No live-gate assertion references any of the four.
Why this went undetected
Accessibility.zig's tests assert against the same in-memory model that produced the elements, and by array index:
try std.testing.expectEqualStrings("workspaces", provider.elements.items[23].id);
This is self-referential - it can never detect that the live tree lacks the element. Index-based assertions have already caused one real bug in this file: an assertion on index 18 silently validated terminal-a instead of the intended move-project-unavailable at index 22, because both expose .text.
Impact
Not a functional regression - the workspace lifecycle feature works, and its non-UIA behavior is covered. The gap is that its accessibility exposure is unproven: assistive technology would not find these elements even though the contract claims them. This is a correctness/a11y gap, not a crash.
The UI Automation tree row in investigation/ui-parity-matrix.md is already Partial, so the ledger is not overclaiming at row level.
Confidence
High on the divergence itself (the move-project-unavailable CI failure is direct empirical proof of the mechanism, and the four ids are verifiably absent from the C++ table). I have not executed the live gate against these four specific ids, so the claim that they are unreachable live is inferred from the same mechanism rather than separately observed.
Suggested fix
- Add the missing elements to the
AccessibilityProvider.cpp fixed table so the contract is honest, or remove them from defaultContract() if they are not meant to be live.
- Add a check that fails when an id in
defaultContract() has no counterpart in the native table - the absence of this check is the root cause.
- Convert the index-based assertions in
Accessibility.zig to assert on id, so element insertions cannot silently re-point an assertion at a neighbouring element.
Context
Found while closing out the Windows parity fleet (#408-#412, #414, #415, #416). Filed rather than fixed inline because it spans two workstreams' merged code and warrants its own review.
Summary
The Windows shell has two independent descriptions of the UI Automation tree, and they have silently diverged:
graphcode-windows/src/Accessibility.zig-defaultContract(), instantiated at runtime inApp.initand wired toWM_GETOBJECT.graphcode-windows/src/AccessibilityProvider.cpp- the hand-rolled native fixed element table that the live UIA tree is actually traversed from.Several ids declared in the Zig contract have no counterpart in the C++ table, so they cannot be found by a live UIA client no matter what the Zig-side unit tests assert.
Evidence
This was first discovered empirically during PR #408. A live-gate assertion for
move-project-unavailablefailed in CI (uia-live-gate.ps1:283, job 106845146915) with "project Move action was not exposed as a disabled UIA menu item" - because that id exists only in the Zig contract. #408 was fixed by replacing the assertion with a closed-set omission proof over theactionsmenu's real children, and that fix is onmain.However, the underlying divergence was never addressed, and more affected ids landed afterwards via PR #409:
Declared in
Accessibility.zigbut absent fromAccessibilityProvider.cpp:workspaces(Accessibility.zig:295)workspace-new(:304)workspace-rename(:311)workspace-delete(:318)Verified on
main(4e88dfa): each of these appears only inAccessibility.zigand its own tests. The sole other match forworkspace-newisworkspace-new-tab-, an unrelated dynamic prefix emitted byAccessibilityProvider.cpp:812. No live-gate assertion references any of the four.Why this went undetected
Accessibility.zig's tests assert against the same in-memory model that produced the elements, and by array index:This is self-referential - it can never detect that the live tree lacks the element. Index-based assertions have already caused one real bug in this file: an assertion on index 18 silently validated
terminal-ainstead of the intendedmove-project-unavailableat index 22, because both expose.text.Impact
Not a functional regression - the workspace lifecycle feature works, and its non-UIA behavior is covered. The gap is that its accessibility exposure is unproven: assistive technology would not find these elements even though the contract claims them. This is a correctness/a11y gap, not a crash.
The
UI Automation treerow ininvestigation/ui-parity-matrix.mdis alreadyPartial, so the ledger is not overclaiming at row level.Confidence
High on the divergence itself (the
move-project-unavailableCI failure is direct empirical proof of the mechanism, and the four ids are verifiably absent from the C++ table). I have not executed the live gate against these four specific ids, so the claim that they are unreachable live is inferred from the same mechanism rather than separately observed.Suggested fix
AccessibilityProvider.cppfixed table so the contract is honest, or remove them fromdefaultContract()if they are not meant to be live.defaultContract()has no counterpart in the native table - the absence of this check is the root cause.Accessibility.zigto assert onid, so element insertions cannot silently re-point an assertion at a neighbouring element.Context
Found while closing out the Windows parity fleet (#408-#412, #414, #415, #416). Filed rather than fixed inline because it spans two workstreams' merged code and warrants its own review.