Seventy-nine rows across all eleven evidence areas, each carrying the stack that produced it and whether it generalizes, so later sub-phases can tell an API-contract fact from an observation about this box and these app versions. Forty-three confirm the roadmap, five contradict it, twenty-six are new edges, and five defer to a named Phase 2 sub-phase. No row is unknown. Six in-place corrections land with it. The CacheRequest multiplier is replaced by its phase decomposition, because a single number is wrong in both directions. The private-file requirements now name ReplaceFile rather than MoveFileEx for a replace over an open destination, record that share-delete is necessary but never sufficient, and permit locality inference only behind a control call, since an out-of-range info class is indistinguishable from a local volume by error code. The Chromium guidance now requires a settle before concluding a tree is thin; its exposure claim was left standing because the probe confirmed it. The event-teardown mitigation now names the hazard that actually dominates removal cost. The completeness check is a script rather than a checklist, and it is negative-tested: blanking a scope, setting a verdict to unknown, or dropping a closure point each fail it loudly. It asserts the hunk index is bijective against the measured diff rather than against a number written in prose.
60 KiB
Windows platform exploration findings (sub-phase 2.0)
The evidence ledger phases.md 2.0 requires. Every row maps one experiment to observed behavior and a doc-alignment verdict, and every row names the client stack that produced it and the scope its conclusion carries.
Read this with probes/windows/README.md (how to run the corpus, the redaction gate, the
safety envelope) and docs/phases.md Phase 2 (the contract these rows are graded against).
How to read a row
| column | meaning |
|---|---|
id |
stable row id, cited by the hunk index at the bottom |
script |
the probe that produced it, or - for session-evidence rows - the exact re-run command |
stack |
managed (.NET System.Windows.Automation), uia3-com (hand-declared CUIAutomation8 shim), or n/a (toolchain and session-evidence rows) |
scope |
api-contract = product-wide under KTD2's forward-compatibility rule. app/provider = specific to this environment and these app versions. Downstream sub-phases read this column to know what generalizes |
verdict |
CONFIRMS, CONTRADICTS, NEW-EDGE, DEFERRED. There are no UNKNOWN rows (R5) |
action |
what the finding obliges. DEFERRED rows carry closure: 2.<n> - the Phase 2 sub-phase that closes them |
KTD1 governs stack authority: where a managed row and a uia3-com row disagree, the COM
row is the product-relevant one, because the Rust adapter wraps a UIA3 COM client. Managed
rows that duplicate a COM-measured fact are labelled non-authoritative cross-checks in
their own capture.
KTD7 governs authority over documentation: a probe row may only outrank docs/phases.md
when its scope is api-contract, or when its environment dependency is explicitly
recorded in the observed cell. An environment artifact never launders itself into the
product contract.
Environment header
Every row in this ledger was measured on one machine, recorded in full at
captures/00-environment/environment.json (re-run:
powershell -NoProfile -ExecutionPolicy Bypass -File .\00-environment.ps1).
| fact | value |
|---|---|
| OS | Windows Server 2019 Datacenter, InstallationType Server, ReleaseId 1809 |
| Build | 17763, UBR 7434 (Microsoft Windows NT 10.0.17763.0) |
| Session | SESSIONNAME=Console, SessionId 1, UserInteractive true, isRemoteSession false, window station Console, single display 1639x732 |
| Integrity | mandatory label S-1-16-12288 (High); EnableLUA 1, FilterAdministratorToken unset, ConsentPromptBehaviorAdmin 5, so Admin Approval Mode is not active and -Verb RunAs yields High-vs-High |
| Identity | user SID RID 500 (built-in Administrator); TokenOwner S-1-5-32-544; new files are owned by S-1-5-32-544 via TokenOwner, not TokenUser |
| Locale | split: InstalledUICulture es-ES, CurrentCulture en-US, ANSI codepage 1252, OEM 850. Well-known accounts render as BUILTIN\Administradores. No probe matches on a localized display name |
| UIA | GAC UIAutomationClient 4.0.0.0 exports 57 types and no IUIAutomation; CUIAutomation and CUIAutomation8 are both registered against C:\Windows\System32\uiautomationcore.dll 7.2.17763.1, threading model Both |
| Toolchain | Windows PowerShell 5.1.17763.7434 (pwsh absent); csc.exe 4.8.3761.0, C# 5 ceiling; .NET Framework 4.8.03761 |
| App versions | Obsidian 1.12.7.0 bundling Chromium 142.0.7444.265 and Electron 39.8.3; classic Win32 Notepad (%WINDIR%\System32\notepad.exe), not the Store/MSIX RichEdit variant |
Because there is exactly one environment (KTD2), the scope column is doing real work: an
app/provider row states what this VM and these app versions do, and nothing more.
Area 1 - full-tree dumps
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A1-1 | 01-tree-dump.ps1 |
managed | app/provider | 2.0 area 1 requires full-tree dumps of Notepad, Explorer, Settings and one Electron app with every property read per node, naming which Notepad variant was captured | four dumps, ControlView, every node carrying a ControlType: notepad 3 nodes at 100 pct non-empty bounds, settings 49 at 95.92 pct, explorer 196 at 100 pct, obsidian 119 at 99.16 pct. Focus identity (ControlType, ClassName, ProcessId, NativeWindowHandle) asserted identical before and after all seven walks, with zero focus-interference rows. Target recorded as classic Win32 Notepad | CONFIRMS | area satisfied; the Notepad-variant clause landed as a phases.md replacement |
| A1-2 | 01-tree-dump.ps1 |
managed | app/provider | Windows Engineering Invariant 10: ElementFromHandle is headless-safe for visible and minimized windows |
minimizing classic Notepad does not change tree completeness: 3 nodes restored, 3 minimized, delta 0. It degenerates geometry in two different shapes: only the top-level Window reports Rect.Empty, while its descendants report non-empty rects with real width and height anchored at the -32000 off-screen origin. IsOffscreen is false on every node in the minimized pass, including the empty-rect Window |
NEW-EDGE | invariant 10 holds and needs no change. 2.7 must not source offscreen evidence from IsOffscreen alone, nor treat Rect.IsEmpty as the only degenerate geometry: a -32000 rect is neither empty nor offscreen by either property |
| A1-3 | 01-tree-dump.ps1 |
managed | app/provider | 2.4 resolves app and window targets; 2.0 area 1 names Settings as a dump target | Settings resolves as ApplicationFrameWindow (pid 8712, ApplicationFrameHost) containing Windows.UI.Core.CoreWindow (pid 9052, SystemSettings). The top-level window a UWP app presents does not belong to the app pid. Selection was by ClassName and ProcessId only; no localized name was compared anywhere |
NEW-EDGE | 2.4 UWP targeting must descend to the CoreWindow rather than match the top-level window's ProcessId. The locale rule is load-bearing rather than cosmetic: this box renders Settings chrome in English on an es-ES install, so a title match would have passed here and broken elsewhere |
| A1-4 | 11-electron-activation.ps1 |
uia3-com | app/provider | P2-O15: Chromium 138+ exposes a UIA tree to any UIA client with no flag | Obsidian 1.12.7.0 bundling Chromium 142.0.7444.265 and Electron 39.8.3, version read from a user-agent scan of the shipped binary after ELECTRON_RUN_AS_NODE and DLL file versions both failed. One fresh instance per stack-by-flag cell: settled RawView is 172 in all four cells, managedActivatesWithoutFlag and comActivatesWithoutFlag both true, clientStackDivergence false |
CONFIRMS | the plan's candidate CONTRADICTS is superseded: the pre-finding of 8 stable managed descendants was measured through an occluded window with an uncontrolled settle window. No client-stack divergence exists on this claim |
| A1-5 | 11-electron-activation.ps1 |
uia3-com | app/provider | the same lines prescribe --force-renderer-accessibility when a tree is still empty or minimal, with no settle requirement |
first contact is 13 RawView nodes on both stacks with no flag, against a settled 172: a 13.2x understatement, deterministic across runs. With the flag, first contact is a race that landed anywhere from a fraction of the settled count to nearly all of it, 141 COM and 164 managed. The flag is redundant for eventual exposure and not redundant for first-contact readiness; neither arm removes the need to settle | CONTRADICTS | guidance that concludes thinness from a single read prescribes the flag for trees that were merely read too early. Corrected in place at the objective, the 2.4 implementation bullet, the docs checklist and the R2 risk row |
| A1-6 | 01-tree-dump.ps1 |
managed | app/provider | none - this is a probe-methodology fact | an earlier revision that left Notepad, Settings and Explorer restored on top of Obsidian at the same rect held Obsidian at its first-contact node count through the full 8 s settle and a 16 s instrumented hold, across three consecutive runs. Minimizing the covering windows fixed it. The causal mechanism was not isolated: Chromium native-window occlusion tracking is the obvious candidate, unproven | NEW-EDGE | recorded as a probe-placement hazard, explicitly not a product-contract claim. A later sub-phase that measures an Electron tree behind another window may be measuring nothing |
| A1-7 | 03-pattern-census.ps1 |
managed | app/provider | none - first observation of the WPF read-too-early failure mode | a UIA client that reads a WPF window before its automation peer is built binds the generic HWND provider permanently: the window reports ClassName HwndWrapper[appdomain;thread;guid] with zero children, and a 30 s in-process poll including FromHandle and a forced FindAll(Descendants, TrueCondition) never recovered it. The same fixture read 8 s after launch reports ClassName=Window with 8 children immediately |
NEW-EDGE | a Windows snapshot taken immediately after launching a WPF app can return a permanently empty one-node tree that retrying in-process cannot fix - the adapter must re-resolve from a new HWND or a new client. Distinct from Electron: Electron under-reports then grows, WPF binds the wrong provider and stays wrong |
| A1-8 | 03-pattern-census.ps1 |
managed | app/provider | none - corollary of A1-7 | ClassName takes two shapes on the same WPF window depending on peer binding, and the unbound shape embeds a per-launch GUID |
NEW-EDGE | 2.5 must not use ClassName as an element-identity component on WPF |
Area 2 - pattern-availability census per ControlType
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A2-1 | 08-uia3-com.ps1 then 03-pattern-census.ps1 |
uia3-com | app/provider | 2.0 area 2 requires a pattern-availability census per ControlType covering Invoke, Toggle, Value, RangeValue, ExpandCollapse, SelectionItem, Scroll, ScrollItem, Text, Window and LegacyIAccessible | 4 targets, 141 RawView elements, 48 ControlType rows, none truncated. All 11 named patterns observed. 35 pattern-availability properties discovered at runtime; only 16 distinct patterns ever appear. Sanity anchors hold on every target: every Button carries Invoke, no Text element carries Invoke | CONFIRMS | area satisfied. 2.3's role and actionability tables are built from this matrix |
| A2-2 | 03-pattern-census.ps1 |
uia3-com | app/provider | KTD1's premise that a managed census would record false absences | LegacyIAccessible is advertised by 141 of 141 COM-walked elements and by 0 of the managed sweep's. Excluding it, per-target pattern coverage drops to 11/35, 37/46, 33/34 and 18/26, so it is not decorative |
CONFIRMS | every pattern-availability row in Phase 2 must carry stack=uia3-com. An adapter deciding actionability from a managed-derived pattern table would under-report exactly the legacy Win32 surfaces where LegacyIAccessible.DoDefaultAction is the only affordance |
| A2-3 | 03-pattern-census.ps1 |
uia3-com | api-contract | KTD1 predicts a set of UIA3-only patterns the managed stack structurally cannot name | the honest divergence is two patterns, not the six KTD1 anticipated: LegacyIAccessible at 141 COM against 0 managed, and Text2 at 1 COM against 0 managed. The other 14 observed patterns are reported by both stacks. Drag, DropTarget, Annotation, Styles and TextChild never appeared on any element, so their managed absence is untested here rather than demonstrated |
NEW-EDGE | KTD1's conclusion stands on two measured patterns. A larger divergence figure reported earlier in this sub-phase was a WPF peer-race artifact (A1-7) and was retracted |
| A2-4 | 03-pattern-census.ps1 |
uia3-com | app/provider | 2.3 maps UIA ControlType integers to the unified role enum | classic Notepad's edit surface is ControlType.Document carrying Value, Text, Text2, Scroll and LegacyIAccessible to the COM client, served by Microsoft: EDIT Proxy (unmanaged:uiautomationcore.dll). The same surface is ControlType.Pane with ClassName Edit and AutomationId 15 to the managed client. Whole-window counts: 26 COM nodes against 3 managed |
NEW-EDGE | 2.3's vocabulary map cannot key an editable text surface off ControlType alone, and a low managed node count on a Win32 target is evidence of client-stack divergence, not of a missing tree |
| A2-5 | 08-uia3-com.ps1 |
uia3-com | api-contract | none - the census had to discover property ids to run at all | pattern-availability property ids are build-specific: IsAnnotationPatternAvailable is 30118 on this build, while 30113, the value a from-memory table would carry, is a different property. 175 properties and 35 pattern-availability properties were enumerated from the live runtime |
NEW-EDGE | 2.2 and 2.3 must obtain pattern-availability property ids from the UIA runtime or the crate's generated constants, never from a hand-written table. This is the single most likely silent failure in a Rust pattern-availability check |
| A2-6 | 03-pattern-census.ps1 |
managed | app/provider | none - internal consistency of this corpus | divergence.json states in prose that the managed stack exposes 22 pattern classes, while the machine-generated field in the same file reflects 21 out of UIAutomationTypes at run time and lists exactly 21 names |
NEW-EDGE | surfaced rather than reconciled silently: the reflected 21 is the trustworthy figure, the prose 22 is hand-written. No conclusion in this ledger depends on which is right |
Area 3 - every interaction exercised raw
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A3-1 | 05-interactions.ps1 |
managed | app/provider | 2.0 area 3 requires invoke, toggle, set value, select, expand and collapse, scroll by pattern and by wheel, text get, selection, caret and insert, and focus, all exercised raw | every one exercised. On the WPF fixture all succeed: Invoke drives the status sink from status:ready to action:1; Toggle Off to On; ValuePattern round-trips ASCII, CJK, an astral-plane payload of 4 UTF-16 units, 3 codepoints and 1 surrogate pair, and a mixed payload, all exact; ExpandCollapse round-trips; SelectionItem takes selection count 0 to 1; ScrollPattern moves vertical percent 0 to 80. TextPattern reads back the ValuePattern-written string exactly across a cross-pattern read, selects a 5-unit range, and collapses a degenerate caret range at offset 4. TextPattern is read-only by contract, so insertion is done through ValuePattern.SetValue and verified through the TextPattern read |
CONFIRMS | 2.6's action set is grounded. Every negative in this capture is filed as the verdict pattern-unavailable, never as a script error |
| A3-2 | 05-interactions.ps1 |
managed | api-contract | 2.6 needs an observable that proves an action landed | TogglePattern.Toggle flips ToggleState Off to On without raising Click, so the fixture's Click-driven status sink stays at action:1 while the element state changes. The post-state was read by re-finding the element by AutomationId and re-acquiring the pattern, not off the object the call returned |
NEW-EDGE | 2.6 must verify an action by re-reading the element, not by trusting an application-level sink. A sink that stays silent is not evidence the action failed |
| A3-3 | 05-interactions.ps1 |
managed | app/provider | 2.3 and 2.6 assume ValuePattern.SetValue reaches an editable surface |
Notepad's Edit is unreachable from the managed stack in a stronger sense than not-enumerated: FindFirst(Descendants, ControlType=Document) does not resolve it, and AutomationElement.FromHandle on the Edit child HWND resolves a Pane whose TryGetCurrentPattern returns false for Text, Value and Scroll. The COM census records the same window as one Document carrying all four |
NEW-EDGE | client-stack divergence, not an absent affordance. Recorded so 2.6 does not conclude Notepad has no text surface. GetSupportedPatterns is also not a reliable negative - it is answered by whichever client-side proxy happens to be bound, which is why every row also records TryGetCurrentPattern |
| A3-4 | 05-interactions.ps1 |
managed | api-contract | headless-first: no foreground activation or focus steal unless --headed opts in |
SetFocus on an element of one probe-owned window moved the desktop foreground away from another probe-owned window: foreground process changed, hasKeyboardFocusAfter true, focused AutomationId txtValue. SetForegroundWindow was never called |
NEW-EDGE | 2.7 cannot treat SetFocus as headless. Any action that needs focus is a headed-policy action on Windows, or must be reached by a focus-independent pattern instead |
| A3-5 | 06-input-synthesis.ps1 |
managed | app/provider | 2.0 area 3 requires scroll by pattern and by wheel | the WinForms pnlScroll could not be driven by pattern from the managed client even though the COM census records Scroll on that exact provider. The SendInput wheel arm drives the same control through the same lblScrollPos sink: 3 ticks at -120 delta moved it from 0 to 140 pixels |
CONFIRMS | the pattern arm and the physical arm are deliberately measured on the same control, so 2.6 has the pattern-versus-physical comparison on one target |
Area 4 - SendInput synthesis
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A4-1 | 06-input-synthesis.ps1 |
managed | api-contract | 2.6: SendInput keyboard map and type_text with UTF-16 chunking for surrogate pairs |
the chunk boundary is forced by the API, not chosen: KEYEVENTF_UNICODE carries one UTF-16 code unit, so a surrogate pair is necessarily two SendInput events. All four payloads round-trip exactly, verified by SHA-256 through a WM_GETTEXT read independent of both the injection path and UIA: ascii 14 units, cjk 3, astral-plane 4 units with 3 codepoints and 1 pair, mixed 5 units with 4 codepoints and 1 pair, zero replacement characters |
CONFIRMS | 2.8's type_text splits on UTF-16 code units and does not need to keep a surrogate pair in one call - the target reassembles it |
| A4-2 | 06-input-synthesis.ps1 |
managed | api-contract | 2.6 mouse and keyboard commands including modifier chords | Ctrl+A then Ctrl+C sent as six discrete foreground-gated SendInput calls put exactly the typed string on the clipboard, verified by comparing the hash of the string the probe itself typed rather than by reading the observed clipboard value. All 11 modifier virtual keys read not-down afterwards | CONFIRMS | the modifier-release sweep is a real requirement and it passes; chords are viable on Windows |
| A4-3 | 06-input-synthesis.ps1 |
managed | app/provider | 2.6 mouse click, move, wheel and drag | click landed exactly on the requested point and drove a different control's sink from status:ready to action:1; wheel moved the scroll sink 0 to 140 px; drag moved a slider 0 to 90 with monotonic non-decreasing samples at 15, 30, 45, 60, 75 and 90 |
CONFIRMS | 2.6's mouse surface is viable. Coordinate caveat carried into A10-6: MOUSEEVENTF_ABSOLUTE normalizes against the primary monitor only |
| A4-4 | 06-input-synthesis.ps1 |
managed | app/provider | Windows Engineering Invariant 5: PostMessage WM_KEYDOWN is DEAD for Chromium, UWP and games |
against Chromium (Obsidian, Chrome_WidgetWin_1 plus its Chrome_RenderWidgetHostHWND child) all five posts returned success and the bounded 250-node RawView fingerprint was byte-identical before and after: 15 nodes at settle, 15 after an idle wait, 15 after the posted keys |
CONFIRMS | invariant 5 holds for Chromium and needs no change. An unchanged fingerprint is evidence of no observable effect within the bounded walk, not proof that nothing changed anywhere, and is recorded as such |
| A4-5 | 06-input-synthesis.ps1 |
managed | api-contract | the same invariant's trailing generalization that message posting is not a viable alternative | a posted WM_KEYDOWN does register on a classic Win32 Edit at equal integrity: one character inserted by the keydown alone, a second by a subsequently posted WM_CHAR. The target thread's own TranslateMessage synthesises the character and does not care that the message was posted. What the path cannot do is carry modifier state, because TranslateMessage reads the target thread's keyboard state, which the poster cannot set |
NEW-EDGE | invariant 5 stays as written - the exception does not make message posting a usable input path, because a shifted character or a chord is unreachable through it. Recorded so a later sub-phase does not rediscover the Win32 case and mistake it for a general fallback |
| A4-6 | 06-input-synthesis.ps1 |
managed | app/provider | none - measurement-method fact behind A4-4 | proving the Chromium row required an idle control pass: the first run of this probe recorded a 13-to-15 node change with nothing posted, because an Electron tree still settling reads as a registered keystroke. The controlled run polls to two agreeing fingerprints, waits 5 s with no input, and only then posts | NEW-EDGE | any Windows behavior measured against a Chromium tree needs an idle control arm, or self-motion is read as effect |
| A4-7 | 06-input-synthesis.ps1 |
managed | app/provider | KTD5: clipboard, cursor and modifier state restored, and every spawned process confirmed gone | no modifier was found down so no key was injected by the sweep; clipboard restored and verified byte-identical by in-memory hash without recording the value or its hash; cursor restored with zero offset; 5 processes spawned, 0 survivors on an independent second re-read. Zero foreground-interference rows across the whole corpus | CONFIRMS | the safety envelope is enforced, not asserted |
Area 5 - ElementFromPoint hit-testing
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A5-1 | 07-hittest.ps1 |
managed | api-contract | 2.7: hit_test three-way result via ElementFromPoint plus window corroboration |
with two probe-owned windows deliberately overlapped, a point inside both resolved to a control of the occluder, and Win32 WindowFromPoint independently agreed on the same top-level window |
CONFIRMS | ElementFromPoint is a sound basis for 2.7's occlusion gate, and Win32 corroboration is available as the second opinion the three-way contract wants |
| A5-2 | 07-hittest.ps1 |
managed | api-contract | 2.7 treats hit-testability as actionability evidence | a zero-size control is reachable by GetDlgItem, reports IsWindowVisible true, and resolves through AutomationElement.FromHandle with an empty rect and IsOffscreen false - yet it is returned by no tree walk (absent from managed ControlView and RawView at 24 nodes each, and from both COM walks of the same fixture) and hit by no point: a hit test at its own origin returns the enclosing Window |
NEW-EDGE | an actionability check that reads not-hit-testable as does-not-exist will be right about the click and wrong about the element. 2.7 must keep those two answers separate |
| A5-3 | 07-hittest.ps1 |
managed | api-contract | 2.7 needs a live signal that survives window-state change | ElementFromPoint is the one primitive here that degrades cleanly: after minimizing the target, the same coordinates resolve to whatever is genuinely on top instead of to the minimized window. Geometry does not: the top-level rect empties while the descendant keeps real dimensions at -31768,-31896, and IsOffscreen stays false on both |
CONFIRMS | 2.7 gates on the hit test. Where it must reason about geometry it has to test the -32000 anchor explicitly, since such a rect is neither empty nor offscreen by either property |
Area 6 - CacheRequest batched versus per-property timing
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A6-1 | 08-uia3-com.ps1 |
uia3-com | api-contract | With CacheRequest for batch attribute retrieval (3-5x faster) |
Explorer folder window over %WINDIR%\System32, 220 nodes, 8 properties, 5 repetitions, median of three runs: overall multiplier 2.69x, ranging 2.69 to 2.78 across runs, below the claimed floor. The single number hides a phase split in both directions - the cache-building find pass is slower than the uncached find at 180.25 ms against 117.38 ms, while the read pass goes from 371.62 ms to 1.25 ms, about 298x |
CONTRADICTS | the claim is too optimistic for a one-shot tree read and wildly too pessimistic for repeated property access over a cached tree. Replaced in place with the decomposition rather than a different single multiplier |
| A6-2 | 02-cache-timing.ps1 |
managed | app/provider | the same claim, cross-checked on the non-authoritative stack | environment dependency recorded per KTD7: these magnitudes are specific to these two windows, on this build, on the managed stack. The cross-check reproduces the direction and the phase split but not the magnitude. On classic Notepad, 3 nodes, batching is a pessimization: overall multiplier 0.5763, find phase 0.436. On Explorer, 196 nodes, overall 1.2967 with a 413.87x read phase. Notepad is served by UIAutomationClientsideProviders running inside the client process, so an uncached read costs no cross-process RPC and the CacheRequest adds pure setup |
NEW-EDGE | what generalizes, and what phases.md now states, is the mechanism rather than the numbers: an adapter that batches unconditionally will make simple Win32 windows slower |
Area 7 - AutomationId coverage and identity stability
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A7-1 | 04-automationid-census.ps1 |
managed | app/provider | P2-O8: UIA AutomationId is the Windows native_id; 2.0 area 7 requires a coverage census across Win32, WinForms, WPF and Electron |
managed ControlView coverage, all elements then interactive-only: Electron/Obsidian 1.5 pct then 0 pct of 8 interactive; WinForms with host providers 88.5 pct then 50 pct; WPF 69.2 pct then 100 pct of 11 interactive; classic Notepad 66.7 pct with no interactive elements; Explorer 64.6 pct then 97.6 pct of 41 interactive. COM RawView cross-check gives larger denominators by construction: 97.1, 73.9, 73.5 and 53.8 pct all-elements | CONFIRMS | native_id is well populated on the surfaces that matter - WPF and Explorer interactive elements - and effectively absent on Electron, which is exactly where 2.5's fallback evidence has to carry the resolution |
| A7-2 | 04-automationid-census.ps1 |
managed | app/provider | 2.5 resolves from RefEntry evidence, AutomationId-first with fingerprint fallback |
across a full process restart at an identical window origin, on all three targets: AutomationId survives 100 pct, RuntimeId survives 0 pct, path survives 100 pct with zero paths lost or gained. ControlType, ClassName and bucketed bounds also 100 pct; Name 96.2 pct on the WinForms rows | CONFIRMS | RuntimeId is unusable as cross-snapshot identity on Windows and must never enter RefEntry. AutomationId-first is the right primary key |
| A7-3 | 04-automationid-census.ps1 |
managed | app/provider | the same, under in-place content mutation rather than restart | path holds at 100 pct for pre-existing nodes on both mutation targets but is the first key to lose meaning as the tree grows. WPF after an in-process list mutation: AutomationId 84.6 pct, RuntimeId 61.5 pct. Explorer after the folder loses one file and gains two, with no window, process or input change: AutomationId 100 pct, RuntimeId 73.2 pct - and Explorer keys its list rows by row index, so of 29 unique AutomationId keys, 29 re-resolved, 0 were lost, and 5 landed on a different element | NEW-EDGE | this is the silent-wrong-target count: a ref keyed on AutomationId alone resolves successfully and acts on the wrong file. It is the direct reason 2.5's RefEntry needs role-conditional stable text identity alongside AutomationId, and why resolve_element_strict must be able to return STALE_REF for an id that still resolves |
| A7-4 | 04-automationid-census.ps1 |
managed | app/provider | none - measurement-method fact behind A7-3 | measuring A7-3 required a 20 s wait for the Explorer window to reflect the file-system change. A 4 s wait was tried first and reported a completely unchanged tree, which would have been filed as Explorer identity being perfectly stable across content mutation - the exact inverse of what the window does once it refreshes | NEW-EDGE | any Windows identity or staleness measurement against a shell window must wait out the shell refresh and state the wait it used. Re-run stability over three runs: every field reproduces except Explorer RuntimeId survival, which moved between 59/82 and 60/82 - real nondeterminism in the property, not probe noise |
Area 8 - event-handler observations
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A8-1 | 08-uia3-com.ps1 |
uia3-com | app/provider | P2-O11 watch is backed by AddAutomationEventHandler, AddFocusChangedEventHandler and AddPropertyChangedEventHandler |
7 handlers registered, 557 events over 20 repetitions. Fired: PropertyChanged:ValuePattern.Value 318, PropertyChanged:Name 180, FocusChanged 42, Window_WindowClosed 10, Window_WindowOpened 7. Registered and never fired: Invoke_Invoked, Text_TextChanged, SelectionItem_ElementSelected. Of 5 watched properties only Name and ValuePattern.Value ever produced an event; ToggleState, HasKeyboardFocus and RangeValue never did |
NEW-EDGE | watch cannot promise every event kind it can register. 2.9 must map its event vocabulary onto what providers actually raise and fall back to polling for the rest |
| A8-2 | 08-uia3-com.ps1 |
uia3-com | app/provider | none - ordering is unstated in phases.md and watch would inherit whatever it is |
property and focus ordering is stable: one distinct core signature across all 20 repetitions, a fixed 12-event prefix. Ordering including window events is unstable: 6 distinct signatures, with Window_WindowClosed arriving before Window_WindowOpened in some reps, the pair interleaved inside the focus pair in another, and the window events dropped entirely in 12 of 20 reps - hence 10 Closed against 7 Opened |
NEW-EDGE | 2.9 may order property and focus events; it must not derive window lifecycle from event order, and must tolerate a dropped window event entirely |
| A8-3 | 08-uia3-com.ps1 |
uia3-com | api-contract | R13's mitigation: the post-remove-barrier pattern is what makes handler teardown safe | the barrier is not where the cost is. Teardown under load, with 296 events backlogged and 300 arriving during the teardown window, completed in 72.49 ms. Teardown on an idle stream after window hide and show churn took 86,184 ms, against 63.75 ms for the identical loop with the window churn removed: about a 1352x increase for a 4.5 pct change in event volume. The window-churn-only arm, with callbacks doing no property reads, still cost 22,757 ms, and the full arm exceeds the sum of its two ingredients by 63 s. Handlers with nothing to do with windows are dragged from about 10 ms to about 10 s each | CONTRADICTS | the documented mitigation addresses a race the measurement shows is cheap and says nothing about the hazard that dominates. R13 corrected in place: bound and budget handler removal, and avoid holding handlers across window open and close churn. Cheap callbacks do not buy you out of it |
| A8-4 | 08-uia3-com.ps1 |
uia3-com | api-contract | Windows Engineering Invariants 2 and 3: COINIT_MULTITHREADED on every UIA worker thread, and handlers created, registered, removed and drained on the same dedicated MTA thread |
main thread MTA, dedicated worker thread MTA, zero worker errors. Callbacks arrive on 4 distinct threads, none of them the main thread, and the set is not disjoint from the registering worker - the worker thread is itself one of the delivery threads | CONFIRMS | invariants 2 and 3 hold. 2.9's handler state must be safe for concurrent callback delivery across several threads, not merely for one worker thread |
Area 9 - elevation and UIPI across an integrity boundary
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A9-1 | 09-elevation-uipi.ps1 |
managed | api-contract | 2.0 area 9 requires elevation and UIPI behavior measured across a real boundary | a real boundary had to be manufactured. Both easy routes fail on this box: -Verb RunAs yields High-vs-High because Admin Approval Mode is off, and runas.exe /trustlevel:0x20000 restricts the token but leaves the mandatory label at S-1-16-12288. The probe duplicates its primary token, lowers the label with SetTokenInformation(TokenIntegrityLevel), launches through CreateProcessAsUser, and reads the spawned token's label back, refusing to continue unless it is exactly S-1-16-8192. A High control arm runs the identical worker |
CONFIRMS | a UIPI observation taken without a verified boundary is an environment artifact. The label read-back is not optional, and the High control arm is what makes the Medium result mean anything |
| A9-2 | 09-elevation-uipi.ps1 |
managed | api-contract | Invariant 4, that SendInput is UIPI-blocked for elevated targets, and Invariant 6, UIPI elevation detection |
against the same High notepad.exe target: reads cross, writes do not. From Medium, UIA FromHandle returns name, ClassName, ControlType, owning pid, node count and bounds byte-identical to the High arm, and SendMessage(WM_GETTEXT) succeeds. SendInput did not land - the target's text was unchanged, observed by the High parent re-reading the Edit child, while the High arm's marker did land. PostMessage(WM_CHAR) from Medium returned false with ERROR_ACCESS_DENIED (5) |
CONFIRMS | invariants 4 and 6 hold. 2.6 maps the Medium-into-High input case onto PERM_DENIED with platform_detail; observation stays available across the boundary and must not be refused |
| A9-3 | 09-elevation-uipi.ps1 |
managed | api-contract | none - this is how the invariant has to be detected, not whether it is true | SendInput reported 6 events accepted with lastError 0 in both arms. Only re-reading the target separates them. The High arm's PostMessage even returned true with a stale lastError of 203 |
NEW-EDGE | 2.6 must never treat a SendInput return value as evidence that input landed. UIPI detection has to be a token-integrity comparison up front, or a post-condition re-read, or both |
Area 10 - session and DPI / multi-monitor bounds
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A10-1 | 10-session-dpi.ps1 |
managed | app/provider | Invariant 12, no cross-session driving, and R12, UIA requires an interactive session | sessionId 1 equals activeConsoleSessionId 1, isRemoteSession false, window station Console, UserInteractive true. The whole corpus therefore ran in a genuinely interactive console session, which is the precondition every input and hit-test row depends on |
CONFIRMS | the corpus's interactivity precondition is measured, not assumed |
| A10-2 | 00-environment.ps1 |
n/a | app/provider | R12: an RDP disconnect can drop the console session to a non-interactive state, with tscon as the documented workaround |
measured environmental fact: this VM runs on the physical console with one interactive session. An RDP session transition cannot be produced here without disconnecting the very session the corpus runs in, so no honest observation is available | DEFERRED | closure: 2.1 - registering the Windows CI runner creates the second, non-console session environment where this facet is measured. Until then no Windows adapter behavior may assume console-session semantics, and phases.md must not claim RDP or remote-session support |
| A10-3 | 10-session-dpi.ps1 |
managed | app/provider | 2.4 owns list_displays and per-monitor scale_factor; 2.0 area 10 requires DPI and multi-monitor bounds behavior |
this display reports no EDID, registry key prefix NOEDID_15AD_0405, and offers exactly one scale step: minimum, current and maximum relative scale are all 0, with zero steps above recommended. Requesting 125 pct makes DisplayConfigSetDeviceInfo return success and persist the value to the registry while the monitor's effective DPI stays 96 in both arms. The aware-versus-unaware bounds delta is therefore a measured numeric zero on all four rectangle fields, with the split genuinely in force: PROCESS_PER_MONITOR_DPI_AWARE against PROCESS_DPI_UNAWARE forced by __COMPAT_LAYER |
DEFERRED | closure: 2.4 - the sub-phase that owns list_displays and per-monitor scale_factor, on a runner with a scalable display. Carried forward regardless of closure: a successful return from DisplayConfigSetDeviceInfo is not evidence the scale applied, so 2.4 must verify against effective DPI |
| A10-4 | 10-session-dpi.ps1 |
managed | api-contract | Invariant 1: SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) at startup |
the V2 call succeeds on build 17763 from a process that starts PROCESS_DPI_UNAWARE. The read-back reports the V1 string PROCESS_PER_MONITOR_DPI_AWARE, because GetProcessDpiAwareness has no V2 enumerant |
CONFIRMS | invariant 1 is viable on the stated 1809 floor. 2.1's bootstrap must not assert V2 by reading awareness back - the read-back cannot express it |
| A10-5 | 00-environment.ps1 |
n/a | app/provider | 2.10: Windows.Graphics.Capture per-window screenshot requires Windows 10 1903+ |
measured environmental fact: this VM is build 17763, that is 1809, below the 1903 floor, so no Windows.Graphics.Capture behavior can be observed here at all |
DEFERRED | closure: 2.10 - on windows-latest, the sub-phase that owns modern capture |
| A10-6 | 06-input-synthesis.ps1 |
managed | app/provider | 2.6 mouse commands and 2.4 multi-monitor bounds | measured environmental fact: one display. MOUSEEVENTF_ABSOLUTE normalizes against GetSystemMetrics(SM_CXSCREEN/SM_CYSCREEN), the primary monitor only, so the correctness of a multi-monitor adapter using MOUSEEVENTF_VIRTUALDESK instead cannot be observed here |
DEFERRED | closure: 2.4 - measured on a multi-monitor runner alongside list_displays. 2.6 writes MOUSEEVENTF_VIRTUALDESK from the start rather than discovering the single-display assumption later |
| A10-7 | 01-tree-dump.ps1 |
managed | app/provider | 2.12 ships the fixture app, and 2.0's tree targets assume a modern app population | measured environmental fact: Server 2019 carries no WinUI3 or MSIX app population. The closest available modern target is the XAML Settings app hosted by ApplicationFrameHost (A1-3), which is a UWP CoreWindow shape rather than WinUI3, and Windows 11's Explorer and Settings are WinUI reimplementations this box cannot present |
DEFERRED | closure: 2.12 - the sub-phase that ships the fixture app. Every tree-shape row in this ledger is app/provider by construction for exactly this reason |
Area 11 - private-file I/O primitives
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| A11-1 | 12-private-file-io.ps1 |
n/a | api-contract | 2.1 requirement: FILE_SHARE_DELETE on every concurrently-open handle across an atomic replace |
42 of 42 combinations definite - 10 succeeded, 22 ERROR_SHARING_VIOLATION (32), 10 ERROR_ACCESS_DENIED (5), and SuccessWithoutShareDelete empty. Share-delete is necessary but not sufficient, and the two APIs have opposite tolerances on opposite sides. MoveFileEx over an open target fails with error 5 in all 10 cases including both share-delete modes, because it issues ReplaceIfExists rather than a POSIX-semantics rename; over an open source it succeeds with share-delete. ReplaceFile is the inverse: it succeeds over an open target with share-delete, and fails with error 32 over an open source in all 10 cases |
CONTRADICTS | ReplaceFile, not MoveFileEx, is the operation that atomically replaces a file a validation handle holds open. Corrected in place in 2.1's requirements. Note for the implementer: code that treats error 32 as the sharing case misclassifies the most likely production failure, which returns 5 |
| A11-2 | 12-private-file-io.ps1 |
n/a | app/provider | 2.1 requirement: owner validation against TokenOwner, not TokenUser |
new files are owned by S-1-5-32-544 at both High, S-1-16-12288, and Medium, S-1-16-8192, with the same TokenUser at RID 500 and the same TokenOwner in both. OwnerMatchesTokenUser false, OwnerMatchesTokenOwner true, identical SDDL. Integrity is not the variable - group membership is |
CONFIRMS | environment dependency recorded: this VM has a single built-in Administrator account and a non-admin CI account could not be exercised, so the row confirms the existing requirement rather than extending it. An owner check comparing against TokenUser fails at every integrity level |
| A11-3 | 12-private-file-io.ps1 |
n/a | api-contract | 2.1 requirement: no locality inference from FileRemoteProtocolInfo |
the API does distinguish: 0 of 6 local targets and 3 of 3 remote targets return data. It signals local by failing with ERROR_INVALID_PARAMETER (87) rather than by returning a local protocol value; remote returns full SMB 3.1.1 data, Protocol 0x00020000, WNNC_NET_SMB, flags LOOPBACK plus MUTUAL_AUTH. The real hazard is ambiguity, not unreliability: an out-of-range info class, tested with 55, returns that same 87 on all 9 targets, while the control class 0 succeeds on all 9. The correct class constant is FileRemoteProtocolInfo = 13 |
CONTRADICTS | the inference is sound once disambiguated. Corrected in place: 2.1 may read 87 as a locality signal only behind a control call on a known-good class, so a wrong class constant cannot masquerade as a local volume |
| A11-4 | 12-private-file-io.ps1 |
n/a | app/provider | 2.1 requirement: an ancestor-versus-leaf validation contract decided deliberately against the unix leaf-only rule | neither leaf carries an explicit ACE: the plain leaf has 0 explicit and 3 inherited ACEs, the protected leaf 0 explicit and 1 inherited, and both report all-ACEs-inherited. The restriction is authored at the nearest protected ancestor - the PAI protected parent for one, the user profile directory for the other. Walking the full chain, the only permissive ancestor is the volume root granting write to BUILTIN\Usuarios; every intermediate directory and both leaves have zero untrusted write grants |
NEW-EDGE | a leaf-only check is structurally blind here, because on Windows a leaf's ACL is a projection of an ancestor decision. 2.1 must decide explicitly: walk to the nearest InheritanceProtected ancestor and validate there, or diverge from unix leaf-only parity on the record. The capture supplies the evidence; the decision is 2.1's and is not pre-empted here |
| A11-5 | ./target/release/agent-desktop.exe status |
n/a | api-contract | R13's framing that FileRemoteProtocolInfo failed closed on plain local NTFS, which is what broke status |
that breakage is historical. The Win32 private-file layer was deleted from core in PR #106 and status now returns ok:true at exit 0, see R6-4; a repo-wide grep for the message string returns nothing |
NEW-EDGE | A11-3 measures the OS primitive because 2.1 rebuilds from scratch and needs the measurement, not because anything is broken today. No row in this ledger claims status is currently broken |
Session evidence (R6)
Rows already executed this session rather than produced by a probe script, carried as
first-class ledger rows with their re-run commands. All were measured on
feat/windows-2.0-probes at d7e14e3 unless stated. The claims-audit rows, C-1 through
C-14, were verified on 2026-07-25 against external sources rather than against this VM,
which is why several carry app/provider scope: they are facts about a specific OS or app
version, not about this machine.
The plan's recorded Windows baseline is stale in four places because it predates 8ad68b8
(refactor!: remove speculative Win32 private-file layer from core, PR #106), an ancestor
of HEAD. The 225 test failures it cites were that deleted code.
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|---|---|---|---|---|---|---|---|
| R6-1 | cargo test --locked -p agent-desktop-core -p agent-desktop-windows --lib |
n/a | api-contract | plan-recorded baseline: 940 tests, 715 passed, 225 failed in four clusters | 933 total, 933 passed, 0 failed, that is 932 core in 24.55 s plus 1 windows. All four recorded clusters absent, and the skills path-separator failure is gone. HOME isolation is not load-bearing: identical result on the operator's default profile, and ~/.agent-desktop was never created either way |
CONFIRMS | the recorded baseline is corrected. Coverage caveat: --lib over core plus windows only, which excludes the agent-desktop binary crate, agent-desktop-ffi and all integration targets |
| R6-2 | cargo test --locked -p agent-desktop --no-fail-fast |
n/a | api-contract | binary-crate tests pass on Windows | 128 total, 125 passed, 3 failed, all three from one root cause: src/tests/snapshot_test.rs:9 builds the binary path without std::env::consts::EXE_SUFFIX, so it looks for target\debug\agent-desktop while the real file is target\debug\agent-desktop.exe. Failing: version_command_outputs_json, snapshot_invalid_root_ref_format_returns_invalid_args, list_apps_on_non_macos_errors_gracefully |
NEW-EDGE | recorded, not fixed - Rust fixes are 2.1's opening scope by session-settled decision. 2.1's lane extension to binary-crate tests must fix this first or the lane lands red |
| R6-3 | cargo test --lib --workspace and cargo build --release |
n/a | api-contract | CLAUDE.md documents both as the standard build and test commands |
both fail hard off macOS: workspace default-members includes crates/macos, so --workspace pulls the macOS crate in on any OS - 186 errors for the test invocation, 112 for the bare release build, starting at an unresolved accessibility_sys crate. CI never hits this because every lane uses explicit -p selection |
NEW-EDGE | recorded, not fixed (2.1). This is why phases.md now says workspace invocations scope to the packages that build on that OS, in both the 2.1 exit criteria and the runner-enforcement paragraph |
| R6-4 | ./target/release/agent-desktop.exe status |
n/a | api-contract | plan-recorded: INTERNAL, cannot verify that the Windows storage is local |
ok:true at exit 0, platform windows, version 0.6.0. The message string died with the Win32 private-file layer in PR #106 |
CONFIRMS | the recorded baseline is corrected; see A11-5 for the consequence for the private-file rows |
| R6-5 | ./target/release/agent-desktop.exe snapshot --app Explorer |
n/a | api-contract | plan-recorded: PERM_DENIED |
PLATFORM_NOT_SUPPORTED at exit 1, the honest fail-closed stub, matching adapter::tests::snapshot_surfaces_fail_closed_until_windows_implements_them. Same envelope for snapshot -i and for --app explorer.exe; list-apps is also PLATFORM_NOT_SUPPORTED at exit 1; snapshot --surface screen is INVALID_ARGS at exit 2 |
CONFIRMS | the recorded baseline is corrected. The stub behaves as designed, which is the correct starting state for 2.2 |
| R6-6 | cargo build --locked --release -p agent-desktop then read the file length |
n/a | api-contract | binary size under the 15 MB cap | 1,920,512 B against a plan-recorded 1,933,824 B, a drop of 13,312 B, which is 12.2 pct of the 15,728,640 B cap | NEW-EDGE | minor drift, plus a gate-coverage gap worth carrying: the CI size gate uses BSD stat -f%z and runs only on the macOS lane, so no Windows lane enforces the cap today. 2.1's lane extension adds a Windows-native size check |
| R6-7 | cargo tree -p agent-desktop-core |
n/a | api-contract | core contains zero platform crate names | 8 dependencies - base64, libc, rustc-hash, serde, serde_json, smallvec, thiserror, tracing - and a grep for the three platform crate names returns 0 | CONFIRMS | the dependency-inversion invariant holds on Windows |
| R6-8 | cargo check -p agent-desktop-core --all-targets --target x86_64-pc-windows-msvc |
n/a | api-contract | core cross-compiles to every supported target | the msvc target succeeds in 19.28 s. The x86_64-unknown-linux-gnu check fails at a missing std crate because that target is not installed on this host; the failure is in once_cell and serde_core std resolution and never reaches agent-desktop-core |
CONFIRMS | Windows leg confirmed. The Linux leg is a host-provisioning gap on this VM, not a code defect, and is covered by the existing test-linux lane on CI |
| R6-9 | rustc -V, cargo -V, cargo clippy -V, cargo fmt -- -V |
n/a | api-contract | prior session recorded that the rust-toolchain.toml minimal profile lacks clippy and rustfmt |
half right with the wrong cause. rustc and cargo 1.89.0, clippy 0.1.89, rustfmt 1.8.0-stable are all present. clippy was already there; rustfmt was genuinely missing and rustup auto-installed it on first demand, into the rustup toolchain directory rather than the repo. The file declares both explicitly under components |
CONFIRMS | the recorded note is corrected. No manual toolchain step is needed before 2.1 |
| R6-10 | git status --porcelain and git diff --stat HEAD |
n/a | api-contract | this PR adds no Rust and no workflows | porcelain shows only the untracked probe corpus, and git diff --stat HEAD is empty. No Rust source, no CI workflow and no .gitignore change was made by any unit of this sub-phase |
CONFIRMS | the R9 stop condition held for the whole sub-phase |
| R6-11 | crates.io API, 2026-07-25 | n/a | api-contract | pins recorded at 2026-04 research time: uiautomation 0.24+, windows 0.62.2, windows-capture 1.5.4 |
windows 0.62.2 and windows-sys 0.61.2 current; uiautomation 0.25.0 dated 2026-05-05; windows-capture 2.0.0 dated 2026-04-14. windows-capture 1.5.4 was never published - the release line is 1.5.0 then 2.0.0. Both uiautomation 0.25.0 and windows-capture 2.0.0 pin windows ^0.62.2. The sole RUSTSEC hit is pre-0.32 windows, which does not apply |
CONFIRMS | pins updated in phases.md in six places: pin table, inline Cargo.toml snippet, key-API rows, new-dependency table and the re-verification note. uiautomation needs an explicit bump because a 0.24 requirement will not auto-resolve to 0.25.x under 0.x semver, and 2.10 must diff-audit windows-capture 1.5.0 to 2.0.0 before first use |
| C-1 | crates.io release history for windows-capture |
n/a | api-contract | the pin table recorded windows-capture 1.5.4 |
that version was never published; the line is 1.5.0 then 2.0.0 on 2026-04-14, a major bump with no upstream changelog | CONFIRMS | pin corrected wherever it appears |
| C-2 | crates.io release history for uiautomation |
n/a | api-contract | uiautomation 0.24+ |
current stable is 0.25.0, dated 2026-05-05 | CONFIRMS | pin corrected; the plus was misleading under 0.x semver and the bump is now explicit |
| C-3 | crates.io metadata for windows |
n/a | api-contract | windows 0.62.2 pinned to match windows-capture 1.5.x's own pin |
0.62.2 is still current as of 2025-10-06 and both downstream crates pin ^0.62.2, so the pin is right but the stated reason named a release that never existed |
CONFIRMS | rationale corrected in place |
| C-4 | developer.chrome.com, 2025-08-14 | n/a | app/provider | Electron and Chrome expose no a11y tree by default, with --force-renderer-accessibility as the primary lever |
Chrome 138 shipped native UIA on by default in August 2025, so the flag is not the primary lever on modern builds | CONFIRMS | P2-O15, the 2.4 detection bullet, the docs checklist and the R2 risk row were all rewritten around this. A1-4 later confirmed it by observation on Chromium 142, and A1-5 corrected the thinness guidance that survived the rewrite |
| C-5 | NVDA issue 14539 | n/a | app/provider | tray overflow items live in NotifyIconOverflowWindow |
Windows 11 22H2, build 22623+, renamed the overflow flyout to TopLevelWindowForOverflowXamlIsland; NotifyIconOverflowWindow exists only before that |
CONFIRMS | corrected in the capability map, the tray command table and the 2.14 list-items bullet. Not probeable here - shell surfaces are out of 2.0 scope and land at 2.14 |
| C-6 | github.blog, 2025-08-07 | n/a | api-contract | the ARM64 npm branch was deferred until an ARM64 runner exists | hosted windows-11-arm runners have been GA for public repos since 2025-08-07 |
CONFIRMS | the deferral was stale and is removed; both win32-x64 and win32-arm64 branches are in scope |
| C-7 | Microsoft lifecycle policy | n/a | api-contract | Windows 10 1809+ with no servicing qualifier | mainstream Windows 10 reached end of support on 2025-10-14, with consumer ESU to 2027-10-12; the serviced 1809-vintage targets are Windows 10 Enterprise LTSC 2019 and Windows Server 2019, both to 2029-01-09 | CONFIRMS | the API floor is unchanged; the practical release targets are now stated as Windows 11 and Server 2019+ |
| C-8 | Windows.Graphics.Capture API documentation |
n/a | api-contract | Windows 10 1903+ for Windows.Graphics.Capture |
1903 is the base floor, but the cursor-capture toggle requires 19041+ and border removal through IsBorderRequired requires 20348+ |
CONFIRMS | version floors stated per feature, so 2.10 does not discover them at implementation time. Unmeasurable here, see A10-5 |
| C-9 | devblogs.microsoft.com on Notepad architecture | n/a | app/provider | 2.0 area 1 named Notepad as one target | Notepad is two different apps: Server SKUs ship the classic Win32 Edit-control Notepad, Windows 11 clients ship the Store and MSIX RichEdit Notepad | CONFIRMS | 2.0's scope now requires tree expectations to name the variant. A1-1 and A2-4 are recorded against the classic Win32 variant |
| C-10 | Windows 11 shell documentation | n/a | app/provider | ActionCenter as a single Windows shell surface |
Windows 11 split Windows 10's Action Center into Notification Center on Win+N and Quick Settings on Win+A, so on Windows 11 the ActionCenter kind maps to the Win+N Notification Center |
CONFIRMS | P2-O14's surface vocabulary now states the mapping. Confirmed by 2.14's shell probes, not by 2.0 |
| C-11 | GitHub Actions runner images, June 2026 | n/a | api-contract | windows-latest as an unqualified label |
windows-latest now resolves to Server 2025 with VS2026 |
CONFIRMS | recorded so 2.1's lane extension and 2.10's windows-latest capture work are planned against the right image |
| C-12 | cargo test --lib --workspace on Windows |
n/a | api-contract | 2.1 exit criteria: workspace green on Windows CI | agent-desktop-macos does not compile on Windows, so an unqualified workspace invocation cannot be green anywhere but macOS |
CONFIRMS | exit criteria and the runner-enforcement paragraph now scope explicitly to core, windows, the binary crate and ffi. Independently reproduced as R6-3 |
| C-13 | cargo test -p agent-desktop --lib |
n/a | api-contract | lane definitions that use --lib to mean all tests |
the agent-desktop binary crate has no lib target, so --lib silently skips it entirely |
CONFIRMS | 2.1's lane extension names cargo test -p agent-desktop explicitly. R6-2 shows what that lane will surface on first run |
| C-14 | git log and .github/workflows/ci.yml |
n/a | api-contract | the release table's newest row was an unreleased Phase 1.6 entry, and the CI table described platform-check as proving the stub crates compile |
v0.6.0 shipped on 2026-07-25: core's speculative Win32 private-file layer removed, six files, windows-sys dropped from core and the workspace, and real test-windows and test-linux lanes added that execute core's platform-conditional code on every PR |
CONFIRMS | release table, CI job table, CI evolution table and the Phase 2 status lines all restated. This is the change that makes the plan's recorded baseline stale, and therefore the root of R6-1, R6-4 and R6-5 |
phases.md hunk index
R7 requires the map to be bijective in both directions: every hunk maps to at least one
backing row, and every CONTRADICTS row maps to at least one hunk.
The authoritative count is measured, not written. git diff -U0 main -- docs/phases.md
reports 37 hunks after this sub-phase's corrections. It reported 36 before U9's
replacements; of U9's six in-place corrections, three merged into existing adjacent hunks
under -U0 (the private-file requirements bullet into H13, the CacheRequest row into
H17, and the Chromium settle clauses into H08, H16 and H36, which already touched those
exact lines), and one opened the new H37. 13-ledger-check.ps1 re-measures the count on
every run and fails if the index and the live diff disagree.
The five CONTRADICTS rows are A1-5, A6-1, A8-3, A11-1 and A11-3; each appears below.
| hunk | phases.md location | backing rows |
|---|---|---|
| H01 | @@ -13 +13,2 @@ release table, v0.6.0 and v0.5.0 rows |
C-14, R6-1, R6-4 |
| H02 | @@ -41 +42 @@ Phase 2 status line |
C-14 |
| H03 | @@ -53 +54 @@ phase summary table row |
C-14 |
| H04 | @@ -579 +580,3 @@ CI job table, platform-check plus the new test-windows and test-linux lanes |
C-14, R6-1, R6-6 |
| H05 | @@ -795,2 +798,2 @@ the integration branch is the base for everything that platform does |
R6-10 |
| H06 | @@ -801 +804,2 @@ no-convenience-deferral rule and the promotion gate |
A10-2, A10-3, A10-5, A10-6, A10-7 |
| H07 | @@ -810 +814 @@ Phase 2 section status |
C-14 |
| H08 | @@ -869,2 +873,2 @@ P2-O14 Action Center mapping, and P2-O15 Chromium exposure plus the settle requirement |
C-10, C-4, A1-5 |
| H09 | @@ -873 +877 @@ P2-O18 ships inside Phase 2 |
A10-2 |
| H10 | @@ -928 +932 @@ capability map, tray overflow window class |
C-5 |
| H11 | @@ -955 +959 @@ 2.0 scope names the Notepad variant |
C-9, A1-1 |
| H12 | @@ -970 +974 @@ 2.1 extends the existing test-windows lane |
C-13, R6-2, R6-6 |
| H13 | @@ -974,2 +978,2 @@ 2.1 records the re-verified pins, and the 2.1 private-file requirements |
R6-11, A11-1, A11-3 |
| H14 | @@ -981 +985 @@ 2.1 exit criteria scope to the packages that build on Windows |
C-12, R6-3 |
| H15 | @@ -996 +1000 @@ 2.2 key APIs, uiautomation 0.25+ |
R6-11, C-2 |
| H16 | @@ -1032 +1036 @@ 2.4 Chromium detection requires a settle before judging thinness |
A1-5, C-4 |
| H17 | @@ -1102,2 +1106,2 @@ API mapping table, tree-root pin and the CacheRequest phase split |
R6-11, A6-1, A6-2 |
| H18 | @@ -1227 +1231 @@ npm postinstall gains win32-arm64 |
C-6 |
| H19 | @@ -1240 +1244 @@ 2.14 title loses the stretch qualifier |
A10-2 |
| H20 | @@ -1244 +1248 @@ 2.14 ships before the 2.15 merge |
A10-2 |
| H21 | @@ -1261 +1265 @@ tray command table, overflow flyout class |
C-5 |
| H22 | @@ -1280 +1284 @@ 2.14 list-items bullet, overflow flyout class |
C-5 |
| H23 | @@ -1298 +1302 @@ 2.15 depends on all of 2.0 through 2.14 |
A10-2 |
| H24 | @@ -1306,2 +1310,2 @@ OS floors, Windows 10 servicing reality and the WGC feature floors |
C-7, C-8, A10-5 |
| H25 | @@ -1316,3 +1320,3 @@ new-dependency pin table |
R6-11, C-1, C-2, C-3 |
| H26 | @@ -1322 +1326 @@ pin re-verification note |
R6-11 |
| H27 | @@ -1335 +1339 @@ Cargo.toml snippet, uiautomation = "0.25" |
R6-11, C-2 |
| H28 | @@ -1337 +1341 @@ Cargo.toml snippet, windows-capture = "2.0.0" |
R6-11, C-1 |
| H29 | @@ -1389 +1393 @@ docs checklist, Windows permissions and Chromium 138 |
C-4, A1-5 |
| H30 | @@ -1716 +1720 @@ 3.14 title loses the stretch qualifier |
A10-2 |
| H31 | @@ -1720 +1724 @@ 3.14 ships before the 3.15 merge |
A10-2 |
| H32 | @@ -1768 +1772 @@ 3.15 depends on all of 3.0 through 3.14 |
A10-2 |
| H33 | @@ -2476,3 +2480,4 @@ CI evolution table, v0.6.0 row and the Phase 2 and 3 rows |
C-14, R6-1, R6-6 |
| H34 | @@ -2482 +2487 @@ runner enforcement scopes to the packages that build on each OS |
C-12, C-13, R6-3 |
| H35 | @@ -2493,3 +2498,3 @@ new-dependencies summary table |
R6-11, C-1, C-2, C-3 |
| H36 | @@ -2536 +2541 @@ R2 risk row, settle before judging a Chromium tree thin |
A1-5, C-4 |
| H37 | @@ -2547 +2552 @@ R13 risk row, handler removal cost is window churn rather than the barrier |
A8-3 |
Completeness self-check
13-ledger-check.ps1 parses this file and fails loud with a nonzero exit unless: all
eleven 2.0 evidence areas carry at least one row; no verdict is UNKNOWN or otherwise
outside the four-value vocabulary; every DEFERRED row names a Phase 2 closure sub-phase
as closure: 2.<n>; every row carries a non-empty stack and scope drawn from the
allowed sets; every row id is unique; and the hunk index is bijective in both directions
against the live git diff -U0 main -- docs/phases.md. It runs as part of run-all.ps1
and writes captures/13-ledger-check/ledger-check.json.
powershell -NoProfile -ExecutionPolicy Bypass -File .\13-ledger-check.ps1