feat: census patterns and automation ids, and measure identity survival

Identity survival is what sub-phase 2.5 designs its Windows RefEntry against,
and the three properties behave nothing alike. AutomationId survives process
restart on every stack measured, at 100 percent across WinForms, WPF, and real
Win32. RuntimeId survives restart nowhere, at zero percent on all three. Path
survives restart perfectly and is the first thing a content change breaks.

Under content mutation the failure is worse than loss. Explorer keys list rows
by row index, so after a folder changes, 29 AutomationId keys still resolve and
5 of them land on a different file. A ref keyed on AutomationId alone does not
fail -- it succeeds against the wrong element. That silent-wrong-target count,
not the survival percentage, is what makes stable text identity load-bearing.

Measuring it needed a real refresh wait: a 4 second settle reported a wholly
unchanged tree and would have recorded Explorer identity as perfectly stable,
the exact inverse of what the window does once it refreshes at 20 seconds.

Two stack facts. WPF automation-peer binding is a one-shot race -- a client
that reads before the peer exists binds the generic HWND provider and never
re-resolves, which a 30 second poll never recovers. And the managed and COM
clients report different AutomationId values for the same window, so the
divergence between stacks is in values as well as in visibility.

The honest pattern divergence is two, not eleven: LegacyIAccessible and Text2
are the only patterns the managed stack structurally cannot name. The larger
figure was an artifact of the WPF peer race and was retracted after the fix.
This commit is contained in:
Lahfir 2026-07-26 18:49:50 -06:00
parent e6f4ecc14e
commit 297cc38dcc
14 changed files with 4326 additions and 0 deletions

View file

@ -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=<census target label> ct=<UIA ControlType short name> n=<elements of that ControlType in the RawView walk> withpat=<how many of them advertise at least one pattern> pats=<Name:count comma separated, "-" when none>. 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 = '<unavailable>'
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}', '<guid>')
}
}
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[<appdomain>;<thread name>;<guid>] 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[...;<guid>] 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

View file

@ -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
<runtimeid>, 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 = '<unavailable>'
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 '<not-found>' }
return [string]$element.Current.Name
} catch { return '<error>' }
}
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 = '<absent>'
$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-<name>), 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 <runtimeid>, 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

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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
}

View file

@ -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": <duration>,
"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
}

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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": <duration>,
"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."
}

View file

@ -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."
}
]
}

View file

@ -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": <duration>,
"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."
}
]
}

View file

@ -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"
}
]
}

View file

@ -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"
}
]
}