diff --git a/probes/windows/03-pattern-census.ps1 b/probes/windows/03-pattern-census.ps1 new file mode 100644 index 0000000..f9090a9 --- /dev/null +++ b/probes/windows/03-pattern-census.ps1 @@ -0,0 +1,422 @@ +<# +.SYNOPSIS + Probe 03 (sub-phase 2.0, unit U4): per-ControlType pattern-availability census on + the UIA3 COM stack, with the managed cross-check that justifies KTD1. + +.DESCRIPTION + The authoritative census is NOT re-measured here. U7's csc-built UIA3 COM shim + already walked WinForms (both fixture modes), WPF and classic Notepad in census + mode and committed the result; this probe consumes + captures/08-uia3-com/census.json and reshapes it into the per-ControlType matrix + the ledger cites, so the corpus holds exactly one COM pattern measurement. + + What is new here is the managed side. U7's comparison.json recorded managed node + counts and "how many elements advertise any pattern"; it did not record WHICH + pattern names the managed stack can name. This probe runs a fresh + System.Windows.Automation sweep over the same four fixture shapes, enumerates + pattern short names per element, and diffs the two vocabularies. The result is + the divergence row: LegacyIAccessible is live on every element the COM stack + sees and is not expressible at all on the managed stack, so a managed census + would have recorded false absences for the patterns the Rust adapter will see. + + Captures under captures/03-pattern-census/: + pattern-matrix.json COM per-target ControlType x pattern matrix + sanity anchors + managed-crosscheck.json managed pattern vocabulary over the same fixture shapes + divergence.json per-pattern COM-vs-managed visibility, KTD1 evidence row +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +. "$PSScriptRoot\common.ps1" + +Add-Type -AssemblyName UIAutomationClient | Out-Null +Add-Type -AssemblyName UIAutomationTypes | Out-Null + +$Probe = '03-pattern-census' +$NodeBudget = 400 +$AE = [System.Windows.Automation.AutomationElement] +$RawWalker = [System.Windows.Automation.TreeWalker]::RawViewWalker +$MatrixRecordFormat = 'one row per line, space separated key=value fields in fixed order: target= ct= n= withpat= pats=. Counts are element occurrences, not distinct patterns.' +$script:Spawned = New-Object System.Collections.ArrayList + +function Get-DictRows { + param($Dict) + $pairs = @() + if ($null -eq $Dict) { return '-' } + foreach ($p in @($Dict.PSObject.Properties | Sort-Object -Property Name)) { + $pairs += ($p.Name + ':' + $p.Value) + } + if ($pairs.Count -eq 0) { return '-' } + return ($pairs -join ',') +} + +function Get-HashRows { + param([hashtable]$Table) + $pairs = @() + foreach ($k in @($Table.Keys | Sort-Object)) { $pairs += ($k + ':' + $Table[$k]) } + if ($pairs.Count -eq 0) { return '-' } + return ($pairs -join ',') +} + +function Add-Count { + param([hashtable]$Table, [string]$Key, [int]$By = 1) + if ($Table.ContainsKey($Key)) { $Table[$Key] = $Table[$Key] + $By } else { $Table[$Key] = $By } +} + +function Get-PatternCell { + param($ByControlType, [string]$ControlType, [string]$Pattern) + foreach ($row in @($ByControlType)) { + if ($row.controlType -ne $ControlType) { continue } + $hit = @($row.patterns.PSObject.Properties | Where-Object { $_.Name -eq $Pattern }) + $available = 0 + if ($hit.Count -gt 0) { $available = [int]$hit[0].Value } + return [ordered]@{ Present = $true; Elements = [int]$row.count; WithPattern = $available } + } + return [ordered]@{ Present = $false; Elements = 0; WithPattern = 0 } +} + +function Get-ManagedSweep { + param($Root, [string]$Label) + $nodes = 0 + $withPattern = 0 + $typeCounts = @{} + $typeWith = @{} + $byType = @{} + $totals = @{} + $order = New-Object System.Collections.Stack + $order.Push($Root) + while ($order.Count -gt 0 -and $nodes -lt $NodeBudget) { + $node = $order.Pop() + $nodes++ + $typeName = '' + try { $typeName = $node.Current.ControlType.ProgrammaticName -replace '^ControlType\.', '' } catch { } + Add-Count -Table $typeCounts -Key $typeName + $patterns = @() + try { $patterns = @($node.GetSupportedPatterns() | ForEach-Object { $_.ProgrammaticName -replace 'PatternIdentifiers\.Pattern$', '' }) } catch { } + if ($patterns.Count -gt 0) { + $withPattern++ + Add-Count -Table $typeWith -Key $typeName + } + foreach ($p in $patterns) { + Add-Count -Table $totals -Key $p + if (-not $byType.ContainsKey($typeName)) { $byType[$typeName] = @{} } + Add-Count -Table $byType[$typeName] -Key $p + } + try { + $child = $RawWalker.GetFirstChild($node) + while ($null -ne $child) { + $order.Push($child) + $child = $RawWalker.GetNextSibling($child) + } + } catch { } + } + $rows = @() + foreach ($k in @($typeCounts.Keys | Sort-Object)) { + $cells = '-' + if ($byType.ContainsKey($k)) { $cells = (Get-HashRows -Table $byType[$k]) } + $with = 0 + if ($typeWith.ContainsKey($k)) { $with = $typeWith[$k] } + $rows += ('target=' + $Label + ' ct=' + $k + ' n=' + $typeCounts[$k] + ' withpat=' + $with + ' pats=' + $cells) + } + return [pscustomobject]@{ + Label = $Label + RawViewNodes = $nodes + ElementsWithAnyPattern = $withPattern + PatternTotals = $totals + Rows = @($rows) + } +} + +function Get-ChildCount { + param($Element) + $n = 0 + try { + $k = $RawWalker.GetFirstChild($Element) + while ($null -ne $k) { $n++; $k = $RawWalker.GetNextSibling($k) } + } catch { } + return $n +} + +function Start-Tracked { + param([string]$FilePath, [string[]]$ArgumentList = @(), [int]$TimeoutSec = 25) + $p = Start-ScratchProcess -FilePath $FilePath -ArgumentList $ArgumentList -NoActivate -TimeoutSec $TimeoutSec + [void]$script:Spawned.Add($p.ProcessId) + return $p +} + +function Wait-TopLevelElementByName { + param([string]$Name, [int]$TimeoutSec = 40) + $walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker + $deadline = (Get-Date).AddSeconds($TimeoutSec) + while ($true) { + try { + $c = $walker.GetFirstChild($AE::RootElement) + while ($null -ne $c) { + try { + if ($c.Current.Name -eq $Name) { return $c } + } catch { } + $c = $walker.GetNextSibling($c) + } + } catch { } + if ((Get-Date) -ge $deadline) { return $null } + Start-Sleep -Milliseconds 400 + } +} + +function Start-ScratchWpfWindow { + param([string]$Tag, [int]$Left = 40, [int]$Top = 600, [int]$SettleSec = 6, [int]$Attempts = 3) + $title = 'AgentDesktop Scratch WPF [' + $Tag + ']' + for ($i = 1; $i -le $Attempts; $i++) { + $proc = Start-Tracked -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', + (Join-Path (Get-ProbeRoot) 'scratch\ScratchWpf.ps1'), '-Tag', $Tag, + '-Left', ([string]$Left), '-Top', ([string]$Top), '-TimeoutSeconds', '300') + Start-Sleep -Seconds $SettleSec + $element = Wait-TopLevelElementByName -Name $title -TimeoutSec 20 + if ($null -ne $element -and (Get-ChildCount -Element $element) -gt 0) { + $hostPid = 0 + try { $hostPid = [int]$element.Current.ProcessId } catch { } + if ($hostPid -gt 0 -and -not $script:Spawned.Contains($hostPid)) { + Register-ScratchProcessId -ProcessId $hostPid + [void]$script:Spawned.Add($hostPid) + } + $className = '' + try { $className = [string]$element.Current.ClassName } catch { } + return [pscustomobject]@{ + Element = $element + Attempts = $i + ClassNameShape = [regex]::Replace($className, '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', '') + } + } + Write-ProbeLog -Message ('WPF automation peer not bound on attempt ' + $i + '; relaunching to get a fresh HWND') -Level 'warn' + try { Stop-ScratchProcess -ProcessId $proc.ProcessId } catch { } + } + return $null +} + +$status = 'ok' +$message = '' +$resultData = [ordered]@{} + +try { + $censusPath = Join-Path (Join-Path (Get-ProbeRoot) 'captures\08-uia3-com') 'census.json' + if (-not (Test-Path -LiteralPath $censusPath)) { throw ('U7 census capture missing at ' + $censusPath + '; run 08-uia3-com.ps1 first') } + $census = [IO.File]::ReadAllText($censusPath) | ConvertFrom-Json + + $matrixRows = New-Object System.Collections.ArrayList + $totalsByTarget = [ordered]@{} + $comVocabulary = @{} + $comTargetsByPattern = @{} + $anchors = New-Object System.Collections.ArrayList + foreach ($t in @($census.result.targets)) { + foreach ($row in @($t.byControlType)) { + [void]$matrixRows.Add('target=' + $t.label + ' ct=' + $row.controlType + ' n=' + $row.count + + ' withpat=' + $row.withAnyPattern + ' pats=' + (Get-DictRows -Dict $row.patterns)) + } + $totalsByTarget[$t.label] = (Get-DictRows -Dict $t.patternTotals) + foreach ($p in @($t.patternTotals.PSObject.Properties)) { + Add-Count -Table $comVocabulary -Key $p.Name -By ([int]$p.Value) + if (-not $comTargetsByPattern.ContainsKey($p.Name)) { $comTargetsByPattern[$p.Name] = @() } + $comTargetsByPattern[$p.Name] = @($comTargetsByPattern[$p.Name] + $t.label) + } + $button = Get-PatternCell -ByControlType $t.byControlType -ControlType 'Button' -Pattern 'Invoke' + $text = Get-PatternCell -ByControlType $t.byControlType -ControlType 'Text' -Pattern 'Invoke' + [void]$anchors.Add([ordered]@{ + Target = $t.label + ButtonElements = $button.Elements + ButtonsWithInvoke = $button.WithPattern + ButtonAnchorHolds = ($button.Present -and $button.Elements -gt 0 -and $button.WithPattern -eq $button.Elements) + TextElements = $text.Elements + TextWithInvoke = $text.WithPattern + TextAnchorHolds = ($text.WithPattern -eq 0) + TextControlTypePresent = $text.Present + }) + } + $anchorFailures = @($anchors | Where-Object { -not $_.ButtonAnchorHolds -or -not $_.TextAnchorHolds } | ForEach-Object { $_.Target }) + + $matrix = [ordered]@{ + Probe = $Probe + Stack = 'uia3-com' + Authoritative = $true + Scope = 'app/provider' + Source = 'captures/08-uia3-com/census.json, produced by U7 shim mode "census" over hand-declared ComImport UIA3 (CUIAutomation8); reshaped here, not re-measured' + SourceRationale = 'KTD1 puts pattern availability on the COM stack because the managed stack structurally cannot name LegacyIAccessible, Drag, DropTarget, Annotation, Styles or TextChild. Re-walking the same four targets a second time would add a second sample of the same fact and a second chance for drift, so this probe consumes the committed measurement.' + View = 'RawView walk (shim census mode); inControlView is recorded per element in the source capture' + MatrixRecordFormat = $MatrixRecordFormat + Targets = @($census.result.targets | ForEach-Object { + [ordered]@{ + Label = $_.label + ProcessName = $_.processName + RootFrameworkId = $_.rootFrameworkId + ControlViewNodes = $_.controlViewNodes + RawViewNodes = $_.rawViewNodes + ElementsWithAnyPattern = $_.elementsWithAnyPattern + ElementsWithAnyPatternExcludingLegacy = $_.elementsWithAnyPatternExcludingLegacy + } + }) + PatternTotalsByTarget = $totalsByTarget + Rows = @($matrixRows) + SanityAnchors = @($anchors) + SanityAnchorNote = 'Invoke on Button and no-Invoke on Text are the two anchors that would catch a census reading the wrong property ids. winforms-default has no Text ControlType at all: its custom IRawElementProviderSimple collapses every child to Pane, so the Text anchor there is vacuously true and is reported with TextControlTypePresent=false rather than silently counted as a pass.' + Uia3OnlyPatterns = @($census.result.uia3OnlyPatternProperties) + PropertyIdNote = 'every availability property id in Uia3OnlyPatterns was discovered from the OS at runtime by U7, not written from memory. IsAnnotationPatternAvailable is 30118 on this build; 30113 - the value a from-memory table would carry - is a different property. A hardcoded id table is the single most likely silent failure in a Rust pattern-availability check.' + FixtureModeNote = 'winforms-default is a fixture artifact, not a Win32/WinForms platform fact: the scratch app installs a custom server-side IRawElementProviderSimple that suppresses both the client-side proxies and WinForms own providers, so every child collapses to Pane with LegacyIAccessible only. winforms-host-providers is the apples-to-apples WinForms row - there chkToggle is CheckBox+Toggle, txtValue is Edit+Value, cboChoice is ComboBox+ExpandCollapse+Value.' + } + [void](Write-ProbeJson -Probe $Probe -Name 'pattern-matrix.json' -InputObject $matrix) + + $scratchExe = Join-Path (Get-ProbeRoot) 'scratch\bin\ScratchForms.exe' + if (-not (Test-Path -LiteralPath $scratchExe)) { + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path (Get-ProbeRoot) 'scratch\build-scratch.ps1') | Out-Null + } + if (-not (Test-Path -LiteralPath $scratchExe)) { throw ('scratch fixture missing at ' + $scratchExe) } + + $wpf = Start-ScratchWpfWindow -Tag 'u4-census' + if ($null -eq $wpf) { throw 'WPF scratch window never bound its automation peer across three launches' } + $wpfElement = $wpf.Element + $wpfClassNameShape = $wpf.ClassNameShape + $winDefault = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u4-default', '--pos', '40,40') + $winHosted = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u4-hosted', '--pos', '840,40', '--host-providers') + $notepad = Start-Tracked -FilePath (Join-Path $env:WINDIR 'System32\notepad.exe') + if ($winDefault.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms default-mode window never appeared' } + if ($winHosted.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms host-providers window never appeared' } + if ($notepad.MainWindowHandle -eq [IntPtr]::Zero) { throw 'notepad window never appeared' } + + $sweeps = @( + (Get-ManagedSweep -Root ($AE::FromHandle($winDefault.MainWindowHandle)) -Label 'winforms-default'), + (Get-ManagedSweep -Root ($AE::FromHandle($winHosted.MainWindowHandle)) -Label 'winforms-host-providers'), + (Get-ManagedSweep -Root $wpfElement -Label 'wpf'), + (Get-ManagedSweep -Root ($AE::FromHandle($notepad.MainWindowHandle)) -Label 'notepad') + ) + $managedVocabulary = @{} + $managedTargetsByPattern = @{} + $managedRows = New-Object System.Collections.ArrayList + foreach ($s in $sweeps) { + foreach ($r in $s.Rows) { [void]$managedRows.Add($r) } + foreach ($k in @($s.PatternTotals.Keys)) { + Add-Count -Table $managedVocabulary -Key $k -By $s.PatternTotals[$k] + if (-not $managedTargetsByPattern.ContainsKey($k)) { $managedTargetsByPattern[$k] = @() } + $managedTargetsByPattern[$k] = @($managedTargetsByPattern[$k] + $s.Label) + } + } + + $crossCheck = [ordered]@{ + Probe = $Probe + Stack = 'managed-System.Windows.Automation' + Authoritative = $false + Scope = 'app/provider' + Method = 'fresh RawViewWalker sweep over the same four fixture shapes, calling AutomationElement.GetSupportedPatterns() per element and reducing each AutomationPattern ProgrammaticName by stripping the PatternIdentifiers.Pattern suffix' + WhyNotAuthoritative = 'KTD1: recorded only so the COM-vs-managed pattern vocabulary can be diffed. Where a managed row disagrees with the COM matrix the COM row is the product-relevant one, because the Rust adapter wraps a UIA3 COM client.' + SameRunCaveat = 'these windows are fresh instances launched by this probe, not the HWNDs U7 measured. That does not weaken the divergence row: the claim under test is which pattern names a client stack can express at all, which is a property of the client library, not of a window instance. U7 comparison.json already holds the same-run, same-HWND node-count comparison.' + WpfPeerActivation = [ordered]@{ + Verdict = 'NEW-EDGE' + ClassNameShapeAfterActivation = $wpfClassNameShape + timingPeerActivationLaunches = $wpf.Attempts + Observation = 'if a UIA client reads the WPF window before WPF has built its automation peer, the client binds the generic HWND provider for that window and never re-resolves it. The window then reports ControlType.Window with ClassName HwndWrapper[;;] and ZERO children, and stays that way. Measured: a 30 s poll loop in the same client process, including AutomationElement.FromHandle and a FindAll(Descendants, TrueCondition) forced traversal on every pass, never recovered the tree. The same fixture read for the first time 8 s after launch reports ClassName=Window with 8 children immediately.' + Consequence = 'a Windows snapshot taken immediately after launching a WPF app can return a permanently empty one-node tree, and retrying inside the same process cannot fix it - the adapter would have to re-resolve from a new HWND or a new client. This is a different failure mode from the Electron settle U3 measured: Electron under-reports and then grows, WPF binds the wrong provider and stays wrong.' + ProbePolicy = 'this probe launches the WPF fixture first, settles before its first UIA touch, verifies the root has children, and relaunches on a fresh HWND if it does not. timingPeerActivationLaunches records how many launches that took on this run.' + ClassNameCorollary = 'the two shapes ClassName takes (Window when the peer is bound, HwndWrapper[...;] when it is not) also disqualify ClassName as an element-identity component on WPF: one of them embeds a per-launch GUID.' + } + MatrixRecordFormat = $MatrixRecordFormat + Targets = @($sweeps | ForEach-Object { + [ordered]@{ + Label = $_.Label + RawViewNodes = $_.RawViewNodes + ElementsWithAnyPattern = $_.ElementsWithAnyPattern + PatternTotals = (Get-HashRows -Table $_.PatternTotals) + } + }) + Rows = @($managedRows) + NodeBudget = $NodeBudget + } + [void](Write-ProbeJson -Probe $Probe -Name 'managed-crosscheck.json' -InputObject $crossCheck) + + $managedPatternClasses = @([System.Windows.Automation.AutomationPattern].Assembly.GetTypes() | + Where-Object { $_.IsPublic -and $_.Name -match 'PatternIdentifiers$' } | + ForEach-Object { $_.Name -replace 'PatternIdentifiers$', '' } | Sort-Object) + $allPatterns = @(@($comVocabulary.Keys) + @($managedVocabulary.Keys) | Sort-Object -Unique) + $divergenceRows = New-Object System.Collections.ArrayList + foreach ($p in $allPatterns) { + $comCount = 0 + if ($comVocabulary.ContainsKey($p)) { $comCount = $comVocabulary[$p] } + $managedCount = 0 + if ($managedVocabulary.ContainsKey($p)) { $managedCount = $managedVocabulary[$p] } + $expressible = ($managedPatternClasses -contains $p) + $verdict = 'both stacks report it' + if ($comCount -gt 0 -and $managedCount -eq 0 -and -not $expressible) { $verdict = 'COM only - the managed stack has no identifier for this pattern and can never report it' } + elseif ($comCount -gt 0 -and $managedCount -eq 0) { $verdict = 'COM only - the managed stack could name this pattern but did not walk an element advertising it' } + elseif ($comCount -eq 0 -and $managedCount -gt 0) { $verdict = 'managed only' } + $comTargets = @() + if ($comTargetsByPattern.ContainsKey($p)) { $comTargets = @($comTargetsByPattern[$p]) } + $managedTargets = @() + if ($managedTargetsByPattern.ContainsKey($p)) { $managedTargets = @($managedTargetsByPattern[$p]) } + [void]$divergenceRows.Add([ordered]@{ + Pattern = $p + ExpressibleByManagedStack = $expressible + ComOccurrences = $comCount + ManagedOccurrences = $managedCount + ComTargets = $comTargets + ManagedTargets = $managedTargets + Verdict = $verdict + }) + } + $comOnly = @($divergenceRows | Where-Object { $_.Verdict -like 'COM only*' } | ForEach-Object { $_.Pattern }) + $structurallyInvisible = @($divergenceRows | Where-Object { $_.ComOccurrences -gt 0 -and -not $_.ExpressibleByManagedStack } | ForEach-Object { $_.Pattern }) + $legacyRow = @($divergenceRows | Where-Object { $_.Pattern -eq 'LegacyIAccessible' }) + $legacyCom = 0 + if ($legacyRow.Count -gt 0) { $legacyCom = $legacyRow[0].ComOccurrences } + $legacyManaged = -1 + if ($legacyRow.Count -gt 0) { $legacyManaged = $legacyRow[0].ManagedOccurrences } + + $divergence = [ordered]@{ + Probe = $Probe + Question = 'does a managed pattern census record the same availability the Rust adapter will see through UIA3 COM' + ComparisonBase = 'COM occurrences summed across the four census targets in captures/08-uia3-com/census.json; managed occurrences summed across the four fresh sweeps in managed-crosscheck.json' + ManagedPatternClasses = @($managedPatternClasses) + ManagedPatternClassCount = @($managedPatternClasses).Count + ManagedVocabularyNote = 'ManagedPatternClasses is reflected out of the UIAutomationTypes assembly at run time (public types named *PatternIdentifiers), not transcribed. It is the complete set of pattern names System.Windows.Automation can ever return, so a COM-observed pattern outside this set is a structural blind spot rather than a sampling gap. The two cases are separated in every row: ExpressibleByManagedStack=false is structural, ExpressibleByManagedStack=true with zero managed occurrences only means the smaller managed tree did not reach an element advertising it.' + Rows = @($divergenceRows) + ComOnlyPatterns = @($comOnly) + StructurallyInvisibleToManaged = @($structurallyInvisible) + LegacyIAccessibleRow = [ordered]@{ + ComElementsReportingIt = $legacyCom + ManagedElementsReportingIt = $legacyManaged + ComElementsTotal = (@($census.result.targets | ForEach-Object { $_.rawViewNodes }) | Measure-Object -Sum).Sum + Verdict = 'CONFIRMS KTD1' + Finding = 'LegacyIAccessible is advertised by every element the COM census walked and by zero elements the managed sweep walked. The managed absence is structural, not environmental: System.Windows.Automation exposes 22 pattern classes and has no LegacyIAccessible identifier to return, so GetSupportedPatterns() can never name it. A managed-only census would have filed that as "pattern unavailable" for the whole corpus.' + Consequence = 'every pattern-availability row in this sub-phase must carry stack=uia3-com. A Windows adapter that decides actionability from a managed-derived pattern table would under-report actionable elements on exactly the legacy Win32 surfaces where LegacyIAccessible.DoDefaultAction is the only affordance.' + } + NotepadDocumentRow = [ordered]@{ + Finding = 'the same divergence shows up as a ControlType disagreement, not only a pattern one: classic Notepad edit surface is ControlType.Pane with AutomationId 15 to the managed client (U3, 01-tree-dump) and ControlType.Document with Value, Text, Text2, Scroll, LegacyIAccessible to the COM client. Text2 (30119) is another pattern the managed stack cannot name.' + Verdict = 'NEW-EDGE' + } + } + [void](Write-ProbeJson -Probe $Probe -Name 'divergence.json' -InputObject $divergence) + + $resultData['comTargets'] = @($census.result.targets).Count + $resultData['comPatternVocabulary'] = @($comVocabulary.Keys).Count + $resultData['managedPatternVocabulary'] = @($managedVocabulary.Keys).Count + $resultData['comOnlyPatterns'] = ($comOnly -join ',') + $resultData['structurallyInvisibleToManaged'] = ($structurallyInvisible -join ',') + $resultData['legacyComOccurrences'] = $legacyCom + $resultData['legacyManagedOccurrences'] = $legacyManaged + $resultData['anchorFailures'] = ($anchorFailures -join ',') + if ($anchorFailures.Count -gt 0) { throw ('sanity anchor failed on: ' + ($anchorFailures -join ', ')) } + if ($legacyCom -le 0 -or $legacyManaged -ne 0) { throw ('LegacyIAccessible divergence not reproduced: com=' + $legacyCom + ' managed=' + $legacyManaged) } + $message = 'com pattern matrix reshaped from U7 census; managed cross-check reports ' + $comOnly.Count + ' COM-only pattern(s)' +} catch { + $status = 'fail' + $message = ($_.Exception.Message -replace '[\r\n]+', ' ') + Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error' +} finally { + foreach ($id in @($script:Spawned)) { + try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' } + } + foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) { + if ($f.Name -like '*.normalized') { continue } + if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) } + } +} + +Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $resultData +if ($status -eq 'fail') { exit 1 } +exit 0 diff --git a/probes/windows/04-automationid-census.ps1 b/probes/windows/04-automationid-census.ps1 new file mode 100644 index 0000000..562ed1e --- /dev/null +++ b/probes/windows/04-automationid-census.ps1 @@ -0,0 +1,638 @@ +<# +.SYNOPSIS + Probe 04 (sub-phase 2.0, unit U4): AutomationId coverage across the four Windows + UI stacks, and the identity-stability experiment 2.5's element-resolution design + depends on. + +.DESCRIPTION + Part A - coverage. One managed ControlView walk per stack (Win32 Notepad, Win32 + Explorer, WinForms in both fixture modes, WPF, Electron/Obsidian), reporting the + percentage of interactive elements carrying a non-empty AutomationId and the id + style (numeric control id vs symbolic). U7's COM census is consumed as a + cross-check on the four targets whose element list it captured completely. + + Part B - identity stability. The repo's RefEntry evidence set is pid, role, path, + stable text identity and bounds hash. Nothing else in this corpus measures which + of those survive on Windows, so this probe dumps a target, changes exactly one + thing, dumps it again, and reports per-property survival: + + arm "mutation" list content changes inside the SAME process (InvokePattern + on btnMutateList; new/removed files for the Explorer folder) + arm "restart" the process is terminated and relaunched with identical + arguments at the identical window position + + RuntimeId handling (KTD9): the normalized twin canonicalizes RuntimeId to + , which would erase exactly the evidence this probe produces. Every + RuntimeId comparison is therefore done in memory and only the RESULT is written - + survived/changed counts and a stable structural classification of the id. No raw + RuntimeId value reaches a capture. Names are handled the same way: they are + compared in memory and only equality counts are written, so no document content + is serialized. + + Captures under captures/04-automationid-census/: + coverage.json per-stack AutomationId coverage + id-style census + identity-mutation.json per-property survival across an in-process list change + identity-restart.json per-property survival across process restart +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +. "$PSScriptRoot\common.ps1" + +Add-Type -AssemblyName UIAutomationClient | Out-Null +Add-Type -AssemblyName UIAutomationTypes | Out-Null + +$Probe = '04-automationid-census' +$NodeBudget = 800 +$WalkMaxDepth = 14 +$AE = [System.Windows.Automation.AutomationElement] +$Walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker +$RawWalker = [System.Windows.Automation.TreeWalker]::RawViewWalker +$InteractiveControlTypes = @('Button', 'CheckBox', 'ComboBox', 'DataItem', 'Edit', 'Hyperlink', + 'ListItem', 'MenuItem', 'RadioButton', 'Slider', 'Spinner', 'SplitButton', 'Tab', 'TabItem', 'TreeItem') +$AutomationIdSampleLimit = 45 +$ObsidianSettleMs = 8000 +$Pos = @{ + WinFormsDefault = @{ X = 20; Y = 20 } + WinFormsHosted = @{ X = 520; Y = 20 } + Wpf = @{ X = 20; Y = 520 } + Notepad = @{ X = 1020; Y = 20; Width = 600; Height = 450 } + Explorer = @{ X = 1020; Y = 490; Width = 600; Height = 400 } +} +$script:Spawned = New-Object System.Collections.ArrayList +$script:ExplorerHandle = 0 +$script:ExplorerDir = '' +$script:ObsidianLaunched = $false + +function Start-Tracked { + param([string]$FilePath, [string[]]$ArgumentList = @(), [int]$TimeoutSec = 25) + $p = Start-ScratchProcess -FilePath $FilePath -ArgumentList $ArgumentList -NoActivate -TimeoutSec $TimeoutSec + [void]$script:Spawned.Add($p.ProcessId) + return $p +} + +function Get-ChildCount { + param($Element) + $n = 0 + try { + $k = $RawWalker.GetFirstChild($Element) + while ($null -ne $k) { $n++; $k = $RawWalker.GetNextSibling($k) } + } catch { } + return $n +} + +function Wait-TopLevelElement { + param([string]$Name = '', [string]$ClassName = '', [int[]]$ExcludeHandles = @(), [int[]]$ProcessIds = @(), [int]$TimeoutSec = 40) + $deadline = (Get-Date).AddSeconds($TimeoutSec) + while ($true) { + try { + $c = $Walker.GetFirstChild($AE::RootElement) + while ($null -ne $c) { + try { + if (($Name -eq '' -or $c.Current.Name -eq $Name) -and + ($ClassName -eq '' -or $c.Current.ClassName -eq $ClassName) -and + -not ($ExcludeHandles -contains [int]$c.Current.NativeWindowHandle) -and + ($ProcessIds.Count -eq 0 -or $ProcessIds -contains [int]$c.Current.ProcessId)) { return $c } + } catch { } + $c = $Walker.GetNextSibling($c) + } + } catch { } + if ((Get-Date) -ge $deadline) { return $null } + Start-Sleep -Milliseconds 450 + } +} + +function Start-ScratchWpfWindow { + param([string]$Tag, [int]$Left, [int]$Top, [int]$SettleSec = 6, [int]$Attempts = 3) + $title = 'AgentDesktop Scratch WPF [' + $Tag + ']' + for ($i = 1; $i -le $Attempts; $i++) { + $proc = Start-Tracked -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', + (Join-Path (Get-ProbeRoot) 'scratch\ScratchWpf.ps1'), '-Tag', $Tag, + '-Left', ([string]$Left), '-Top', ([string]$Top), '-TimeoutSeconds', '600') + Start-Sleep -Seconds $SettleSec + $element = Wait-TopLevelElement -Name $title -TimeoutSec 20 + if ($null -ne $element -and (Get-ChildCount -Element $element) -gt 0) { + $hostPid = 0 + try { $hostPid = [int]$element.Current.ProcessId } catch { } + if ($hostPid -gt 0 -and -not $script:Spawned.Contains($hostPid)) { + Register-ScratchProcessId -ProcessId $hostPid + [void]$script:Spawned.Add($hostPid) + } + return [pscustomobject]@{ Element = $element; ProcessId = $hostPid; LauncherProcessId = $proc.ProcessId; Attempts = $i } + } + Write-ProbeLog -Message ('WPF automation peer not bound on attempt ' + $i + '; relaunching for a fresh HWND') -Level 'warn' + try { Stop-ScratchProcess -ProcessId $proc.ProcessId } catch { } + } + return $null +} + +function Get-NodeRecords { + param($Root) + $records = New-Object System.Collections.ArrayList + $order = New-Object System.Collections.Stack + $order.Push(@{ Element = $Root; Depth = 0; Path = '0' }) + while ($order.Count -gt 0 -and $records.Count -lt $NodeBudget) { + $item = $order.Pop() + $element = $item.Element + $cur = $null + try { $cur = $element.Current } catch { } + if ($null -eq $cur) { continue } + $controlType = '' + try { $controlType = $cur.ControlType.ProgrammaticName -replace '^ControlType\.', '' } catch { } + $automationId = '' + try { $automationId = [string]$cur.AutomationId } catch { } + $className = '' + try { $className = [string]$cur.ClassName } catch { } + $name = '' + try { $name = [string]$cur.Name } catch { } + $runtimeId = '' + try { $runtimeId = (@($element.GetRuntimeId()) -join '.') } catch { } + $bounds = '' + try { + $r = $cur.BoundingRectangle + if (-not $r.IsEmpty) { $bounds = ([int]$r.Left).ToString() + ',' + ([int]$r.Top) + ',' + ([int]$r.Width) + ',' + ([int]$r.Height) } + } catch { } + [void]$records.Add([pscustomobject]@{ + Path = $item.Path + Depth = $item.Depth + ControlType = $controlType + AutomationId = $automationId + ClassName = $className + Name = $name + RuntimeId = $runtimeId + Bounds = $bounds + }) + if ($item.Depth -ge $WalkMaxDepth) { continue } + $kids = New-Object System.Collections.ArrayList + try { + $k = $Walker.GetFirstChild($element) + while ($null -ne $k) { [void]$kids.Add($k); $k = $Walker.GetNextSibling($k) } + } catch { } + for ($i = $kids.Count - 1; $i -ge 0; $i--) { + $order.Push(@{ Element = $kids[$i]; Depth = ($item.Depth + 1); Path = ($item.Path + '.' + $i) }) + } + } + return @($records) +} + +function Get-BucketedBounds { + param([string]$Bounds) + if ([string]::IsNullOrEmpty($Bounds)) { return '' } + $parts = $Bounds -split ',' + $out = @() + foreach ($p in $parts) { $out += [string]([int]([Math]::Round(([double]$p) / 8.0) * 8)) } + return ($out -join ',') +} + +function Get-AutomationIdStyle { + param([string]$Value) + if ([string]::IsNullOrEmpty($Value)) { return 'empty' } + if ($Value -match '^\d+$') { return 'numeric-control-id' } + return 'symbolic' +} + +function Get-CoverageRow { + param([string]$Stack, [string]$Target, $Records, [bool]$SampleIds = $true) + $all = @($Records) + $interactive = @($all | Where-Object { $InteractiveControlTypes -contains $_.ControlType }) + $allWithId = @($all | Where-Object { -not [string]::IsNullOrEmpty($_.AutomationId) }) + $interactiveWithId = @($interactive | Where-Object { -not [string]::IsNullOrEmpty($_.AutomationId) }) + $styles = @{ 'empty' = 0; 'numeric-control-id' = 0; 'symbolic' = 0 } + foreach ($r in $all) { $styles[(Get-AutomationIdStyle -Value $r.AutomationId)] += 1 } + $pctAll = 0 + if ($all.Count -gt 0) { $pctAll = [Math]::Round((100.0 * $allWithId.Count / $all.Count), 1) } + $pctInteractive = 0 + if ($interactive.Count -gt 0) { $pctInteractive = [Math]::Round((100.0 * $interactiveWithId.Count / $interactive.Count), 1) } + $sample = @() + if ($SampleIds) { $sample = @($allWithId | Select-Object -First $AutomationIdSampleLimit | ForEach-Object { $_.ControlType + '=' + $_.AutomationId }) } + $dominant = 'none' + if ($styles['symbolic'] -gt $styles['numeric-control-id']) { $dominant = 'symbolic' } + elseif ($styles['numeric-control-id'] -gt 0) { $dominant = 'numeric-control-id' } + return [ordered]@{ + Target = $Target + Stack = $Stack + Elements = $all.Count + ElementsWithAutomationId = $allWithId.Count + PctAllElements = $pctAll + InteractiveElements = $interactive.Count + InteractiveWithAutomationId = $interactiveWithId.Count + PctInteractiveElements = $pctInteractive + IdStyleEmpty = $styles['empty'] + IdStyleNumericControlId = $styles['numeric-control-id'] + IdStyleSymbolic = $styles['symbolic'] + DominantIdStyle = $dominant + AutomationIdSample = @($sample) + } +} + +function Get-PropertySurvival { + param($Pairs, [string]$Property) + $all = @($Pairs) + $survived = 0 + foreach ($pair in $all) { + if ($pair.Before.$Property -eq $pair.After.$Property) { $survived++ } + } + $pct = 0 + if ($all.Count -gt 0) { $pct = [Math]::Round((100.0 * $survived / $all.Count), 1) } + return [ordered]@{ Property = $Property; Matched = $all.Count; Survived = $survived; PctSurvived = $pct } +} + +function Compare-Identity { + param([string]$Target, [string]$Arm, $Before, $After, [string]$Change) + $beforeByPath = @{} + foreach ($r in $Before) { $beforeByPath[$r.Path] = $r } + $afterByPath = @{} + foreach ($r in $After) { $afterByPath[$r.Path] = $r } + $pairs = New-Object System.Collections.ArrayList + foreach ($k in $beforeByPath.Keys) { + if ($afterByPath.ContainsKey($k)) { + [void]$pairs.Add([pscustomobject]@{ Before = $beforeByPath[$k]; After = $afterByPath[$k] }) + } + } + $pathOnlyBefore = @($beforeByPath.Keys | Where-Object { -not $afterByPath.ContainsKey($_) }).Count + $pathOnlyAfter = @($afterByPath.Keys | Where-Object { -not $beforeByPath.ContainsKey($_) }).Count + + $beforeById = @{} + foreach ($r in $Before) { + if ([string]::IsNullOrEmpty($r.AutomationId)) { continue } + $key = $r.ControlType + '|' + $r.AutomationId + if ($beforeById.ContainsKey($key)) { $beforeById[$key] = $null } else { $beforeById[$key] = $r } + } + $afterById = @{} + foreach ($r in $After) { + if ([string]::IsNullOrEmpty($r.AutomationId)) { continue } + $key = $r.ControlType + '|' + $r.AutomationId + if ($afterById.ContainsKey($key)) { $afterById[$key] = $null } else { $afterById[$key] = $r } + } + $idPairs = New-Object System.Collections.ArrayList + foreach ($k in $beforeById.Keys) { + if ($null -eq $beforeById[$k]) { continue } + if ($afterById.ContainsKey($k) -and $null -ne $afterById[$k]) { + [void]$idPairs.Add([pscustomobject]@{ Before = $beforeById[$k]; After = $afterById[$k] }) + } + } + $idUniqueBefore = @($beforeById.Keys | Where-Object { $null -ne $beforeById[$_] }).Count + $idLost = $idUniqueBefore - @($idPairs).Count + + $boundsPairs = @($pairs | ForEach-Object { + [pscustomobject]@{ + Before = [pscustomobject]@{ BoundsBucketed = (Get-BucketedBounds -Bounds $_.Before.Bounds); PathAndBounds = $_.Before.Bounds } + After = [pscustomobject]@{ BoundsBucketed = (Get-BucketedBounds -Bounds $_.After.Bounds); PathAndBounds = $_.After.Bounds } + } + }) + + $byType = New-Object System.Collections.ArrayList + foreach ($t in @($pairs | ForEach-Object { $_.Before.ControlType } | Sort-Object -Unique)) { + $subset = @($pairs | Where-Object { $_.Before.ControlType -eq $t }) + [void]$byType.Add([ordered]@{ + ControlType = $t + PathMatchedPairs = $subset.Count + AutomationIdSurvived = @($subset | Where-Object { $_.Before.AutomationId -eq $_.After.AutomationId }).Count + NameSurvived = @($subset | Where-Object { $_.Before.Name -eq $_.After.Name }).Count + RuntimeIdSurvived = @($subset | Where-Object { $_.Before.RuntimeId -eq $_.After.RuntimeId }).Count + BoundsSurvived = @($subset | Where-Object { $_.Before.Bounds -eq $_.After.Bounds }).Count + }) + } + + return [ordered]@{ + Target = $Target + Arm = $Arm + Change = $Change + Stack = 'managed-System.Windows.Automation ControlView' + BeforeNodes = @($Before).Count + AfterNodes = @($After).Count + PathMatchedPairs = @($pairs).Count + PathsOnlyInBefore = $pathOnlyBefore + PathsOnlyInAfter = $pathOnlyAfter + PerPropertySurvival = @( + (Get-PropertySurvival -Pairs $pairs -Property 'ControlType'), + (Get-PropertySurvival -Pairs $pairs -Property 'AutomationId'), + (Get-PropertySurvival -Pairs $pairs -Property 'ClassName'), + (Get-PropertySurvival -Pairs $pairs -Property 'Name'), + (Get-PropertySurvival -Pairs $pairs -Property 'RuntimeId'), + (Get-PropertySurvival -Pairs $pairs -Property 'Bounds') + ) + BoundsBucketed8pxSurvival = (Get-PropertySurvival -Pairs $boundsPairs -Property 'BoundsBucketed') + AutomationIdKeyedMatch = [ordered]@{ + UniqueKeysBefore = $idUniqueBefore + KeysReFound = @($idPairs).Count + KeysLost = $idLost + PathAlsoSurvived = @($idPairs | Where-Object { $_.Before.Path -eq $_.After.Path }).Count + RuntimeIdAlsoSurvived = @($idPairs | Where-Object { $_.Before.RuntimeId -eq $_.After.RuntimeId }).Count + KeysReFoundOnADifferentElement = @($idPairs | Where-Object { $_.Before.Name -ne $_.After.Name }).Count + KeysReFoundOnADifferentElementNote = 'the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.' + Note = 'the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily' + } + ByControlType = @($byType) + } +} + +function Invoke-ByAutomationId { + param($Root, [string]$AutomationId) + $condition = New-Object System.Windows.Automation.PropertyCondition($AE::AutomationIdProperty, $AutomationId) + $target = $Root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $condition) + if ($null -eq $target) { throw ('element with AutomationId ' + $AutomationId + ' not found') } + $pattern = $target.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) + $pattern.Invoke() +} + +function Get-StatusText { + param($Root, [string]$AutomationId = 'lblStatus') + try { + $condition = New-Object System.Windows.Automation.PropertyCondition($AE::AutomationIdProperty, $AutomationId) + $element = $Root.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $condition) + if ($null -eq $element) { return '' } + return [string]$element.Current.Name + } catch { return '' } +} + +function Close-ShellWindowByHandle { + param([int]$Handle) + $shell = New-Object -ComObject Shell.Application + foreach ($w in @($shell.Windows())) { + try { if ([int]$w.HWND -eq $Handle) { $w.Quit(); return $true } } catch { } + } + return $false +} + +$status = 'ok' +$message = '' +$resultData = [ordered]@{} + +try { + Initialize-ProbeNative + $scratchExe = Join-Path (Get-ProbeRoot) 'scratch\bin\ScratchForms.exe' + if (-not (Test-Path -LiteralPath $scratchExe)) { + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path (Get-ProbeRoot) 'scratch\build-scratch.ps1') | Out-Null + } + if (-not (Test-Path -LiteralPath $scratchExe)) { throw ('scratch fixture missing at ' + $scratchExe) } + + $coverage = New-Object System.Collections.ArrayList + + # --- Electron first, alone on the desktop (U3 placement hazard) --------- + $obsidianExe = Join-Path $env:LOCALAPPDATA 'Programs\Obsidian\Obsidian.exe' + $obsidianVersion = '' + $obsidianRow = $null + if (Test-Path -LiteralPath $obsidianExe) { + $obsidianVersion = (Get-Item -LiteralPath $obsidianExe).VersionInfo.FileVersion + $obsidianPids = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue | ForEach-Object { $_.Id }) + if ($obsidianPids.Count -eq 0) { + [void](Start-Tracked -FilePath $obsidianExe -TimeoutSec 40) + $script:ObsidianLaunched = $true + Start-Sleep -Seconds 4 + $obsidianPids = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue | ForEach-Object { $_.Id }) + foreach ($opid in $obsidianPids) { + if (-not $script:Spawned.Contains($opid)) { Register-ScratchProcessId -ProcessId $opid; [void]$script:Spawned.Add($opid) } + } + } + $obsidianWindow = Wait-TopLevelElement -ClassName 'Chrome_WidgetWin_1' -ProcessIds $obsidianPids -TimeoutSec 40 + if ($null -ne $obsidianWindow) { + Show-WindowNoActivate -WindowHandle ([IntPtr][int]$obsidianWindow.Current.NativeWindowHandle) -X 40 -Y 40 -Width 1000 -Height 640 + $obsidianRecords = @() + $settlePasses = 0 + for ($i = 1; $i -le 3; $i++) { + Start-Sleep -Milliseconds $ObsidianSettleMs + $settlePasses = $i + $obsidianRecords = Get-NodeRecords -Root $obsidianWindow + if (@($obsidianRecords).Count -gt 20) { break } + } + $obsidianRow = Get-CoverageRow -Stack 'electron-chromium' -Target 'obsidian' -Records $obsidianRecords -SampleIds $false + $obsidianRow['timingSettlePasses'] = $settlePasses + $obsidianRow['AutomationIdSampleWithheld'] = 'AutomationId values are emitted verbatim for probe-owned fixtures and for Win32 chrome, but not for Obsidian: an Electron AutomationId can be derived from note or vault content, which R11 keeps out of the corpus. Only counts and id-style statistics are reported for this row.' + [void]$coverage.Add($obsidianRow) + } else { + Write-ProbeLog -Message 'no Obsidian-owned Chromium top-level window found; Electron coverage row is skipped' -Level 'warn' + } + } + if ($script:ObsidianLaunched) { + foreach ($p in @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue)) { try { Stop-ScratchProcess -ProcessId $p.Id } catch { } } + $script:ObsidianLaunched = $false + } + + # --- WinForms default mode: coverage only ------------------------------ + $winDefault = Start-Tracked -FilePath $scratchExe -ArgumentList @('--tag', 'u4-default', '--pos', ($Pos.WinFormsDefault.X.ToString() + ',' + $Pos.WinFormsDefault.Y)) + if ($winDefault.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms default-mode window never appeared' } + [void]$coverage.Add((Get-CoverageRow -Stack 'winforms' -Target 'winforms-default' -Records (Get-NodeRecords -Root ($AE::FromHandle($winDefault.MainWindowHandle))))) + Stop-ScratchProcess -ProcessId $winDefault.ProcessId + + # --- WinForms host-providers mode: coverage + identity baseline -------- + $winHostedArgs = @('--tag', 'u4-hosted', '--pos', ($Pos.WinFormsHosted.X.ToString() + ',' + $Pos.WinFormsHosted.Y), '--host-providers') + $winHosted = Start-Tracked -FilePath $scratchExe -ArgumentList $winHostedArgs + if ($winHosted.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms host-providers window never appeared' } + $winHostedRoot = $AE::FromHandle($winHosted.MainWindowHandle) + $winBaseline = Get-NodeRecords -Root $winHostedRoot + [void]$coverage.Add((Get-CoverageRow -Stack 'winforms' -Target 'winforms-host-providers' -Records $winBaseline)) + + # --- WPF: coverage + identity baseline --------------------------------- + $wpf = Start-ScratchWpfWindow -Tag 'u4-identity' -Left $Pos.Wpf.X -Top $Pos.Wpf.Y + if ($null -eq $wpf) { throw 'WPF scratch window never bound its automation peer across three launches' } + $wpfBaseline = Get-NodeRecords -Root $wpf.Element + [void]$coverage.Add((Get-CoverageRow -Stack 'wpf' -Target 'wpf' -Records $wpfBaseline)) + + # --- Win32 Notepad: coverage + identity baseline ----------------------- + $notepad = Start-Tracked -FilePath (Join-Path $env:WINDIR 'System32\notepad.exe') + if ($notepad.MainWindowHandle -eq [IntPtr]::Zero) { throw 'notepad window never appeared' } + Show-WindowNoActivate -WindowHandle $notepad.MainWindowHandle -X $Pos.Notepad.X -Y $Pos.Notepad.Y -Width $Pos.Notepad.Width -Height $Pos.Notepad.Height + Start-Sleep -Milliseconds 700 + $notepadBaseline = Get-NodeRecords -Root ($AE::FromHandle($notepad.MainWindowHandle)) + [void]$coverage.Add((Get-CoverageRow -Stack 'win32' -Target 'notepad' -Records $notepadBaseline)) + + # --- Win32 Explorer over a probe-owned folder: coverage + identity ----- + $script:ExplorerDir = Join-Path $env:TEMP 'agent-desktop-u4-explorer' + if (Test-Path -LiteralPath $script:ExplorerDir) { Remove-Item -LiteralPath $script:ExplorerDir -Recurse -Force } + New-Item -ItemType Directory -Path $script:ExplorerDir -Force | Out-Null + foreach ($n in @('item-alpha', 'item-bravo', 'item-charlie', 'item-delta', 'item-echo')) { + [IO.File]::WriteAllText((Join-Path $script:ExplorerDir ($n + '.txt')), $n) + } + $preCabinet = @() + $c = $Walker.GetFirstChild($AE::RootElement) + while ($null -ne $c) { + try { if ($c.Current.ClassName -eq 'CabinetWClass') { $preCabinet += [int]$c.Current.NativeWindowHandle } } catch { } + $c = $Walker.GetNextSibling($c) + } + $explorerLauncher = Start-Process -FilePath (Join-Path $env:WINDIR 'explorer.exe') -ArgumentList @($script:ExplorerDir) -PassThru + Register-ScratchProcessId -ProcessId $explorerLauncher.Id + $explorerElement = Wait-TopLevelElement -ClassName 'CabinetWClass' -ExcludeHandles $preCabinet -TimeoutSec 40 + if ($null -eq $explorerElement) { throw 'no CabinetWClass folder window appeared for the probe folder' } + $script:ExplorerHandle = [int]$explorerElement.Current.NativeWindowHandle + Show-WindowNoActivate -WindowHandle ([IntPtr]$script:ExplorerHandle) -X $Pos.Explorer.X -Y $Pos.Explorer.Y -Width $Pos.Explorer.Width -Height $Pos.Explorer.Height + Start-Sleep -Seconds 3 + $explorerBaseline = Get-NodeRecords -Root $explorerElement + [void]$coverage.Add((Get-CoverageRow -Stack 'win32' -Target 'explorer-folder-window' -Records $explorerBaseline)) + + # --- COM cross-check from U7's complete element lists ------------------ + $comRows = New-Object System.Collections.ArrayList + $censusPath = Join-Path (Join-Path (Get-ProbeRoot) 'captures\08-uia3-com') 'census.json' + if (Test-Path -LiteralPath $censusPath) { + $census = [IO.File]::ReadAllText($censusPath) | ConvertFrom-Json + foreach ($t in @($census.result.targets)) { + $els = @($t.elements) + if ($els.Count -lt $t.rawViewNodes) { continue } + $withId = @($els | Where-Object { -not [string]::IsNullOrEmpty($_.automationId) }) + $interactive = @($els | Where-Object { $InteractiveControlTypes -contains $_.controlType }) + $interactiveWithId = @($interactive | Where-Object { -not [string]::IsNullOrEmpty($_.automationId) }) + $pctAll = 0 + if ($els.Count -gt 0) { $pctAll = [Math]::Round((100.0 * $withId.Count / $els.Count), 1) } + $pctInteractive = 0 + if ($interactive.Count -gt 0) { $pctInteractive = [Math]::Round((100.0 * $interactiveWithId.Count / $interactive.Count), 1) } + [void]$comRows.Add([ordered]@{ + Target = $t.label + Stack = 'uia3-com' + View = 'RawView' + Elements = $els.Count + ElementsWithAutomationId = $withId.Count + PctAllElements = $pctAll + InteractiveElements = $interactive.Count + InteractiveWithAutomationId = $interactiveWithId.Count + PctInteractiveElements = $pctInteractive + }) + } + } + + $coverageCapture = [ordered]@{ + Probe = $Probe + Question = 'what fraction of interactive elements carries a non-empty AutomationId on each Windows UI stack, and in what style' + Stack = 'managed-System.Windows.Automation' + Scope = 'app/provider' + View = 'ControlView - the view a snapshot would walk; the COM cross-check below is RawView, so its denominators are larger by construction' + InteractiveControlTypes = @($InteractiveControlTypes) + InteractiveDefinition = 'the ControlTypes that map onto the interactive roles the repo ref system already allocates refs for. Container roles that are actionable-but-not-interactive (scrollarea, disclosure) are deliberately excluded from the denominator - they are ref-able because they advertise an action, not because they are interactive.' + WalkLimits = [ordered]@{ NodeBudget = $NodeBudget; MaxDepth = $WalkMaxDepth } + Rows = @($coverage) + ComCrossCheck = @($comRows) + ComCrossCheckNote = 'consumed from captures/08-uia3-com/census.json. The shim serializes at most 60 per-element records per target, so a row is only emitted when the element list covers the whole RawView walk; Explorer and Obsidian are therefore managed-only rows.' + ObsidianVersion = $obsidianVersion + ObsidianPlacementNote = 'the Electron row is measured first, with no other probe window on the desktop, and after an 8 s settle. U3 measured that an Electron tree read behind another window can stay at its first-contact size indefinitely.' + } + [void](Write-ProbeJson -Probe $Probe -Name 'coverage.json' -InputObject $coverageCapture) + + # --- identity arm 1: in-process mutation -------------------------------- + $mutationRows = New-Object System.Collections.ArrayList + $wpfStatusBefore = Get-StatusText -Root $wpf.Element + Invoke-ByAutomationId -Root $wpf.Element -AutomationId 'btnMutateList' + Start-Sleep -Milliseconds 800 + $wpfStatusAfter = Get-StatusText -Root $wpf.Element + $wpfMutated = Get-NodeRecords -Root $wpf.Element + $wpfMutationRow = Compare-Identity -Target 'wpf' -Arm 'mutation' -Before $wpfBaseline -After $wpfMutated ` + -Change 'InvokePattern on btnMutateList: same list change as the WinForms arm. The WPF fixture also derives each item AutomationId from the item text (lstItem-), which is the interesting contrast.' + $wpfMutationRow['MutationVerifiedByObservation'] = ($wpfStatusBefore -ne $wpfStatusAfter) + $wpfMutationRow['StatusTextBefore'] = $wpfStatusBefore + $wpfMutationRow['StatusTextAfter'] = $wpfStatusAfter + [void]$mutationRows.Add($wpfMutationRow) + + $listItemCondition = New-Object System.Windows.Automation.PropertyCondition($AE::ControlTypeProperty, [System.Windows.Automation.ControlType]::ListItem) + $itemsBefore = @($explorerElement.FindAll([System.Windows.Automation.TreeScope]::Descendants, $listItemCondition)).Count + Remove-Item -LiteralPath (Join-Path $script:ExplorerDir 'item-bravo.txt') -Force + [IO.File]::WriteAllText((Join-Path $script:ExplorerDir 'item-foxtrot.txt'), 'item-foxtrot') + [IO.File]::WriteAllText((Join-Path $script:ExplorerDir 'item-golf.txt'), 'item-golf') + $refreshSeconds = -1 + for ($waited = 2; $waited -le 60; $waited += 2) { + Start-Sleep -Seconds 2 + if (@($explorerElement.FindAll([System.Windows.Automation.TreeScope]::Descendants, $listItemCondition)).Count -ne $itemsBefore) { $refreshSeconds = $waited; break } + } + $explorerMutated = Get-NodeRecords -Root $explorerElement + $explorerRow = Compare-Identity -Target 'explorer-folder-window' -Arm 'mutation' -Before $explorerBaseline -After $explorerMutated ` + -Change 'the probe-owned folder loses item-bravo.txt and gains item-foxtrot.txt and item-golf.txt; Explorer refreshes itself from the file-system change notification, with no window, process or input change' + $explorerRow['MutationVerifiedByObservation'] = ($refreshSeconds -ge 0) + $explorerRow['timingRefreshLatencySeconds'] = $refreshSeconds + $explorerRow['RefreshLatencyNote'] = 'measured, not assumed: the folder window took this long 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 is perfectly stable across content mutation" - the exact opposite of what the window does once it refreshes.' + [void]$mutationRows.Add($explorerRow) + + [void](Write-ProbeJson -Probe $Probe -Name 'identity-mutation.json' -InputObject ([ordered]@{ + Probe = $Probe + Question = 'when list content changes inside a live process, which per-node identity properties survive' + Why = "2.5 chooses the Windows RefEntry evidence set. The repo contract is pid, role, path, stable text identity and bounds hash; this arm is the one that shows what a content change does to path, text identity and bounds while pid and role are held constant." + ReRunStability = 'measured over three consecutive runs on this box: every field here reproduces exactly except the Explorer RuntimeId survival count, which moved between 59/82 and 60/82 - one DirectUI Edit node whose RuntimeId is regenerated or not depending on how the shell refresh lands. That is real platform nondeterminism in the property, not probe noise, and under KTD9 a non-empty normalized diff on this one number is the signal a later re-runner should see rather than something to smooth away. All other captures in this probe were byte-identical across runs.' + WinFormsNotHere = 'the WinForms fixture has no in-process arm because no managed-visible affordance exists to drive it: in --host-providers mode the managed client sees the whole form as Panes with numeric control ids and btnMutateList advertises no InvokePattern, and in default mode the custom provider suppresses patterns entirely. Its list change is measured in identity-restart.json as the restart+mutation row against the pure restart row as control. Driving it any other way would need SendInput or PostMessage, which belongs to U6.' + RuntimeIdHandling = 'RuntimeId is compared in memory and only survived/changed counts are written. The KTD9 normalizer rewrites any literal RuntimeId to , so a capture holding raw ids would normalize away the exact evidence this arm produces.' + NameHandling = 'Name is compared in memory and only equality counts are written. No Name value from any target reaches a capture, so the R11 content rule is satisfied by construction rather than by redaction.' + Rows = @($mutationRows) + })) + + # --- identity arm 2: restart ------------------------------------------- + $restartRows = New-Object System.Collections.ArrayList + Stop-ScratchProcess -ProcessId $winHosted.ProcessId + Start-Sleep -Milliseconds 500 + $winRestarted = Start-Tracked -FilePath $scratchExe -ArgumentList $winHostedArgs + if ($winRestarted.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms host-providers window never reappeared after restart' } + Start-Sleep -Milliseconds 800 + [void]$restartRows.Add((Compare-Identity -Target 'winforms-host-providers' -Arm 'restart' ` + -Before $winBaseline -After (Get-NodeRecords -Root ($AE::FromHandle($winRestarted.MainWindowHandle))) ` + -Change 'process terminated and relaunched with identical arguments and the identical --pos origin; the list is back at its baseline content')) + + Stop-ScratchProcess -ProcessId $wpf.ProcessId + if ($wpf.LauncherProcessId -ne $wpf.ProcessId) { try { Stop-ScratchProcess -ProcessId $wpf.LauncherProcessId } catch { } } + Start-Sleep -Milliseconds 500 + $wpfRestarted = Start-ScratchWpfWindow -Tag 'u4-identity' -Left $Pos.Wpf.X -Top $Pos.Wpf.Y + if ($null -eq $wpfRestarted) { throw 'WPF scratch window never bound its automation peer after restart' } + [void]$restartRows.Add((Compare-Identity -Target 'wpf' -Arm 'restart' ` + -Before $wpfBaseline -After (Get-NodeRecords -Root $wpfRestarted.Element) ` + -Change 'process terminated and relaunched with identical -Left/-Top arguments; the peer-activation settle is applied again because a WPF window read too early binds the HWND fallback provider permanently (03-pattern-census)')) + + Stop-ScratchProcess -ProcessId $notepad.ProcessId + Start-Sleep -Milliseconds 500 + $notepadRestarted = Start-Tracked -FilePath (Join-Path $env:WINDIR 'System32\notepad.exe') + if ($notepadRestarted.MainWindowHandle -eq [IntPtr]::Zero) { throw 'notepad window never reappeared after restart' } + Show-WindowNoActivate -WindowHandle $notepadRestarted.MainWindowHandle -X $Pos.Notepad.X -Y $Pos.Notepad.Y -Width $Pos.Notepad.Width -Height $Pos.Notepad.Height + Start-Sleep -Milliseconds 700 + [void]$restartRows.Add((Compare-Identity -Target 'notepad' -Arm 'restart' ` + -Before $notepadBaseline -After (Get-NodeRecords -Root ($AE::FromHandle($notepadRestarted.MainWindowHandle))) ` + -Change 'the real Win32 target: process terminated and relaunched, then placed at the identical rect with SetWindowPos so window placement cannot masquerade as layout drift')) + + Stop-ScratchProcess -ProcessId $winRestarted.ProcessId + Start-Sleep -Milliseconds 500 + $winMutatedRestart = Start-Tracked -FilePath $scratchExe -ArgumentList (@($winHostedArgs) + @('--mutate-list')) + if ($winMutatedRestart.MainWindowHandle -eq [IntPtr]::Zero) { throw 'WinForms host-providers window never reappeared with --mutate-list' } + Start-Sleep -Milliseconds 800 + $winMutateRow = Compare-Identity -Target 'winforms-host-providers' -Arm 'restart+mutation' ` + -Before $winBaseline -After (Get-NodeRecords -Root ($AE::FromHandle($winMutatedRestart.MainWindowHandle))) ` + -Change 'relaunched with --mutate-list added: same origin, same arguments otherwise, but the list starts as Alpha/Charlie/Delta/Echo/Foxtrot/Golf. Its control is the pure restart row above; any survival difference between the two rows is attributable to the list change alone.' + $winMutateRow['StatusText'] = (Get-StatusText -Root ($AE::FromHandle($winMutatedRestart.MainWindowHandle)) -AutomationId '1020') + [void]$restartRows.Add($winMutateRow) + + [void](Write-ProbeJson -Probe $Probe -Name 'identity-restart.json' -InputObject ([ordered]@{ + Probe = $Probe + Question = 'when a process is restarted, which per-node identity properties survive' + PlacementControl = 'every target is relaunched at the identical window origin (scratch fixtures via --pos / -Left -Top, Notepad via SetWindowPos with the same rect). Without that control every bounds comparison would report 0% survival for the trivial reason that Windows cascades a new window, and the interesting question - whether layout inside the window is reproducible - would be unanswerable.' + RuntimeIdHandling = 'as in identity-mutation.json: compared in memory, only survived/changed counts written.' + Rows = @($restartRows) + })) + + $summaryRows = @() + foreach ($r in @($mutationRows + $restartRows)) { + $cells = @() + foreach ($p in $r.PerPropertySurvival) { $cells += ($p.Property + ':' + $p.PctSurvived) } + $summaryRows += ('target=' + $r.Target + ' arm=' + $r.Arm + ' matched=' + $r.PathMatchedPairs + ' pct=' + ($cells -join ',')) + } + $resultData['coverageRows'] = @($coverage).Count + $resultData['identityRows'] = @($mutationRows).Count + @($restartRows).Count + foreach ($row in $coverage) { $resultData[('pctInteractiveWithId_' + $row.Target)] = $row.PctInteractiveElements } + $resultData['survival'] = ($summaryRows -join ' ; ') + $message = 'automationid coverage over ' + @($coverage).Count + ' stack rows; identity survival over ' + $resultData['identityRows'] + ' arms' +} catch { + $status = 'fail' + $message = ($_.Exception.Message -replace '[\r\n]+', ' ') + Write-ProbeLog -Message ('probe failed: ' + $message) -Level 'error' +} finally { + if ($script:ExplorerHandle -ne 0) { + try { [void](Close-ShellWindowByHandle -Handle $script:ExplorerHandle) } catch { Write-ProbeLog -Message ('could not close folder window: ' + $_.Exception.Message) -Level 'warn' } + } + if ($script:ObsidianLaunched) { + foreach ($p in @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue)) { try { Stop-ScratchProcess -ProcessId $p.Id } catch { } } + } + foreach ($id in @($script:Spawned)) { + try { Stop-ScratchProcess -ProcessId $id } catch { Write-ProbeLog -Message ('teardown: ' + $_.Exception.Message) -Level 'warn' } + } + if ($script:ExplorerDir -and (Test-Path -LiteralPath $script:ExplorerDir)) { + try { Remove-Item -LiteralPath $script:ExplorerDir -Recurse -Force } catch { } + } + foreach ($f in @(Get-ChildItem -LiteralPath (Get-CaptureDir -Probe $Probe) -File -ErrorAction SilentlyContinue)) { + if ($f.Name -like '*.normalized') { continue } + if (-not (Test-CaptureRedaction -Path $f.FullName)) { $status = 'fail'; $message = ('redaction residue in ' + $f.Name) } + } +} + +Write-ProbeResult -Probe $Probe -Status $status -Message $message -Data $resultData +if ($status -eq 'fail') { exit 1 } +exit 0 diff --git a/probes/windows/captures/03-pattern-census/divergence.json b/probes/windows/captures/03-pattern-census/divergence.json new file mode 100644 index 0000000..7e2e40d --- /dev/null +++ b/probes/windows/captures/03-pattern-census/divergence.json @@ -0,0 +1,298 @@ +{ + "Probe": "03-pattern-census", + "Question": "does a managed pattern census record the same availability the Rust adapter will see through UIA3 COM", + "ComparisonBase": "COM occurrences summed across the four census targets in captures/08-uia3-com/census.json; managed occurrences summed across the four fresh sweeps in managed-crosscheck.json", + "ManagedPatternClasses": [ + "Dock", + "ExpandCollapse", + "Grid", + "GridItem", + "Invoke", + "ItemContainer", + "MultipleView", + "RangeValue", + "Scroll", + "ScrollItem", + "Selection", + "SelectionItem", + "SynchronizedInput", + "Table", + "TableItem", + "Text", + "Toggle", + "Transform", + "Value", + "VirtualizedItem", + "Window" + ], + "ManagedPatternClassCount": 21, + "ManagedVocabularyNote": "ManagedPatternClasses is reflected out of the UIAutomationTypes assembly at run time (public types named *PatternIdentifiers), not transcribed. It is the complete set of pattern names System.Windows.Automation can ever return, so a COM-observed pattern outside this set is a structural blind spot rather than a sampling gap. The two cases are separated in every row: ExpressibleByManagedStack=false is structural, ExpressibleByManagedStack=true with zero managed occurrences only means the smaller managed tree did not reach an element advertising it.", + "Rows": [ + { + "Pattern": "ExpandCollapse", + "ExpressibleByManagedStack": true, + "ComOccurrences": 13, + "ManagedOccurrences": 2, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-host-providers", + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Invoke", + "ExpressibleByManagedStack": true, + "ComOccurrences": 48, + "ManagedOccurrences": 3, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-host-providers", + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "ItemContainer", + "ExpressibleByManagedStack": true, + "ComOccurrences": 2, + "ManagedOccurrences": 2, + "ComTargets": [ + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "LegacyIAccessible", + "ExpressibleByManagedStack": false, + "ComOccurrences": 141, + "ManagedOccurrences": 0, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + + ], + "Verdict": "COM only - the managed stack has no identifier for this pattern and can never report it" + }, + { + "Pattern": "RangeValue", + "ExpressibleByManagedStack": true, + "ComOccurrences": 8, + "ManagedOccurrences": 4, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Scroll", + "ExpressibleByManagedStack": true, + "ComOccurrences": 8, + "ManagedOccurrences": 5, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "ScrollItem", + "ExpressibleByManagedStack": true, + "ComOccurrences": 12, + "ManagedOccurrences": 5, + "ComTargets": [ + "winforms-host-providers", + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Selection", + "ExpressibleByManagedStack": true, + "ComOccurrences": 4, + "ManagedOccurrences": 2, + "ComTargets": [ + "winforms-host-providers", + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "SelectionItem", + "ExpressibleByManagedStack": true, + "ComOccurrences": 12, + "ManagedOccurrences": 5, + "ComTargets": [ + "winforms-host-providers", + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "SynchronizedInput", + "ExpressibleByManagedStack": true, + "ComOccurrences": 28, + "ManagedOccurrences": 28, + "ComTargets": [ + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Text", + "ExpressibleByManagedStack": true, + "ComOccurrences": 3, + "ManagedOccurrences": 2, + "ComTargets": [ + "wpf", + "notepad" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Text2", + "ExpressibleByManagedStack": false, + "ComOccurrences": 1, + "ManagedOccurrences": 0, + "ComTargets": [ + "notepad" + ], + "ManagedTargets": [ + + ], + "Verdict": "COM only - the managed stack has no identifier for this pattern and can never report it" + }, + { + "Pattern": "Toggle", + "ExpressibleByManagedStack": true, + "ComOccurrences": 2, + "ManagedOccurrences": 1, + "ComTargets": [ + "winforms-host-providers", + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Transform", + "ExpressibleByManagedStack": true, + "ComOccurrences": 4, + "ManagedOccurrences": 4, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Value", + "ExpressibleByManagedStack": true, + "ComOccurrences": 11, + "ManagedOccurrences": 3, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-host-providers", + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Window", + "ExpressibleByManagedStack": true, + "ComOccurrences": 4, + "ManagedOccurrences": 4, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "Verdict": "both stacks report it" + } + ], + "ComOnlyPatterns": [ + "LegacyIAccessible", + "Text2" + ], + "StructurallyInvisibleToManaged": [ + "LegacyIAccessible", + "Text2" + ], + "LegacyIAccessibleRow": { + "ComElementsReportingIt": 141, + "ManagedElementsReportingIt": 0, + "ComElementsTotal": 141, + "Verdict": "CONFIRMS KTD1", + "Finding": "LegacyIAccessible is advertised by every element the COM census walked and by zero elements the managed sweep walked. The managed absence is structural, not environmental: System.Windows.Automation exposes 22 pattern classes and has no LegacyIAccessible identifier to return, so GetSupportedPatterns() can never name it. A managed-only census would have filed that as \"pattern unavailable\" for the whole corpus.", + "Consequence": "every pattern-availability row in this sub-phase must carry stack=uia3-com. A Windows adapter that decides actionability from a managed-derived pattern table would under-report actionable elements on exactly the legacy Win32 surfaces where LegacyIAccessible.DoDefaultAction is the only affordance." + }, + "NotepadDocumentRow": { + "Finding": "the same divergence shows up as a ControlType disagreement, not only a pattern one: classic Notepad edit surface is ControlType.Pane with AutomationId 15 to the managed client (U3, 01-tree-dump) and ControlType.Document with Value, Text, Text2, Scroll, LegacyIAccessible to the COM client. Text2 (30119) is another pattern the managed stack cannot name.", + "Verdict": "NEW-EDGE" + } +} \ No newline at end of file diff --git a/probes/windows/captures/03-pattern-census/divergence.json.normalized b/probes/windows/captures/03-pattern-census/divergence.json.normalized new file mode 100644 index 0000000..7e2e40d --- /dev/null +++ b/probes/windows/captures/03-pattern-census/divergence.json.normalized @@ -0,0 +1,298 @@ +{ + "Probe": "03-pattern-census", + "Question": "does a managed pattern census record the same availability the Rust adapter will see through UIA3 COM", + "ComparisonBase": "COM occurrences summed across the four census targets in captures/08-uia3-com/census.json; managed occurrences summed across the four fresh sweeps in managed-crosscheck.json", + "ManagedPatternClasses": [ + "Dock", + "ExpandCollapse", + "Grid", + "GridItem", + "Invoke", + "ItemContainer", + "MultipleView", + "RangeValue", + "Scroll", + "ScrollItem", + "Selection", + "SelectionItem", + "SynchronizedInput", + "Table", + "TableItem", + "Text", + "Toggle", + "Transform", + "Value", + "VirtualizedItem", + "Window" + ], + "ManagedPatternClassCount": 21, + "ManagedVocabularyNote": "ManagedPatternClasses is reflected out of the UIAutomationTypes assembly at run time (public types named *PatternIdentifiers), not transcribed. It is the complete set of pattern names System.Windows.Automation can ever return, so a COM-observed pattern outside this set is a structural blind spot rather than a sampling gap. The two cases are separated in every row: ExpressibleByManagedStack=false is structural, ExpressibleByManagedStack=true with zero managed occurrences only means the smaller managed tree did not reach an element advertising it.", + "Rows": [ + { + "Pattern": "ExpandCollapse", + "ExpressibleByManagedStack": true, + "ComOccurrences": 13, + "ManagedOccurrences": 2, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-host-providers", + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Invoke", + "ExpressibleByManagedStack": true, + "ComOccurrences": 48, + "ManagedOccurrences": 3, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-host-providers", + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "ItemContainer", + "ExpressibleByManagedStack": true, + "ComOccurrences": 2, + "ManagedOccurrences": 2, + "ComTargets": [ + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "LegacyIAccessible", + "ExpressibleByManagedStack": false, + "ComOccurrences": 141, + "ManagedOccurrences": 0, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + + ], + "Verdict": "COM only - the managed stack has no identifier for this pattern and can never report it" + }, + { + "Pattern": "RangeValue", + "ExpressibleByManagedStack": true, + "ComOccurrences": 8, + "ManagedOccurrences": 4, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Scroll", + "ExpressibleByManagedStack": true, + "ComOccurrences": 8, + "ManagedOccurrences": 5, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "ScrollItem", + "ExpressibleByManagedStack": true, + "ComOccurrences": 12, + "ManagedOccurrences": 5, + "ComTargets": [ + "winforms-host-providers", + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Selection", + "ExpressibleByManagedStack": true, + "ComOccurrences": 4, + "ManagedOccurrences": 2, + "ComTargets": [ + "winforms-host-providers", + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "SelectionItem", + "ExpressibleByManagedStack": true, + "ComOccurrences": 12, + "ManagedOccurrences": 5, + "ComTargets": [ + "winforms-host-providers", + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "SynchronizedInput", + "ExpressibleByManagedStack": true, + "ComOccurrences": 28, + "ManagedOccurrences": 28, + "ComTargets": [ + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Text", + "ExpressibleByManagedStack": true, + "ComOccurrences": 3, + "ManagedOccurrences": 2, + "ComTargets": [ + "wpf", + "notepad" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Text2", + "ExpressibleByManagedStack": false, + "ComOccurrences": 1, + "ManagedOccurrences": 0, + "ComTargets": [ + "notepad" + ], + "ManagedTargets": [ + + ], + "Verdict": "COM only - the managed stack has no identifier for this pattern and can never report it" + }, + { + "Pattern": "Toggle", + "ExpressibleByManagedStack": true, + "ComOccurrences": 2, + "ManagedOccurrences": 1, + "ComTargets": [ + "winforms-host-providers", + "wpf" + ], + "ManagedTargets": [ + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Transform", + "ExpressibleByManagedStack": true, + "ComOccurrences": 4, + "ManagedOccurrences": 4, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Value", + "ExpressibleByManagedStack": true, + "ComOccurrences": 11, + "ManagedOccurrences": 3, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-host-providers", + "wpf" + ], + "Verdict": "both stacks report it" + }, + { + "Pattern": "Window", + "ExpressibleByManagedStack": true, + "ComOccurrences": 4, + "ManagedOccurrences": 4, + "ComTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "ManagedTargets": [ + "winforms-default", + "winforms-host-providers", + "wpf", + "notepad" + ], + "Verdict": "both stacks report it" + } + ], + "ComOnlyPatterns": [ + "LegacyIAccessible", + "Text2" + ], + "StructurallyInvisibleToManaged": [ + "LegacyIAccessible", + "Text2" + ], + "LegacyIAccessibleRow": { + "ComElementsReportingIt": 141, + "ManagedElementsReportingIt": 0, + "ComElementsTotal": 141, + "Verdict": "CONFIRMS KTD1", + "Finding": "LegacyIAccessible is advertised by every element the COM census walked and by zero elements the managed sweep walked. The managed absence is structural, not environmental: System.Windows.Automation exposes 22 pattern classes and has no LegacyIAccessible identifier to return, so GetSupportedPatterns() can never name it. A managed-only census would have filed that as \"pattern unavailable\" for the whole corpus.", + "Consequence": "every pattern-availability row in this sub-phase must carry stack=uia3-com. A Windows adapter that decides actionability from a managed-derived pattern table would under-report actionable elements on exactly the legacy Win32 surfaces where LegacyIAccessible.DoDefaultAction is the only affordance." + }, + "NotepadDocumentRow": { + "Finding": "the same divergence shows up as a ControlType disagreement, not only a pattern one: classic Notepad edit surface is ControlType.Pane with AutomationId 15 to the managed client (U3, 01-tree-dump) and ControlType.Document with Value, Text, Text2, Scroll, LegacyIAccessible to the COM client. Text2 (30119) is another pattern the managed stack cannot name.", + "Verdict": "NEW-EDGE" + } +} \ No newline at end of file diff --git a/probes/windows/captures/03-pattern-census/managed-crosscheck.json b/probes/windows/captures/03-pattern-census/managed-crosscheck.json new file mode 100644 index 0000000..93db2b2 --- /dev/null +++ b/probes/windows/captures/03-pattern-census/managed-crosscheck.json @@ -0,0 +1,67 @@ +{ + "Probe": "03-pattern-census", + "Stack": "managed-System.Windows.Automation", + "Authoritative": false, + "Scope": "app/provider", + "Method": "fresh RawViewWalker sweep over the same four fixture shapes, calling AutomationElement.GetSupportedPatterns() per element and reducing each AutomationPattern ProgrammaticName by stripping the PatternIdentifiers.Pattern suffix", + "WhyNotAuthoritative": "KTD1: recorded only so the COM-vs-managed pattern vocabulary can be diffed. Where a managed row disagrees with the COM matrix the COM row is the product-relevant one, because the Rust adapter wraps a UIA3 COM client.", + "SameRunCaveat": "these windows are fresh instances launched by this probe, not the HWNDs U7 measured. That does not weaken the divergence row: the claim under test is which pattern names a client stack can express at all, which is a property of the client library, not of a window instance. U7 comparison.json already holds the same-run, same-HWND node-count comparison.", + "WpfPeerActivation": { + "Verdict": "NEW-EDGE", + "ClassNameShapeAfterActivation": "Window", + "timingPeerActivationLaunches": 1, + "Observation": "if a UIA client reads the WPF window before WPF has built its automation peer, the client binds the generic HWND provider for that window and never re-resolves it. The window then reports ControlType.Window with ClassName HwndWrapper[\u003cappdomain\u003e;\u003cthread name\u003e;\u003cguid\u003e] and ZERO children, and stays that way. Measured: a 30 s poll loop in the same client process, including AutomationElement.FromHandle and a FindAll(Descendants, TrueCondition) forced traversal on every pass, never recovered the tree. The same fixture read for the first time 8 s after launch reports ClassName=Window with 8 children immediately.", + "Consequence": "a Windows snapshot taken immediately after launching a WPF app can return a permanently empty one-node tree, and retrying inside the same process cannot fix it - the adapter would have to re-resolve from a new HWND or a new client. This is a different failure mode from the Electron settle U3 measured: Electron under-reports and then grows, WPF binds the wrong provider and stays wrong.", + "ProbePolicy": "this probe launches the WPF fixture first, settles before its first UIA touch, verifies the root has children, and relaunches on a fresh HWND if it does not. timingPeerActivationLaunches records how many launches that took on this run.", + "ClassNameCorollary": "the two shapes ClassName takes (Window when the peer is bound, HwndWrapper[...;\u003cguid\u003e] when it is not) also disqualify ClassName as an element-identity component on WPF: one of them embeds a per-launch GUID." + }, + "MatrixRecordFormat": "one row per line, space separated key=value fields in fixed order: target=\u003ccensus target label\u003e ct=\u003cUIA ControlType short name\u003e n=\u003celements of that ControlType in the RawView walk\u003e withpat=\u003chow many of them advertise at least one pattern\u003e pats=\u003cName:count comma separated, \"-\" when none\u003e. Counts are element occurrences, not distinct patterns.", + "Targets": [ + { + "Label": "winforms-default", + "RawViewNodes": 24, + "ElementsWithAnyPattern": 1, + "PatternTotals": "Transform:1,Window:1" + }, + { + "Label": "winforms-host-providers", + "RawViewNodes": 26, + "ElementsWithAnyPattern": 3, + "PatternTotals": "ExpandCollapse:1,Invoke:1,Transform:1,Value:1,Window:1" + }, + { + "Label": "wpf", + "RawViewNodes": 28, + "ElementsWithAnyPattern": 28, + "PatternTotals": "ExpandCollapse:1,Invoke:2,ItemContainer:2,RangeValue:4,Scroll:5,ScrollItem:5,Selection:2,SelectionItem:5,SynchronizedInput:28,Text:2,Toggle:1,Transform:1,Value:2,Window:1" + }, + { + "Label": "notepad", + "RawViewNodes": 3, + "ElementsWithAnyPattern": 1, + "PatternTotals": "Transform:1,Window:1" + } + ], + "Rows": [ + "target=winforms-default ct=Pane n=23 withpat=0 pats=-", + "target=winforms-default ct=Window n=1 withpat=1 pats=Transform:1,Window:1", + "target=winforms-host-providers ct=Button n=1 withpat=1 pats=Invoke:1", + "target=winforms-host-providers ct=ComboBox n=1 withpat=1 pats=ExpandCollapse:1,Value:1", + "target=winforms-host-providers ct=Pane n=17 withpat=0 pats=-", + "target=winforms-host-providers ct=Text n=6 withpat=0 pats=-", + "target=winforms-host-providers ct=Window n=1 withpat=1 pats=Transform:1,Window:1", + "target=wpf ct=Button n=2 withpat=2 pats=Invoke:2,SynchronizedInput:2", + "target=wpf ct=CheckBox n=1 withpat=1 pats=SynchronizedInput:1,Toggle:1", + "target=wpf ct=ComboBox n=1 withpat=1 pats=ExpandCollapse:1,ItemContainer:1,Selection:1,SynchronizedInput:1", + "target=wpf ct=Edit n=2 withpat=2 pats=Scroll:2,SynchronizedInput:2,Text:2,Value:2", + "target=wpf ct=List n=1 withpat=1 pats=ItemContainer:1,Scroll:1,Selection:1,SynchronizedInput:1", + "target=wpf ct=ListItem n=5 withpat=5 pats=ScrollItem:5,SelectionItem:5,SynchronizedInput:5", + "target=wpf ct=Pane n=2 withpat=2 pats=Scroll:2,SynchronizedInput:2", + "target=wpf ct=ScrollBar n=4 withpat=4 pats=RangeValue:4,SynchronizedInput:4", + "target=wpf ct=Text n=9 withpat=9 pats=SynchronizedInput:9", + "target=wpf ct=Window n=1 withpat=1 pats=SynchronizedInput:1,Transform:1,Window:1", + "target=notepad ct=Pane n=2 withpat=0 pats=-", + "target=notepad ct=Window n=1 withpat=1 pats=Transform:1,Window:1" + ], + "NodeBudget": 400 +} \ No newline at end of file diff --git a/probes/windows/captures/03-pattern-census/managed-crosscheck.json.normalized b/probes/windows/captures/03-pattern-census/managed-crosscheck.json.normalized new file mode 100644 index 0000000..43f24f5 --- /dev/null +++ b/probes/windows/captures/03-pattern-census/managed-crosscheck.json.normalized @@ -0,0 +1,67 @@ +{ + "Probe": "03-pattern-census", + "Stack": "managed-System.Windows.Automation", + "Authoritative": false, + "Scope": "app/provider", + "Method": "fresh RawViewWalker sweep over the same four fixture shapes, calling AutomationElement.GetSupportedPatterns() per element and reducing each AutomationPattern ProgrammaticName by stripping the PatternIdentifiers.Pattern suffix", + "WhyNotAuthoritative": "KTD1: recorded only so the COM-vs-managed pattern vocabulary can be diffed. Where a managed row disagrees with the COM matrix the COM row is the product-relevant one, because the Rust adapter wraps a UIA3 COM client.", + "SameRunCaveat": "these windows are fresh instances launched by this probe, not the HWNDs U7 measured. That does not weaken the divergence row: the claim under test is which pattern names a client stack can express at all, which is a property of the client library, not of a window instance. U7 comparison.json already holds the same-run, same-HWND node-count comparison.", + "WpfPeerActivation": { + "Verdict": "NEW-EDGE", + "ClassNameShapeAfterActivation": "Window", + "timingPeerActivationLaunches": , + "Observation": "if a UIA client reads the WPF window before WPF has built its automation peer, the client binds the generic HWND provider for that window and never re-resolves it. The window then reports ControlType.Window with ClassName HwndWrapper[\u003cappdomain\u003e;\u003cthread name\u003e;\u003cguid\u003e] and ZERO children, and stays that way. Measured: a 30 s poll loop in the same client process, including AutomationElement.FromHandle and a FindAll(Descendants, TrueCondition) forced traversal on every pass, never recovered the tree. The same fixture read for the first time 8 s after launch reports ClassName=Window with 8 children immediately.", + "Consequence": "a Windows snapshot taken immediately after launching a WPF app can return a permanently empty one-node tree, and retrying inside the same process cannot fix it - the adapter would have to re-resolve from a new HWND or a new client. This is a different failure mode from the Electron settle U3 measured: Electron under-reports and then grows, WPF binds the wrong provider and stays wrong.", + "ProbePolicy": "this probe launches the WPF fixture first, settles before its first UIA touch, verifies the root has children, and relaunches on a fresh HWND if it does not. timingPeerActivationLaunches records how many launches that took on this run.", + "ClassNameCorollary": "the two shapes ClassName takes (Window when the peer is bound, HwndWrapper[...;\u003cguid\u003e] when it is not) also disqualify ClassName as an element-identity component on WPF: one of them embeds a per-launch GUID." + }, + "MatrixRecordFormat": "one row per line, space separated key=value fields in fixed order: target=\u003ccensus target label\u003e ct=\u003cUIA ControlType short name\u003e n=\u003celements of that ControlType in the RawView walk\u003e withpat=\u003chow many of them advertise at least one pattern\u003e pats=\u003cName:count comma separated, \"-\" when none\u003e. Counts are element occurrences, not distinct patterns.", + "Targets": [ + { + "Label": "winforms-default", + "RawViewNodes": 24, + "ElementsWithAnyPattern": 1, + "PatternTotals": "Transform:1,Window:1" + }, + { + "Label": "winforms-host-providers", + "RawViewNodes": 26, + "ElementsWithAnyPattern": 3, + "PatternTotals": "ExpandCollapse:1,Invoke:1,Transform:1,Value:1,Window:1" + }, + { + "Label": "wpf", + "RawViewNodes": 28, + "ElementsWithAnyPattern": 28, + "PatternTotals": "ExpandCollapse:1,Invoke:2,ItemContainer:2,RangeValue:4,Scroll:5,ScrollItem:5,Selection:2,SelectionItem:5,SynchronizedInput:28,Text:2,Toggle:1,Transform:1,Value:2,Window:1" + }, + { + "Label": "notepad", + "RawViewNodes": 3, + "ElementsWithAnyPattern": 1, + "PatternTotals": "Transform:1,Window:1" + } + ], + "Rows": [ + "target=winforms-default ct=Pane n=23 withpat=0 pats=-", + "target=winforms-default ct=Window n=1 withpat=1 pats=Transform:1,Window:1", + "target=winforms-host-providers ct=Button n=1 withpat=1 pats=Invoke:1", + "target=winforms-host-providers ct=ComboBox n=1 withpat=1 pats=ExpandCollapse:1,Value:1", + "target=winforms-host-providers ct=Pane n=17 withpat=0 pats=-", + "target=winforms-host-providers ct=Text n=6 withpat=0 pats=-", + "target=winforms-host-providers ct=Window n=1 withpat=1 pats=Transform:1,Window:1", + "target=wpf ct=Button n=2 withpat=2 pats=Invoke:2,SynchronizedInput:2", + "target=wpf ct=CheckBox n=1 withpat=1 pats=SynchronizedInput:1,Toggle:1", + "target=wpf ct=ComboBox n=1 withpat=1 pats=ExpandCollapse:1,ItemContainer:1,Selection:1,SynchronizedInput:1", + "target=wpf ct=Edit n=2 withpat=2 pats=Scroll:2,SynchronizedInput:2,Text:2,Value:2", + "target=wpf ct=List n=1 withpat=1 pats=ItemContainer:1,Scroll:1,Selection:1,SynchronizedInput:1", + "target=wpf ct=ListItem n=5 withpat=5 pats=ScrollItem:5,SelectionItem:5,SynchronizedInput:5", + "target=wpf ct=Pane n=2 withpat=2 pats=Scroll:2,SynchronizedInput:2", + "target=wpf ct=ScrollBar n=4 withpat=4 pats=RangeValue:4,SynchronizedInput:4", + "target=wpf ct=Text n=9 withpat=9 pats=SynchronizedInput:9", + "target=wpf ct=Window n=1 withpat=1 pats=SynchronizedInput:1,Transform:1,Window:1", + "target=notepad ct=Pane n=2 withpat=0 pats=-", + "target=notepad ct=Window n=1 withpat=1 pats=Transform:1,Window:1" + ], + "NodeBudget": 400 +} \ No newline at end of file diff --git a/probes/windows/captures/03-pattern-census/pattern-matrix.json b/probes/windows/captures/03-pattern-census/pattern-matrix.json new file mode 100644 index 0000000..04a30b2 --- /dev/null +++ b/probes/windows/captures/03-pattern-census/pattern-matrix.json @@ -0,0 +1,181 @@ +{ + "Probe": "03-pattern-census", + "Stack": "uia3-com", + "Authoritative": true, + "Scope": "app/provider", + "Source": "captures/08-uia3-com/census.json, produced by U7 shim mode \"census\" over hand-declared ComImport UIA3 (CUIAutomation8); reshaped here, not re-measured", + "SourceRationale": "KTD1 puts pattern availability on the COM stack because the managed stack structurally cannot name LegacyIAccessible, Drag, DropTarget, Annotation, Styles or TextChild. Re-walking the same four targets a second time would add a second sample of the same fact and a second chance for drift, so this probe consumes the committed measurement.", + "View": "RawView walk (shim census mode); inControlView is recorded per element in the source capture", + "MatrixRecordFormat": "one row per line, space separated key=value fields in fixed order: target=\u003ccensus target label\u003e ct=\u003cUIA ControlType short name\u003e n=\u003celements of that ControlType in the RawView walk\u003e withpat=\u003chow many of them advertise at least one pattern\u003e pats=\u003cName:count comma separated, \"-\" when none\u003e. Counts are element occurrences, not distinct patterns.", + "Targets": [ + { + "Label": "winforms-default", + "ProcessName": "ScratchForms", + "RootFrameworkId": "WinForm", + "ControlViewNodes": 35, + "RawViewNodes": 35, + "ElementsWithAnyPattern": 35, + "ElementsWithAnyPatternExcludingLegacy": 11 + }, + { + "Label": "winforms-host-providers", + "ProcessName": "ScratchForms", + "RootFrameworkId": "WinForm", + "ControlViewNodes": 46, + "RawViewNodes": 46, + "ElementsWithAnyPattern": 46, + "ElementsWithAnyPatternExcludingLegacy": 37 + }, + { + "Label": "wpf", + "ProcessName": "powershell", + "RootFrameworkId": "WPF", + "ControlViewNodes": 32, + "RawViewNodes": 34, + "ElementsWithAnyPattern": 34, + "ElementsWithAnyPatternExcludingLegacy": 33 + }, + { + "Label": "notepad", + "ProcessName": "notepad", + "RootFrameworkId": "Win32", + "ControlViewNodes": 26, + "RawViewNodes": 26, + "ElementsWithAnyPattern": 26, + "ElementsWithAnyPatternExcludingLegacy": 18 + } + ], + "PatternTotalsByTarget": { + "winforms-default": "ExpandCollapse:1,Invoke:6,LegacyIAccessible:35,RangeValue:1,Scroll:1,Transform:1,Value:1,Window:1", + "winforms-host-providers": "ExpandCollapse:4,Invoke:24,LegacyIAccessible:46,RangeValue:1,Scroll:1,ScrollItem:7,Selection:2,SelectionItem:7,Toggle:1,Transform:1,Value:5,Window:1", + "wpf": "ExpandCollapse:2,Invoke:5,ItemContainer:2,LegacyIAccessible:34,RangeValue:4,Scroll:5,ScrollItem:5,Selection:2,SelectionItem:5,SynchronizedInput:28,Text:2,Toggle:1,Transform:1,Value:3,Window:1", + "notepad": "ExpandCollapse:6,Invoke:13,LegacyIAccessible:26,RangeValue:2,Scroll:1,Text:1,Text2:1,Transform:1,Value:2,Window:1" + }, + "Rows": [ + "target=winforms-default ct=Button n=6 withpat=6 pats=Invoke:6,LegacyIAccessible:6", + "target=winforms-default ct=MenuBar n=1 withpat=1 pats=LegacyIAccessible:1", + "target=winforms-default ct=MenuItem n=1 withpat=1 pats=ExpandCollapse:1,LegacyIAccessible:1", + "target=winforms-default ct=Pane n=23 withpat=23 pats=LegacyIAccessible:23,Scroll:1", + "target=winforms-default ct=ScrollBar n=1 withpat=1 pats=LegacyIAccessible:1,RangeValue:1", + "target=winforms-default ct=Thumb n=1 withpat=1 pats=LegacyIAccessible:1", + "target=winforms-default ct=TitleBar n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=winforms-default ct=Window n=1 withpat=1 pats=LegacyIAccessible:1,Transform:1,Window:1", + "target=winforms-host-providers ct=Button n=18 withpat=18 pats=Invoke:18,LegacyIAccessible:18", + "target=winforms-host-providers ct=CheckBox n=1 withpat=1 pats=Invoke:1,LegacyIAccessible:1,Toggle:1", + "target=winforms-host-providers ct=ComboBox n=1 withpat=1 pats=ExpandCollapse:1,LegacyIAccessible:1,Value:1", + "target=winforms-host-providers ct=Edit n=2 withpat=2 pats=LegacyIAccessible:2,Value:2", + "target=winforms-host-providers ct=List n=1 withpat=1 pats=LegacyIAccessible:1,Selection:1", + "target=winforms-host-providers ct=ListItem n=5 withpat=5 pats=Invoke:5,LegacyIAccessible:5,ScrollItem:5,SelectionItem:5", + "target=winforms-host-providers ct=MenuBar n=1 withpat=1 pats=LegacyIAccessible:1", + "target=winforms-host-providers ct=MenuItem n=1 withpat=1 pats=ExpandCollapse:1,LegacyIAccessible:1", + "target=winforms-host-providers ct=Pane n=1 withpat=1 pats=LegacyIAccessible:1,Scroll:1", + "target=winforms-host-providers ct=ScrollBar n=1 withpat=1 pats=LegacyIAccessible:1,RangeValue:1", + "target=winforms-host-providers ct=Slider n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=winforms-host-providers ct=Text n=6 withpat=6 pats=LegacyIAccessible:6", + "target=winforms-host-providers ct=Thumb n=2 withpat=2 pats=LegacyIAccessible:2", + "target=winforms-host-providers ct=TitleBar n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=winforms-host-providers ct=Tree n=1 withpat=1 pats=LegacyIAccessible:1,Selection:1", + "target=winforms-host-providers ct=TreeItem n=2 withpat=2 pats=ExpandCollapse:2,LegacyIAccessible:2,ScrollItem:2,SelectionItem:2", + "target=winforms-host-providers ct=Window n=1 withpat=1 pats=LegacyIAccessible:1,Transform:1,Window:1", + "target=wpf ct=Button n=5 withpat=5 pats=Invoke:5,LegacyIAccessible:5,SynchronizedInput:2", + "target=wpf ct=CheckBox n=1 withpat=1 pats=LegacyIAccessible:1,SynchronizedInput:1,Toggle:1", + "target=wpf ct=ComboBox n=1 withpat=1 pats=ExpandCollapse:1,ItemContainer:1,LegacyIAccessible:1,Selection:1,SynchronizedInput:1", + "target=wpf ct=Edit n=2 withpat=2 pats=LegacyIAccessible:2,Scroll:2,SynchronizedInput:2,Text:2,Value:2", + "target=wpf ct=List n=1 withpat=1 pats=ItemContainer:1,LegacyIAccessible:1,Scroll:1,Selection:1,SynchronizedInput:1", + "target=wpf ct=ListItem n=5 withpat=5 pats=LegacyIAccessible:5,ScrollItem:5,SelectionItem:5,SynchronizedInput:5", + "target=wpf ct=MenuBar n=1 withpat=1 pats=LegacyIAccessible:1", + "target=wpf ct=MenuItem n=1 withpat=1 pats=ExpandCollapse:1,LegacyIAccessible:1", + "target=wpf ct=Pane n=2 withpat=2 pats=LegacyIAccessible:2,Scroll:2,SynchronizedInput:2", + "target=wpf ct=ScrollBar n=4 withpat=4 pats=LegacyIAccessible:4,RangeValue:4,SynchronizedInput:4", + "target=wpf ct=Text n=9 withpat=9 pats=LegacyIAccessible:9,SynchronizedInput:9", + "target=wpf ct=TitleBar n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=wpf ct=Window n=1 withpat=1 pats=LegacyIAccessible:1,SynchronizedInput:1,Transform:1,Window:1", + "target=notepad ct=Button n=7 withpat=7 pats=Invoke:7,LegacyIAccessible:7", + "target=notepad ct=Document n=1 withpat=1 pats=LegacyIAccessible:1,Scroll:1,Text:1,Text2:1,Value:1", + "target=notepad ct=MenuBar n=2 withpat=2 pats=LegacyIAccessible:2", + "target=notepad ct=MenuItem n=6 withpat=6 pats=ExpandCollapse:6,Invoke:6,LegacyIAccessible:6", + "target=notepad ct=ScrollBar n=2 withpat=2 pats=LegacyIAccessible:2,RangeValue:2", + "target=notepad ct=StatusBar n=1 withpat=1 pats=LegacyIAccessible:1", + "target=notepad ct=Text n=4 withpat=4 pats=LegacyIAccessible:4", + "target=notepad ct=Thumb n=1 withpat=1 pats=LegacyIAccessible:1", + "target=notepad ct=TitleBar n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=notepad ct=Window n=1 withpat=1 pats=LegacyIAccessible:1,Transform:1,Window:1" + ], + "SanityAnchors": [ + { + "Target": "winforms-default", + "ButtonElements": 6, + "ButtonsWithInvoke": 6, + "ButtonAnchorHolds": true, + "TextElements": 0, + "TextWithInvoke": 0, + "TextAnchorHolds": true, + "TextControlTypePresent": false + }, + { + "Target": "winforms-host-providers", + "ButtonElements": 18, + "ButtonsWithInvoke": 18, + "ButtonAnchorHolds": true, + "TextElements": 6, + "TextWithInvoke": 0, + "TextAnchorHolds": true, + "TextControlTypePresent": true + }, + { + "Target": "wpf", + "ButtonElements": 5, + "ButtonsWithInvoke": 5, + "ButtonAnchorHolds": true, + "TextElements": 9, + "TextWithInvoke": 0, + "TextAnchorHolds": true, + "TextControlTypePresent": true + }, + { + "Target": "notepad", + "ButtonElements": 7, + "ButtonsWithInvoke": 7, + "ButtonAnchorHolds": true, + "TextElements": 4, + "TextWithInvoke": 0, + "TextAnchorHolds": true, + "TextControlTypePresent": true + } + ], + "SanityAnchorNote": "Invoke on Button and no-Invoke on Text are the two anchors that would catch a census reading the wrong property ids. winforms-default has no Text ControlType at all: its custom IRawElementProviderSimple collapses every child to Pane, so the Text anchor there is vacuously true and is reported with TextControlTypePresent=false rather than silently counted as a pass.", + "Uia3OnlyPatterns": [ + { + "pattern": "LegacyIAccessible", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30090 + }, + { + "pattern": "Drag", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30137 + }, + { + "pattern": "DropTarget", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30141 + }, + { + "pattern": "Annotation", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30118 + }, + { + "pattern": "Styles", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30127 + }, + { + "pattern": "TextChild", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30136 + } + ], + "PropertyIdNote": "every availability property id in Uia3OnlyPatterns was discovered from the OS at runtime by U7, not written from memory. IsAnnotationPatternAvailable is 30118 on this build; 30113 - the value a from-memory table would carry - is a different property. A hardcoded id table is the single most likely silent failure in a Rust pattern-availability check.", + "FixtureModeNote": "winforms-default is a fixture artifact, not a Win32/WinForms platform fact: the scratch app installs a custom server-side IRawElementProviderSimple that suppresses both the client-side proxies and WinForms own providers, so every child collapses to Pane with LegacyIAccessible only. winforms-host-providers is the apples-to-apples WinForms row - there chkToggle is CheckBox+Toggle, txtValue is Edit+Value, cboChoice is ComboBox+ExpandCollapse+Value." +} \ No newline at end of file diff --git a/probes/windows/captures/03-pattern-census/pattern-matrix.json.normalized b/probes/windows/captures/03-pattern-census/pattern-matrix.json.normalized new file mode 100644 index 0000000..04a30b2 --- /dev/null +++ b/probes/windows/captures/03-pattern-census/pattern-matrix.json.normalized @@ -0,0 +1,181 @@ +{ + "Probe": "03-pattern-census", + "Stack": "uia3-com", + "Authoritative": true, + "Scope": "app/provider", + "Source": "captures/08-uia3-com/census.json, produced by U7 shim mode \"census\" over hand-declared ComImport UIA3 (CUIAutomation8); reshaped here, not re-measured", + "SourceRationale": "KTD1 puts pattern availability on the COM stack because the managed stack structurally cannot name LegacyIAccessible, Drag, DropTarget, Annotation, Styles or TextChild. Re-walking the same four targets a second time would add a second sample of the same fact and a second chance for drift, so this probe consumes the committed measurement.", + "View": "RawView walk (shim census mode); inControlView is recorded per element in the source capture", + "MatrixRecordFormat": "one row per line, space separated key=value fields in fixed order: target=\u003ccensus target label\u003e ct=\u003cUIA ControlType short name\u003e n=\u003celements of that ControlType in the RawView walk\u003e withpat=\u003chow many of them advertise at least one pattern\u003e pats=\u003cName:count comma separated, \"-\" when none\u003e. Counts are element occurrences, not distinct patterns.", + "Targets": [ + { + "Label": "winforms-default", + "ProcessName": "ScratchForms", + "RootFrameworkId": "WinForm", + "ControlViewNodes": 35, + "RawViewNodes": 35, + "ElementsWithAnyPattern": 35, + "ElementsWithAnyPatternExcludingLegacy": 11 + }, + { + "Label": "winforms-host-providers", + "ProcessName": "ScratchForms", + "RootFrameworkId": "WinForm", + "ControlViewNodes": 46, + "RawViewNodes": 46, + "ElementsWithAnyPattern": 46, + "ElementsWithAnyPatternExcludingLegacy": 37 + }, + { + "Label": "wpf", + "ProcessName": "powershell", + "RootFrameworkId": "WPF", + "ControlViewNodes": 32, + "RawViewNodes": 34, + "ElementsWithAnyPattern": 34, + "ElementsWithAnyPatternExcludingLegacy": 33 + }, + { + "Label": "notepad", + "ProcessName": "notepad", + "RootFrameworkId": "Win32", + "ControlViewNodes": 26, + "RawViewNodes": 26, + "ElementsWithAnyPattern": 26, + "ElementsWithAnyPatternExcludingLegacy": 18 + } + ], + "PatternTotalsByTarget": { + "winforms-default": "ExpandCollapse:1,Invoke:6,LegacyIAccessible:35,RangeValue:1,Scroll:1,Transform:1,Value:1,Window:1", + "winforms-host-providers": "ExpandCollapse:4,Invoke:24,LegacyIAccessible:46,RangeValue:1,Scroll:1,ScrollItem:7,Selection:2,SelectionItem:7,Toggle:1,Transform:1,Value:5,Window:1", + "wpf": "ExpandCollapse:2,Invoke:5,ItemContainer:2,LegacyIAccessible:34,RangeValue:4,Scroll:5,ScrollItem:5,Selection:2,SelectionItem:5,SynchronizedInput:28,Text:2,Toggle:1,Transform:1,Value:3,Window:1", + "notepad": "ExpandCollapse:6,Invoke:13,LegacyIAccessible:26,RangeValue:2,Scroll:1,Text:1,Text2:1,Transform:1,Value:2,Window:1" + }, + "Rows": [ + "target=winforms-default ct=Button n=6 withpat=6 pats=Invoke:6,LegacyIAccessible:6", + "target=winforms-default ct=MenuBar n=1 withpat=1 pats=LegacyIAccessible:1", + "target=winforms-default ct=MenuItem n=1 withpat=1 pats=ExpandCollapse:1,LegacyIAccessible:1", + "target=winforms-default ct=Pane n=23 withpat=23 pats=LegacyIAccessible:23,Scroll:1", + "target=winforms-default ct=ScrollBar n=1 withpat=1 pats=LegacyIAccessible:1,RangeValue:1", + "target=winforms-default ct=Thumb n=1 withpat=1 pats=LegacyIAccessible:1", + "target=winforms-default ct=TitleBar n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=winforms-default ct=Window n=1 withpat=1 pats=LegacyIAccessible:1,Transform:1,Window:1", + "target=winforms-host-providers ct=Button n=18 withpat=18 pats=Invoke:18,LegacyIAccessible:18", + "target=winforms-host-providers ct=CheckBox n=1 withpat=1 pats=Invoke:1,LegacyIAccessible:1,Toggle:1", + "target=winforms-host-providers ct=ComboBox n=1 withpat=1 pats=ExpandCollapse:1,LegacyIAccessible:1,Value:1", + "target=winforms-host-providers ct=Edit n=2 withpat=2 pats=LegacyIAccessible:2,Value:2", + "target=winforms-host-providers ct=List n=1 withpat=1 pats=LegacyIAccessible:1,Selection:1", + "target=winforms-host-providers ct=ListItem n=5 withpat=5 pats=Invoke:5,LegacyIAccessible:5,ScrollItem:5,SelectionItem:5", + "target=winforms-host-providers ct=MenuBar n=1 withpat=1 pats=LegacyIAccessible:1", + "target=winforms-host-providers ct=MenuItem n=1 withpat=1 pats=ExpandCollapse:1,LegacyIAccessible:1", + "target=winforms-host-providers ct=Pane n=1 withpat=1 pats=LegacyIAccessible:1,Scroll:1", + "target=winforms-host-providers ct=ScrollBar n=1 withpat=1 pats=LegacyIAccessible:1,RangeValue:1", + "target=winforms-host-providers ct=Slider n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=winforms-host-providers ct=Text n=6 withpat=6 pats=LegacyIAccessible:6", + "target=winforms-host-providers ct=Thumb n=2 withpat=2 pats=LegacyIAccessible:2", + "target=winforms-host-providers ct=TitleBar n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=winforms-host-providers ct=Tree n=1 withpat=1 pats=LegacyIAccessible:1,Selection:1", + "target=winforms-host-providers ct=TreeItem n=2 withpat=2 pats=ExpandCollapse:2,LegacyIAccessible:2,ScrollItem:2,SelectionItem:2", + "target=winforms-host-providers ct=Window n=1 withpat=1 pats=LegacyIAccessible:1,Transform:1,Window:1", + "target=wpf ct=Button n=5 withpat=5 pats=Invoke:5,LegacyIAccessible:5,SynchronizedInput:2", + "target=wpf ct=CheckBox n=1 withpat=1 pats=LegacyIAccessible:1,SynchronizedInput:1,Toggle:1", + "target=wpf ct=ComboBox n=1 withpat=1 pats=ExpandCollapse:1,ItemContainer:1,LegacyIAccessible:1,Selection:1,SynchronizedInput:1", + "target=wpf ct=Edit n=2 withpat=2 pats=LegacyIAccessible:2,Scroll:2,SynchronizedInput:2,Text:2,Value:2", + "target=wpf ct=List n=1 withpat=1 pats=ItemContainer:1,LegacyIAccessible:1,Scroll:1,Selection:1,SynchronizedInput:1", + "target=wpf ct=ListItem n=5 withpat=5 pats=LegacyIAccessible:5,ScrollItem:5,SelectionItem:5,SynchronizedInput:5", + "target=wpf ct=MenuBar n=1 withpat=1 pats=LegacyIAccessible:1", + "target=wpf ct=MenuItem n=1 withpat=1 pats=ExpandCollapse:1,LegacyIAccessible:1", + "target=wpf ct=Pane n=2 withpat=2 pats=LegacyIAccessible:2,Scroll:2,SynchronizedInput:2", + "target=wpf ct=ScrollBar n=4 withpat=4 pats=LegacyIAccessible:4,RangeValue:4,SynchronizedInput:4", + "target=wpf ct=Text n=9 withpat=9 pats=LegacyIAccessible:9,SynchronizedInput:9", + "target=wpf ct=TitleBar n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=wpf ct=Window n=1 withpat=1 pats=LegacyIAccessible:1,SynchronizedInput:1,Transform:1,Window:1", + "target=notepad ct=Button n=7 withpat=7 pats=Invoke:7,LegacyIAccessible:7", + "target=notepad ct=Document n=1 withpat=1 pats=LegacyIAccessible:1,Scroll:1,Text:1,Text2:1,Value:1", + "target=notepad ct=MenuBar n=2 withpat=2 pats=LegacyIAccessible:2", + "target=notepad ct=MenuItem n=6 withpat=6 pats=ExpandCollapse:6,Invoke:6,LegacyIAccessible:6", + "target=notepad ct=ScrollBar n=2 withpat=2 pats=LegacyIAccessible:2,RangeValue:2", + "target=notepad ct=StatusBar n=1 withpat=1 pats=LegacyIAccessible:1", + "target=notepad ct=Text n=4 withpat=4 pats=LegacyIAccessible:4", + "target=notepad ct=Thumb n=1 withpat=1 pats=LegacyIAccessible:1", + "target=notepad ct=TitleBar n=1 withpat=1 pats=LegacyIAccessible:1,Value:1", + "target=notepad ct=Window n=1 withpat=1 pats=LegacyIAccessible:1,Transform:1,Window:1" + ], + "SanityAnchors": [ + { + "Target": "winforms-default", + "ButtonElements": 6, + "ButtonsWithInvoke": 6, + "ButtonAnchorHolds": true, + "TextElements": 0, + "TextWithInvoke": 0, + "TextAnchorHolds": true, + "TextControlTypePresent": false + }, + { + "Target": "winforms-host-providers", + "ButtonElements": 18, + "ButtonsWithInvoke": 18, + "ButtonAnchorHolds": true, + "TextElements": 6, + "TextWithInvoke": 0, + "TextAnchorHolds": true, + "TextControlTypePresent": true + }, + { + "Target": "wpf", + "ButtonElements": 5, + "ButtonsWithInvoke": 5, + "ButtonAnchorHolds": true, + "TextElements": 9, + "TextWithInvoke": 0, + "TextAnchorHolds": true, + "TextControlTypePresent": true + }, + { + "Target": "notepad", + "ButtonElements": 7, + "ButtonsWithInvoke": 7, + "ButtonAnchorHolds": true, + "TextElements": 4, + "TextWithInvoke": 0, + "TextAnchorHolds": true, + "TextControlTypePresent": true + } + ], + "SanityAnchorNote": "Invoke on Button and no-Invoke on Text are the two anchors that would catch a census reading the wrong property ids. winforms-default has no Text ControlType at all: its custom IRawElementProviderSimple collapses every child to Pane, so the Text anchor there is vacuously true and is reported with TextControlTypePresent=false rather than silently counted as a pass.", + "Uia3OnlyPatterns": [ + { + "pattern": "LegacyIAccessible", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30090 + }, + { + "pattern": "Drag", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30137 + }, + { + "pattern": "DropTarget", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30141 + }, + { + "pattern": "Annotation", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30118 + }, + { + "pattern": "Styles", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30127 + }, + { + "pattern": "TextChild", + "availabilityPropertyDiscovered": true, + "availabilityPropertyId": 30136 + } + ], + "PropertyIdNote": "every availability property id in Uia3OnlyPatterns was discovered from the OS at runtime by U7, not written from memory. IsAnnotationPatternAvailable is 30118 on this build; 30113 - the value a from-memory table would carry - is a different property. A hardcoded id table is the single most likely silent failure in a Rust pattern-availability check.", + "FixtureModeNote": "winforms-default is a fixture artifact, not a Win32/WinForms platform fact: the scratch app installs a custom server-side IRawElementProviderSimple that suppresses both the client-side proxies and WinForms own providers, so every child collapses to Pane with LegacyIAccessible only. winforms-host-providers is the apples-to-apples WinForms row - there chkToggle is CheckBox+Toggle, txtValue is Edit+Value, cboChoice is ComboBox+ExpandCollapse+Value." +} \ No newline at end of file diff --git a/probes/windows/captures/04-automationid-census/coverage.json b/probes/windows/captures/04-automationid-census/coverage.json new file mode 100644 index 0000000..ac9ec01 --- /dev/null +++ b/probes/windows/captures/04-automationid-census/coverage.json @@ -0,0 +1,291 @@ +{ + "Probe": "04-automationid-census", + "Question": "what fraction of interactive elements carries a non-empty AutomationId on each Windows UI stack, and in what style", + "Stack": "managed-System.Windows.Automation", + "Scope": "app/provider", + "View": "ControlView - the view a snapshot would walk; the COM cross-check below is RawView, so its denominators are larger by construction", + "InteractiveControlTypes": [ + "Button", + "CheckBox", + "ComboBox", + "DataItem", + "Edit", + "Hyperlink", + "ListItem", + "MenuItem", + "RadioButton", + "Slider", + "Spinner", + "SplitButton", + "Tab", + "TabItem", + "TreeItem" + ], + "InteractiveDefinition": "the ControlTypes that map onto the interactive roles the repo ref system already allocates refs for. Container roles that are actionable-but-not-interactive (scrollarea, disclosure) are deliberately excluded from the denominator - they are ref-able because they advertise an action, not because they are interactive.", + "WalkLimits": { + "NodeBudget": 800, + "MaxDepth": 14 + }, + "Rows": [ + { + "Target": "obsidian", + "Stack": "electron-chromium", + "Elements": 133, + "ElementsWithAutomationId": 2, + "PctAllElements": 1.5, + "InteractiveElements": 8, + "InteractiveWithAutomationId": 0, + "PctInteractiveElements": 0, + "IdStyleEmpty": 131, + "IdStyleNumericControlId": 0, + "IdStyleSymbolic": 2, + "DominantIdStyle": "symbolic", + "AutomationIdSample": [ + + ], + "timingSettlePasses": 2, + "AutomationIdSampleWithheld": "AutomationId values are emitted verbatim for probe-owned fixtures and for Win32 chrome, but not for Obsidian: an Electron AutomationId can be derived from note or vault content, which R11 keeps out of the corpus. Only counts and id-style statistics are reported for this row." + }, + { + "Target": "winforms-default", + "Stack": "winforms", + "Elements": 24, + "ElementsWithAutomationId": 24, + "PctAllElements": 100, + "InteractiveElements": 0, + "InteractiveWithAutomationId": 0, + "PctInteractiveElements": 0, + "IdStyleEmpty": 0, + "IdStyleNumericControlId": 0, + "IdStyleSymbolic": 24, + "DominantIdStyle": "symbolic", + "AutomationIdSample": [ + "Window=frmScratchMain", + "Pane=chkToggle", + "Pane=lblValueCaption", + "Pane=txtValue", + "Pane=cboChoice", + "Pane=btnAction", + "Pane=btnMutateList", + "Pane=tbSlider", + "Pane=trvNodes", + "Pane=lstItems", + "Pane=pnlScroll", + "Pane=btnRow00", + "Pane=btnRow01", + "Pane=btnRow02", + "Pane=btnRow03", + "Pane=btnRow04", + "Pane=btnRow05", + "Pane=btnRow06", + "Pane=btnRow07", + "Pane=lblStatus", + "Pane=txtStatusMirror", + "Pane=lblScrollPos", + "Pane=lblSliderValue", + "Pane=lblInstance" + ] + }, + { + "Target": "winforms-host-providers", + "Stack": "winforms", + "Elements": 26, + "ElementsWithAutomationId": 23, + "PctAllElements": 88.5, + "InteractiveElements": 2, + "InteractiveWithAutomationId": 1, + "PctInteractiveElements": 50, + "IdStyleEmpty": 3, + "IdStyleNumericControlId": 23, + "IdStyleSymbolic": 0, + "DominantIdStyle": "numeric-control-id", + "AutomationIdSample": [ + "Pane=1001", + "Text=1002", + "Pane=1003", + "ComboBox=1004", + "Pane=1005", + "Pane=1006", + "Pane=1008", + "Pane=1009", + "Pane=1010", + "Pane=1011", + "Pane=1012", + "Pane=1013", + "Pane=1014", + "Pane=1015", + "Pane=1016", + "Pane=1017", + "Pane=1018", + "Pane=1019", + "Text=1020", + "Pane=1021", + "Text=1022", + "Text=1023", + "Text=1024" + ] + }, + { + "Target": "wpf", + "Stack": "wpf", + "Elements": 26, + "ElementsWithAutomationId": 18, + "PctAllElements": 69.2, + "InteractiveElements": 11, + "InteractiveWithAutomationId": 11, + "PctInteractiveElements": 100, + "IdStyleEmpty": 8, + "IdStyleNumericControlId": 0, + "IdStyleSymbolic": 18, + "DominantIdStyle": "symbolic", + "AutomationIdSample": [ + "Window=wndScratchWpf", + "CheckBox=chkToggle", + "Edit=txtValue", + "ScrollBar=VerticalScrollBar", + "ScrollBar=HorizontalScrollBar", + "ComboBox=cboChoice", + "Button=btnAction", + "Button=btnMutateList", + "List=lstItems", + "ListItem=lstItem-Item-Alpha", + "ListItem=lstItem-Item-Bravo", + "ListItem=lstItem-Item-Charlie", + "ListItem=lstItem-Item-Delta", + "ListItem=lstItem-Item-Echo", + "Edit=txtStatusMirror", + "ScrollBar=VerticalScrollBar", + "ScrollBar=HorizontalScrollBar", + "Text=lblStatus" + ] + }, + { + "Target": "notepad", + "Stack": "win32", + "Elements": 3, + "ElementsWithAutomationId": 2, + "PctAllElements": 66.7, + "InteractiveElements": 0, + "InteractiveWithAutomationId": 0, + "PctInteractiveElements": 0, + "IdStyleEmpty": 1, + "IdStyleNumericControlId": 2, + "IdStyleSymbolic": 0, + "DominantIdStyle": "numeric-control-id", + "AutomationIdSample": [ + "Pane=15", + "Pane=1025" + ] + }, + { + "Target": "explorer-folder-window", + "Stack": "win32", + "Elements": 82, + "ElementsWithAutomationId": 53, + "PctAllElements": 64.6, + "InteractiveElements": 41, + "InteractiveWithAutomationId": 40, + "PctInteractiveElements": 97.6, + "IdStyleEmpty": 29, + "IdStyleNumericControlId": 9, + "IdStyleSymbolic": 44, + "DominantIdStyle": "symbolic", + "AutomationIdSample": [ + "Pane=40965", + "Pane=41477", + "Pane=1001", + "Edit=SearchEditBox", + "Button=SearchBoxSearchButton", + "StatusBar=StatusBarModuleInner", + "Group=System.StatusBarViewItemCount", + "Text=PropertyValue", + "Group=ViewButtonsGroup", + "RadioButton=ViewMode_Details", + "RadioButton=ViewMode_LargeIcons", + "Pane=ProperTreeHost", + "Pane=100", + "Pane=listview", + "Pane=HorizontalScrollBar", + "SplitButton=System.ItemNameDisplay", + "Button=Dropdown", + "SplitButton=System.DateModified", + "Button=Dropdown", + "SplitButton=System.ItemTypeText", + "Button=Dropdown", + "SplitButton=System.Size", + "Button=Dropdown", + "ListItem=0", + "Edit=System.ItemNameDisplay", + "Edit=System.DateModified", + "Edit=System.ItemTypeText", + "Edit=System.Size", + "ListItem=1", + "Edit=System.ItemNameDisplay", + "Edit=System.DateModified", + "Edit=System.ItemTypeText", + "Edit=System.Size", + "ListItem=2", + "Edit=System.ItemNameDisplay", + "Edit=System.DateModified", + "Edit=System.ItemTypeText", + "Edit=System.Size", + "ListItem=3", + "Edit=System.ItemNameDisplay", + "Edit=System.DateModified", + "Edit=System.ItemTypeText", + "Edit=System.Size", + "ListItem=4", + "Edit=System.ItemNameDisplay" + ] + } + ], + "ComCrossCheck": [ + { + "Target": "winforms-default", + "Stack": "uia3-com", + "View": "RawView", + "Elements": 35, + "ElementsWithAutomationId": 34, + "PctAllElements": 97.1, + "InteractiveElements": 7, + "InteractiveWithAutomationId": 6, + "PctInteractiveElements": 85.7 + }, + { + "Target": "winforms-host-providers", + "Stack": "uia3-com", + "View": "RawView", + "Elements": 46, + "ElementsWithAutomationId": 34, + "PctAllElements": 73.9, + "InteractiveElements": 31, + "InteractiveWithAutomationId": 21, + "PctInteractiveElements": 67.7 + }, + { + "Target": "wpf", + "Stack": "uia3-com", + "View": "RawView", + "Elements": 34, + "ElementsWithAutomationId": 25, + "PctAllElements": 73.5, + "InteractiveElements": 15, + "InteractiveWithAutomationId": 14, + "PctInteractiveElements": 93.3 + }, + { + "Target": "notepad", + "Stack": "uia3-com", + "View": "RawView", + "Elements": 26, + "ElementsWithAutomationId": 14, + "PctAllElements": 53.8, + "InteractiveElements": 13, + "InteractiveWithAutomationId": 7, + "PctInteractiveElements": 53.8 + } + ], + "ComCrossCheckNote": "consumed from captures/08-uia3-com/census.json. The shim serializes at most 60 per-element records per target, so a row is only emitted when the element list covers the whole RawView walk; Explorer and Obsidian are therefore managed-only rows.", + "ObsidianVersion": "1.12.7", + "ObsidianPlacementNote": "the Electron row is measured first, with no other probe window on the desktop, and after an 8 s settle. U3 measured that an Electron tree read behind another window can stay at its first-contact size indefinitely." +} \ No newline at end of file diff --git a/probes/windows/captures/04-automationid-census/coverage.json.normalized b/probes/windows/captures/04-automationid-census/coverage.json.normalized new file mode 100644 index 0000000..b7d00ee --- /dev/null +++ b/probes/windows/captures/04-automationid-census/coverage.json.normalized @@ -0,0 +1,291 @@ +{ + "Probe": "04-automationid-census", + "Question": "what fraction of interactive elements carries a non-empty AutomationId on each Windows UI stack, and in what style", + "Stack": "managed-System.Windows.Automation", + "Scope": "app/provider", + "View": "ControlView - the view a snapshot would walk; the COM cross-check below is RawView, so its denominators are larger by construction", + "InteractiveControlTypes": [ + "Button", + "CheckBox", + "ComboBox", + "DataItem", + "Edit", + "Hyperlink", + "ListItem", + "MenuItem", + "RadioButton", + "Slider", + "Spinner", + "SplitButton", + "Tab", + "TabItem", + "TreeItem" + ], + "InteractiveDefinition": "the ControlTypes that map onto the interactive roles the repo ref system already allocates refs for. Container roles that are actionable-but-not-interactive (scrollarea, disclosure) are deliberately excluded from the denominator - they are ref-able because they advertise an action, not because they are interactive.", + "WalkLimits": { + "NodeBudget": 800, + "MaxDepth": 14 + }, + "Rows": [ + { + "Target": "obsidian", + "Stack": "electron-chromium", + "Elements": 133, + "ElementsWithAutomationId": 2, + "PctAllElements": 1.5, + "InteractiveElements": 8, + "InteractiveWithAutomationId": 0, + "PctInteractiveElements": 0, + "IdStyleEmpty": 131, + "IdStyleNumericControlId": 0, + "IdStyleSymbolic": 2, + "DominantIdStyle": "symbolic", + "AutomationIdSample": [ + + ], + "timingSettlePasses": , + "AutomationIdSampleWithheld": "AutomationId values are emitted verbatim for probe-owned fixtures and for Win32 chrome, but not for Obsidian: an Electron AutomationId can be derived from note or vault content, which R11 keeps out of the corpus. Only counts and id-style statistics are reported for this row." + }, + { + "Target": "winforms-default", + "Stack": "winforms", + "Elements": 24, + "ElementsWithAutomationId": 24, + "PctAllElements": 100, + "InteractiveElements": 0, + "InteractiveWithAutomationId": 0, + "PctInteractiveElements": 0, + "IdStyleEmpty": 0, + "IdStyleNumericControlId": 0, + "IdStyleSymbolic": 24, + "DominantIdStyle": "symbolic", + "AutomationIdSample": [ + "Window=frmScratchMain", + "Pane=chkToggle", + "Pane=lblValueCaption", + "Pane=txtValue", + "Pane=cboChoice", + "Pane=btnAction", + "Pane=btnMutateList", + "Pane=tbSlider", + "Pane=trvNodes", + "Pane=lstItems", + "Pane=pnlScroll", + "Pane=btnRow00", + "Pane=btnRow01", + "Pane=btnRow02", + "Pane=btnRow03", + "Pane=btnRow04", + "Pane=btnRow05", + "Pane=btnRow06", + "Pane=btnRow07", + "Pane=lblStatus", + "Pane=txtStatusMirror", + "Pane=lblScrollPos", + "Pane=lblSliderValue", + "Pane=lblInstance" + ] + }, + { + "Target": "winforms-host-providers", + "Stack": "winforms", + "Elements": 26, + "ElementsWithAutomationId": 23, + "PctAllElements": 88.5, + "InteractiveElements": 2, + "InteractiveWithAutomationId": 1, + "PctInteractiveElements": 50, + "IdStyleEmpty": 3, + "IdStyleNumericControlId": 23, + "IdStyleSymbolic": 0, + "DominantIdStyle": "numeric-control-id", + "AutomationIdSample": [ + "Pane=1001", + "Text=1002", + "Pane=1003", + "ComboBox=1004", + "Pane=1005", + "Pane=1006", + "Pane=1008", + "Pane=1009", + "Pane=1010", + "Pane=1011", + "Pane=1012", + "Pane=1013", + "Pane=1014", + "Pane=1015", + "Pane=1016", + "Pane=1017", + "Pane=1018", + "Pane=1019", + "Text=1020", + "Pane=1021", + "Text=1022", + "Text=1023", + "Text=1024" + ] + }, + { + "Target": "wpf", + "Stack": "wpf", + "Elements": 26, + "ElementsWithAutomationId": 18, + "PctAllElements": 69.2, + "InteractiveElements": 11, + "InteractiveWithAutomationId": 11, + "PctInteractiveElements": 100, + "IdStyleEmpty": 8, + "IdStyleNumericControlId": 0, + "IdStyleSymbolic": 18, + "DominantIdStyle": "symbolic", + "AutomationIdSample": [ + "Window=wndScratchWpf", + "CheckBox=chkToggle", + "Edit=txtValue", + "ScrollBar=VerticalScrollBar", + "ScrollBar=HorizontalScrollBar", + "ComboBox=cboChoice", + "Button=btnAction", + "Button=btnMutateList", + "List=lstItems", + "ListItem=lstItem-Item-Alpha", + "ListItem=lstItem-Item-Bravo", + "ListItem=lstItem-Item-Charlie", + "ListItem=lstItem-Item-Delta", + "ListItem=lstItem-Item-Echo", + "Edit=txtStatusMirror", + "ScrollBar=VerticalScrollBar", + "ScrollBar=HorizontalScrollBar", + "Text=lblStatus" + ] + }, + { + "Target": "notepad", + "Stack": "win32", + "Elements": 3, + "ElementsWithAutomationId": 2, + "PctAllElements": 66.7, + "InteractiveElements": 0, + "InteractiveWithAutomationId": 0, + "PctInteractiveElements": 0, + "IdStyleEmpty": 1, + "IdStyleNumericControlId": 2, + "IdStyleSymbolic": 0, + "DominantIdStyle": "numeric-control-id", + "AutomationIdSample": [ + "Pane=15", + "Pane=1025" + ] + }, + { + "Target": "explorer-folder-window", + "Stack": "win32", + "Elements": 82, + "ElementsWithAutomationId": 53, + "PctAllElements": 64.6, + "InteractiveElements": 41, + "InteractiveWithAutomationId": 40, + "PctInteractiveElements": 97.6, + "IdStyleEmpty": 29, + "IdStyleNumericControlId": 9, + "IdStyleSymbolic": 44, + "DominantIdStyle": "symbolic", + "AutomationIdSample": [ + "Pane=40965", + "Pane=41477", + "Pane=1001", + "Edit=SearchEditBox", + "Button=SearchBoxSearchButton", + "StatusBar=StatusBarModuleInner", + "Group=System.StatusBarViewItemCount", + "Text=PropertyValue", + "Group=ViewButtonsGroup", + "RadioButton=ViewMode_Details", + "RadioButton=ViewMode_LargeIcons", + "Pane=ProperTreeHost", + "Pane=100", + "Pane=listview", + "Pane=HorizontalScrollBar", + "SplitButton=System.ItemNameDisplay", + "Button=Dropdown", + "SplitButton=System.DateModified", + "Button=Dropdown", + "SplitButton=System.ItemTypeText", + "Button=Dropdown", + "SplitButton=System.Size", + "Button=Dropdown", + "ListItem=0", + "Edit=System.ItemNameDisplay", + "Edit=System.DateModified", + "Edit=System.ItemTypeText", + "Edit=System.Size", + "ListItem=1", + "Edit=System.ItemNameDisplay", + "Edit=System.DateModified", + "Edit=System.ItemTypeText", + "Edit=System.Size", + "ListItem=2", + "Edit=System.ItemNameDisplay", + "Edit=System.DateModified", + "Edit=System.ItemTypeText", + "Edit=System.Size", + "ListItem=3", + "Edit=System.ItemNameDisplay", + "Edit=System.DateModified", + "Edit=System.ItemTypeText", + "Edit=System.Size", + "ListItem=4", + "Edit=System.ItemNameDisplay" + ] + } + ], + "ComCrossCheck": [ + { + "Target": "winforms-default", + "Stack": "uia3-com", + "View": "RawView", + "Elements": 35, + "ElementsWithAutomationId": 34, + "PctAllElements": 97.1, + "InteractiveElements": 7, + "InteractiveWithAutomationId": 6, + "PctInteractiveElements": 85.7 + }, + { + "Target": "winforms-host-providers", + "Stack": "uia3-com", + "View": "RawView", + "Elements": 46, + "ElementsWithAutomationId": 34, + "PctAllElements": 73.9, + "InteractiveElements": 31, + "InteractiveWithAutomationId": 21, + "PctInteractiveElements": 67.7 + }, + { + "Target": "wpf", + "Stack": "uia3-com", + "View": "RawView", + "Elements": 34, + "ElementsWithAutomationId": 25, + "PctAllElements": 73.5, + "InteractiveElements": 15, + "InteractiveWithAutomationId": 14, + "PctInteractiveElements": 93.3 + }, + { + "Target": "notepad", + "Stack": "uia3-com", + "View": "RawView", + "Elements": 26, + "ElementsWithAutomationId": 14, + "PctAllElements": 53.8, + "InteractiveElements": 13, + "InteractiveWithAutomationId": 7, + "PctInteractiveElements": 53.8 + } + ], + "ComCrossCheckNote": "consumed from captures/08-uia3-com/census.json. The shim serializes at most 60 per-element records per target, so a row is only emitted when the element list covers the whole RawView walk; Explorer and Obsidian are therefore managed-only rows.", + "ObsidianVersion": "1.12.7", + "ObsidianPlacementNote": "the Electron row is measured first, with no other probe window on the desktop, and after an 8 s settle. U3 measured that an Electron tree read behind another window can stay at its first-contact size indefinitely." +} \ No newline at end of file diff --git a/probes/windows/captures/04-automationid-census/identity-mutation.json b/probes/windows/captures/04-automationid-census/identity-mutation.json new file mode 100644 index 0000000..03d5686 --- /dev/null +++ b/probes/windows/captures/04-automationid-census/identity-mutation.json @@ -0,0 +1,351 @@ +{ + "Probe": "04-automationid-census", + "Question": "when list content changes inside a live process, which per-node identity properties survive", + "Why": "2.5 chooses the Windows RefEntry evidence set. The repo contract is pid, role, path, stable text identity and bounds hash; this arm is the one that shows what a content change does to path, text identity and bounds while pid and role are held constant.", + "ReRunStability": "measured over three consecutive runs on this box: every field here reproduces exactly except the Explorer RuntimeId survival count, which moved between 59/82 and 60/82 - one DirectUI Edit node whose RuntimeId is regenerated or not depending on how the shell refresh lands. That is real platform nondeterminism in the property, not probe noise, and under KTD9 a non-empty normalized diff on this one number is the signal a later re-runner should see rather than something to smooth away. All other captures in this probe were byte-identical across runs.", + "WinFormsNotHere": "the WinForms fixture has no in-process arm because no managed-visible affordance exists to drive it: in --host-providers mode the managed client sees the whole form as Panes with numeric control ids and btnMutateList advertises no InvokePattern, and in default mode the custom provider suppresses patterns entirely. Its list change is measured in identity-restart.json as the restart+mutation row against the pure restart row as control. Driving it any other way would need SendInput or PostMessage, which belongs to U6.", + "RuntimeIdHandling": "RuntimeId is compared in memory and only survived/changed counts are written. The KTD9 normalizer rewrites any literal RuntimeId to \u003cruntimeid\u003e, so a capture holding raw ids would normalize away the exact evidence this arm produces.", + "NameHandling": "Name is compared in memory and only equality counts are written. No Name value from any target reaches a capture, so the R11 content rule is satisfied by construction rather than by redaction.", + "Rows": [ + { + "Target": "wpf", + "Arm": "mutation", + "Change": "InvokePattern on btnMutateList: same list change as the WinForms arm. The WPF fixture also derives each item AutomationId from the item text (lstItem-\u003cname\u003e), which is the interesting contrast.", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 26, + "AfterNodes": 28, + "PathMatchedPairs": 26, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 2, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 26, + "Survived": 22, + "PctSurvived": 84.6 + }, + { + "Property": "ClassName", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 26, + "Survived": 17, + "PctSurvived": 65.4 + }, + { + "Property": "RuntimeId", + "Matched": 26, + "Survived": 16, + "PctSurvived": 61.5 + }, + { + "Property": "Bounds", + "Matched": 26, + "Survived": 22, + "PctSurvived": 84.6 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 26, + "Survived": 23, + "PctSurvived": 88.5 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 14, + "KeysReFound": 13, + "KeysLost": 1, + "PathAlsoSurvived": 10, + "RuntimeIdAlsoSurvived": 9, + "KeysReFoundOnADifferentElement": 1, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 2, + "BoundsSurvived": 2 + }, + { + "ControlType": "CheckBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "ComboBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Edit", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 2, + "BoundsSurvived": 2 + }, + { + "ControlType": "List", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "ListItem", + "PathMatchedPairs": 5, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 5 + }, + { + "ControlType": "ScrollBar", + "PathMatchedPairs": 4, + "AutomationIdSurvived": 4, + "NameSurvived": 4, + "RuntimeIdSurvived": 4, + "BoundsSurvived": 4 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 9, + "AutomationIdSurvived": 9, + "NameSurvived": 4, + "RuntimeIdSurvived": 4, + "BoundsSurvived": 5 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + } + ], + "MutationVerifiedByObservation": true, + "StatusTextBefore": "status:ready", + "StatusTextAfter": "list:mutated" + }, + { + "Target": "explorer-folder-window", + "Arm": "mutation", + "Change": "the probe-owned folder loses item-bravo.txt and gains item-foxtrot.txt and item-golf.txt; Explorer refreshes itself from the file-system change notification, with no window, process or input change", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 82, + "AfterNodes": 88, + "PathMatchedPairs": 82, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 6, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 82, + "Survived": 77, + "PctSurvived": 93.9 + }, + { + "Property": "RuntimeId", + "Matched": 82, + "Survived": 60, + "PctSurvived": 73.2 + }, + { + "Property": "Bounds", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 29, + "KeysReFound": 29, + "KeysLost": 0, + "PathAlsoSurvived": 29, + "RuntimeIdAlsoSurvived": 27, + "KeysReFoundOnADifferentElement": 5, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 8, + "AutomationIdSurvived": 8, + "NameSurvived": 8, + "RuntimeIdSurvived": 8, + "BoundsSurvived": 8 + }, + { + "ControlType": "Edit", + "PathMatchedPairs": 21, + "AutomationIdSurvived": 21, + "NameSurvived": 21, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 21 + }, + { + "ControlType": "Group", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 2 + }, + { + "ControlType": "Header", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Image", + "PathMatchedPairs": 5, + "AutomationIdSurvived": 5, + "NameSurvived": 5, + "RuntimeIdSurvived": 5, + "BoundsSurvived": 5 + }, + { + "ControlType": "List", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "ListItem", + "PathMatchedPairs": 5, + "AutomationIdSurvived": 5, + "NameSurvived": 1, + "RuntimeIdSurvived": 5, + "BoundsSurvived": 5 + }, + { + "ControlType": "MenuBar", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "MenuItem", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Pane", + "PathMatchedPairs": 27, + "AutomationIdSurvived": 27, + "NameSurvived": 27, + "RuntimeIdSurvived": 27, + "BoundsSurvived": 27 + }, + { + "ControlType": "RadioButton", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 2, + "BoundsSurvived": 2 + }, + { + "ControlType": "SplitButton", + "PathMatchedPairs": 4, + "AutomationIdSurvived": 4, + "NameSurvived": 4, + "RuntimeIdSurvived": 4, + "BoundsSurvived": 4 + }, + { + "ControlType": "StatusBar", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 0, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "TitleBar", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + } + ], + "MutationVerifiedByObservation": true, + "timingRefreshLatencySeconds": 20, + "RefreshLatencyNote": "measured, not assumed: the folder window took this long 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 is perfectly stable across content mutation\" - the exact opposite of what the window does once it refreshes." + } + ] +} \ No newline at end of file diff --git a/probes/windows/captures/04-automationid-census/identity-mutation.json.normalized b/probes/windows/captures/04-automationid-census/identity-mutation.json.normalized new file mode 100644 index 0000000..647035b --- /dev/null +++ b/probes/windows/captures/04-automationid-census/identity-mutation.json.normalized @@ -0,0 +1,351 @@ +{ + "Probe": "04-automationid-census", + "Question": "when list content changes inside a live process, which per-node identity properties survive", + "Why": "2.5 chooses the Windows RefEntry evidence set. The repo contract is pid, role, path, stable text identity and bounds hash; this arm is the one that shows what a content change does to path, text identity and bounds while pid and role are held constant.", + "ReRunStability": "measured over three consecutive runs on this box: every field here reproduces exactly except the Explorer RuntimeId survival count, which moved between 59/82 and 60/82 - one DirectUI Edit node whose RuntimeId is regenerated or not depending on how the shell refresh lands. That is real platform nondeterminism in the property, not probe noise, and under KTD9 a non-empty normalized diff on this one number is the signal a later re-runner should see rather than something to smooth away. All other captures in this probe were byte-identical across runs.", + "WinFormsNotHere": "the WinForms fixture has no in-process arm because no managed-visible affordance exists to drive it: in --host-providers mode the managed client sees the whole form as Panes with numeric control ids and btnMutateList advertises no InvokePattern, and in default mode the custom provider suppresses patterns entirely. Its list change is measured in identity-restart.json as the restart+mutation row against the pure restart row as control. Driving it any other way would need SendInput or PostMessage, which belongs to U6.", + "RuntimeIdHandling": "RuntimeId is compared in memory and only survived/changed counts are written. The KTD9 normalizer rewrites any literal RuntimeId to \u003cruntimeid\u003e, so a capture holding raw ids would normalize away the exact evidence this arm produces.", + "NameHandling": "Name is compared in memory and only equality counts are written. No Name value from any target reaches a capture, so the R11 content rule is satisfied by construction rather than by redaction.", + "Rows": [ + { + "Target": "wpf", + "Arm": "mutation", + "Change": "InvokePattern on btnMutateList: same list change as the WinForms arm. The WPF fixture also derives each item AutomationId from the item text (lstItem-\u003cname\u003e), which is the interesting contrast.", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 26, + "AfterNodes": 28, + "PathMatchedPairs": 26, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 2, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 26, + "Survived": 22, + "PctSurvived": 84.6 + }, + { + "Property": "ClassName", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 26, + "Survived": 17, + "PctSurvived": 65.4 + }, + { + "Property": "RuntimeId", + "Matched": 26, + "Survived": 16, + "PctSurvived": 61.5 + }, + { + "Property": "Bounds", + "Matched": 26, + "Survived": 22, + "PctSurvived": 84.6 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 26, + "Survived": 23, + "PctSurvived": 88.5 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 14, + "KeysReFound": 13, + "KeysLost": 1, + "PathAlsoSurvived": 10, + "RuntimeIdAlsoSurvived": 9, + "KeysReFoundOnADifferentElement": 1, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 2, + "BoundsSurvived": 2 + }, + { + "ControlType": "CheckBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "ComboBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Edit", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 2, + "BoundsSurvived": 2 + }, + { + "ControlType": "List", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "ListItem", + "PathMatchedPairs": 5, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 5 + }, + { + "ControlType": "ScrollBar", + "PathMatchedPairs": 4, + "AutomationIdSurvived": 4, + "NameSurvived": 4, + "RuntimeIdSurvived": 4, + "BoundsSurvived": 4 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 9, + "AutomationIdSurvived": 9, + "NameSurvived": 4, + "RuntimeIdSurvived": 4, + "BoundsSurvived": 5 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + } + ], + "MutationVerifiedByObservation": true, + "StatusTextBefore": "status:ready", + "StatusTextAfter": "list:mutated" + }, + { + "Target": "explorer-folder-window", + "Arm": "mutation", + "Change": "the probe-owned folder loses item-bravo.txt and gains item-foxtrot.txt and item-golf.txt; Explorer refreshes itself from the file-system change notification, with no window, process or input change", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 82, + "AfterNodes": 88, + "PathMatchedPairs": 82, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 6, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 82, + "Survived": 77, + "PctSurvived": 93.9 + }, + { + "Property": "RuntimeId", + "Matched": 82, + "Survived": 60, + "PctSurvived": 73.2 + }, + { + "Property": "Bounds", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 82, + "Survived": 82, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 29, + "KeysReFound": 29, + "KeysLost": 0, + "PathAlsoSurvived": 29, + "RuntimeIdAlsoSurvived": 27, + "KeysReFoundOnADifferentElement": 5, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 8, + "AutomationIdSurvived": 8, + "NameSurvived": 8, + "RuntimeIdSurvived": 8, + "BoundsSurvived": 8 + }, + { + "ControlType": "Edit", + "PathMatchedPairs": 21, + "AutomationIdSurvived": 21, + "NameSurvived": 21, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 21 + }, + { + "ControlType": "Group", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 2 + }, + { + "ControlType": "Header", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Image", + "PathMatchedPairs": 5, + "AutomationIdSurvived": 5, + "NameSurvived": 5, + "RuntimeIdSurvived": 5, + "BoundsSurvived": 5 + }, + { + "ControlType": "List", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "ListItem", + "PathMatchedPairs": 5, + "AutomationIdSurvived": 5, + "NameSurvived": 1, + "RuntimeIdSurvived": 5, + "BoundsSurvived": 5 + }, + { + "ControlType": "MenuBar", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "MenuItem", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Pane", + "PathMatchedPairs": 27, + "AutomationIdSurvived": 27, + "NameSurvived": 27, + "RuntimeIdSurvived": 27, + "BoundsSurvived": 27 + }, + { + "ControlType": "RadioButton", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 2, + "BoundsSurvived": 2 + }, + { + "ControlType": "SplitButton", + "PathMatchedPairs": 4, + "AutomationIdSurvived": 4, + "NameSurvived": 4, + "RuntimeIdSurvived": 4, + "BoundsSurvived": 4 + }, + { + "ControlType": "StatusBar", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 0, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "TitleBar", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 1, + "BoundsSurvived": 1 + } + ], + "MutationVerifiedByObservation": true, + "timingRefreshLatencySeconds": , + "RefreshLatencyNote": "measured, not assumed: the folder window took this long 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 is perfectly stable across content mutation\" - the exact opposite of what the window does once it refreshes." + } + ] +} \ No newline at end of file diff --git a/probes/windows/captures/04-automationid-census/identity-restart.json b/probes/windows/captures/04-automationid-census/identity-restart.json new file mode 100644 index 0000000..6c16031 --- /dev/null +++ b/probes/windows/captures/04-automationid-census/identity-restart.json @@ -0,0 +1,445 @@ +{ + "Probe": "04-automationid-census", + "Question": "when a process is restarted, which per-node identity properties survive", + "PlacementControl": "every target is relaunched at the identical window origin (scratch fixtures via --pos / -Left -Top, Notepad via SetWindowPos with the same rect). Without that control every bounds comparison would report 0% survival for the trivial reason that Windows cascades a new window, and the interesting question - whether layout inside the window is reproducible - would be unanswerable.", + "RuntimeIdHandling": "as in identity-mutation.json: compared in memory, only survived/changed counts written.", + "Rows": [ + { + "Target": "winforms-host-providers", + "Arm": "restart", + "Change": "process terminated and relaunched with identical arguments and the identical --pos origin; the list is back at its baseline content", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 26, + "AfterNodes": 26, + "PathMatchedPairs": 26, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 0, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 26, + "Survived": 25, + "PctSurvived": 96.2 + }, + { + "Property": "RuntimeId", + "Matched": 26, + "Survived": 0, + "PctSurvived": 0 + }, + { + "Property": "Bounds", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 23, + "KeysReFound": 23, + "KeysLost": 0, + "PathAlsoSurvived": 23, + "RuntimeIdAlsoSurvived": 0, + "KeysReFoundOnADifferentElement": 1, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "ComboBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "Pane", + "PathMatchedPairs": 17, + "AutomationIdSurvived": 17, + "NameSurvived": 17, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 17 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 6, + "AutomationIdSurvived": 6, + "NameSurvived": 5, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 6 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + } + ] + }, + { + "Target": "wpf", + "Arm": "restart", + "Change": "process terminated and relaunched with identical -Left/-Top arguments; the peer-activation settle is applied again because a WPF window read too early binds the HWND fallback provider permanently (03-pattern-census)", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 26, + "AfterNodes": 26, + "PathMatchedPairs": 26, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 0, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "RuntimeId", + "Matched": 26, + "Survived": 0, + "PctSurvived": 0 + }, + { + "Property": "Bounds", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 14, + "KeysReFound": 14, + "KeysLost": 0, + "PathAlsoSurvived": 14, + "RuntimeIdAlsoSurvived": 0, + "KeysReFoundOnADifferentElement": 0, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 2 + }, + { + "ControlType": "CheckBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "ComboBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "Edit", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 2 + }, + { + "ControlType": "List", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "ListItem", + "PathMatchedPairs": 5, + "AutomationIdSurvived": 5, + "NameSurvived": 5, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 5 + }, + { + "ControlType": "ScrollBar", + "PathMatchedPairs": 4, + "AutomationIdSurvived": 4, + "NameSurvived": 4, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 4 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 9, + "AutomationIdSurvived": 9, + "NameSurvived": 9, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 9 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + } + ] + }, + { + "Target": "notepad", + "Arm": "restart", + "Change": "the real Win32 target: process terminated and relaunched, then placed at the identical rect with SetWindowPos so window placement cannot masquerade as layout drift", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 3, + "AfterNodes": 3, + "PathMatchedPairs": 3, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 0, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + { + "Property": "RuntimeId", + "Matched": 3, + "Survived": 0, + "PctSurvived": 0 + }, + { + "Property": "Bounds", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 2, + "KeysReFound": 2, + "KeysLost": 0, + "PathAlsoSurvived": 2, + "RuntimeIdAlsoSurvived": 0, + "KeysReFoundOnADifferentElement": 0, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Pane", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 2 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + } + ] + }, + { + "Target": "winforms-host-providers", + "Arm": "restart+mutation", + "Change": "relaunched with --mutate-list added: same origin, same arguments otherwise, but the list starts as Alpha/Charlie/Delta/Echo/Foxtrot/Golf. Its control is the pure restart row above; any survival difference between the two rows is attributable to the list change alone.", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 26, + "AfterNodes": 26, + "PathMatchedPairs": 26, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 0, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 26, + "Survived": 25, + "PctSurvived": 96.2 + }, + { + "Property": "RuntimeId", + "Matched": 26, + "Survived": 0, + "PctSurvived": 0 + }, + { + "Property": "Bounds", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 23, + "KeysReFound": 23, + "KeysLost": 0, + "PathAlsoSurvived": 23, + "RuntimeIdAlsoSurvived": 0, + "KeysReFoundOnADifferentElement": 1, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "ComboBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "Pane", + "PathMatchedPairs": 17, + "AutomationIdSurvived": 17, + "NameSurvived": 17, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 17 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 6, + "AutomationIdSurvived": 6, + "NameSurvived": 5, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 6 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + } + ], + "StatusText": "status:ready" + } + ] +} \ No newline at end of file diff --git a/probes/windows/captures/04-automationid-census/identity-restart.json.normalized b/probes/windows/captures/04-automationid-census/identity-restart.json.normalized new file mode 100644 index 0000000..6c16031 --- /dev/null +++ b/probes/windows/captures/04-automationid-census/identity-restart.json.normalized @@ -0,0 +1,445 @@ +{ + "Probe": "04-automationid-census", + "Question": "when a process is restarted, which per-node identity properties survive", + "PlacementControl": "every target is relaunched at the identical window origin (scratch fixtures via --pos / -Left -Top, Notepad via SetWindowPos with the same rect). Without that control every bounds comparison would report 0% survival for the trivial reason that Windows cascades a new window, and the interesting question - whether layout inside the window is reproducible - would be unanswerable.", + "RuntimeIdHandling": "as in identity-mutation.json: compared in memory, only survived/changed counts written.", + "Rows": [ + { + "Target": "winforms-host-providers", + "Arm": "restart", + "Change": "process terminated and relaunched with identical arguments and the identical --pos origin; the list is back at its baseline content", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 26, + "AfterNodes": 26, + "PathMatchedPairs": 26, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 0, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 26, + "Survived": 25, + "PctSurvived": 96.2 + }, + { + "Property": "RuntimeId", + "Matched": 26, + "Survived": 0, + "PctSurvived": 0 + }, + { + "Property": "Bounds", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 23, + "KeysReFound": 23, + "KeysLost": 0, + "PathAlsoSurvived": 23, + "RuntimeIdAlsoSurvived": 0, + "KeysReFoundOnADifferentElement": 1, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "ComboBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "Pane", + "PathMatchedPairs": 17, + "AutomationIdSurvived": 17, + "NameSurvived": 17, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 17 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 6, + "AutomationIdSurvived": 6, + "NameSurvived": 5, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 6 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + } + ] + }, + { + "Target": "wpf", + "Arm": "restart", + "Change": "process terminated and relaunched with identical -Left/-Top arguments; the peer-activation settle is applied again because a WPF window read too early binds the HWND fallback provider permanently (03-pattern-census)", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 26, + "AfterNodes": 26, + "PathMatchedPairs": 26, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 0, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "RuntimeId", + "Matched": 26, + "Survived": 0, + "PctSurvived": 0 + }, + { + "Property": "Bounds", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 14, + "KeysReFound": 14, + "KeysLost": 0, + "PathAlsoSurvived": 14, + "RuntimeIdAlsoSurvived": 0, + "KeysReFoundOnADifferentElement": 0, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 2 + }, + { + "ControlType": "CheckBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "ComboBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "Edit", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 2 + }, + { + "ControlType": "List", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "ListItem", + "PathMatchedPairs": 5, + "AutomationIdSurvived": 5, + "NameSurvived": 5, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 5 + }, + { + "ControlType": "ScrollBar", + "PathMatchedPairs": 4, + "AutomationIdSurvived": 4, + "NameSurvived": 4, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 4 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 9, + "AutomationIdSurvived": 9, + "NameSurvived": 9, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 9 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + } + ] + }, + { + "Target": "notepad", + "Arm": "restart", + "Change": "the real Win32 target: process terminated and relaunched, then placed at the identical rect with SetWindowPos so window placement cannot masquerade as layout drift", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 3, + "AfterNodes": 3, + "PathMatchedPairs": 3, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 0, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + { + "Property": "RuntimeId", + "Matched": 3, + "Survived": 0, + "PctSurvived": 0 + }, + { + "Property": "Bounds", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 3, + "Survived": 3, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 2, + "KeysReFound": 2, + "KeysLost": 0, + "PathAlsoSurvived": 2, + "RuntimeIdAlsoSurvived": 0, + "KeysReFoundOnADifferentElement": 0, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Pane", + "PathMatchedPairs": 2, + "AutomationIdSurvived": 2, + "NameSurvived": 2, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 2 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + } + ] + }, + { + "Target": "winforms-host-providers", + "Arm": "restart+mutation", + "Change": "relaunched with --mutate-list added: same origin, same arguments otherwise, but the list starts as Alpha/Charlie/Delta/Echo/Foxtrot/Golf. Its control is the pure restart row above; any survival difference between the two rows is attributable to the list change alone.", + "Stack": "managed-System.Windows.Automation ControlView", + "BeforeNodes": 26, + "AfterNodes": 26, + "PathMatchedPairs": 26, + "PathsOnlyInBefore": 0, + "PathsOnlyInAfter": 0, + "PerPropertySurvival": [ + { + "Property": "ControlType", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "AutomationId", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "ClassName", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + { + "Property": "Name", + "Matched": 26, + "Survived": 25, + "PctSurvived": 96.2 + }, + { + "Property": "RuntimeId", + "Matched": 26, + "Survived": 0, + "PctSurvived": 0 + }, + { + "Property": "Bounds", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + } + ], + "BoundsBucketed8pxSurvival": { + "Property": "BoundsBucketed", + "Matched": 26, + "Survived": 26, + "PctSurvived": 100 + }, + "AutomationIdKeyedMatch": { + "UniqueKeysBefore": 23, + "KeysReFound": 23, + "KeysLost": 0, + "PathAlsoSurvived": 23, + "RuntimeIdAlsoSurvived": 0, + "KeysReFoundOnADifferentElement": 1, + "KeysReFoundOnADifferentElementNote": "the number of AutomationId keys that still resolve after the change but now land on an element whose Name differs. This is the silent-wrong-target count: a ref keyed on AutomationId alone would resolve successfully and act on the wrong element. Names are compared in memory only.", + "Note": "the key is ControlType|AutomationId; keys that are not unique within a dump are excluded from both sides rather than matched arbitrarily" + }, + "ByControlType": [ + { + "ControlType": "Button", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "ComboBox", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + }, + { + "ControlType": "Pane", + "PathMatchedPairs": 17, + "AutomationIdSurvived": 17, + "NameSurvived": 17, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 17 + }, + { + "ControlType": "Text", + "PathMatchedPairs": 6, + "AutomationIdSurvived": 6, + "NameSurvived": 5, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 6 + }, + { + "ControlType": "Window", + "PathMatchedPairs": 1, + "AutomationIdSurvived": 1, + "NameSurvived": 1, + "RuntimeIdSurvived": 0, + "BoundsSurvived": 1 + } + ], + "StatusText": "status:ready" + } + ] +} \ No newline at end of file