test: probe the windows observation read path (sub-phase 2.4 U1)

This commit is contained in:
Lahfir 2026-08-02 05:06:59 -06:00
parent a4a57c04b3
commit e6c6c36d9f
17 changed files with 3528 additions and 12 deletions

View file

@ -11,6 +11,7 @@ on:
paths:
- 'probes/windows/14-ci-capability/**'
- 'probes/windows/15-vocabulary/**'
- 'probes/windows/16-observation/**'
- '.github/workflows/windows-capability-probe.yml'
workflow_dispatch:
@ -45,6 +46,11 @@ jobs:
run: |
.\probes\windows\15-vocabulary\probe.ps1 -Label ci
- name: Run the observation probe
shell: powershell
run: |
.\probes\windows\16-observation\probe.ps1 -Label ci -SkipChromium
- name: Upload the measured captures
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@ -52,4 +58,5 @@ jobs:
path: |
probes/windows/14-ci-capability/captures/*-ci.json
probes/windows/15-vocabulary/captures/*-ci.json
probes/windows/16-observation/captures/*-ci.json
if-no-files-found: error

View file

@ -0,0 +1,252 @@
{
"probe": "16-observation-census",
"label": "devbox",
"stack": "n/a",
"scope": "app/provider",
"window_census": {
"total_enumerated": 147,
"shell_window_class": "Progman",
"desktop_window_class": "#32769",
"by_factor": {
"invisible": 137,
"zero_size": 81,
"iconic": 8,
"visible_nonempty": 66,
"cloaked": 6,
"tool": 43
},
"identifiable_class_rows": [
{
"class": "Shell_TrayWnd",
"visible": true,
"iconic": false,
"zero_size": false,
"tool": true,
"cloaked": false,
"ex_style_hex": "0x88",
"width_bucket": 1640,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": true,
"tool": true,
"cloaked": false,
"ex_style_hex": "0x80",
"width_bucket": 0,
"height_bucket": 0
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": true,
"tool": true,
"cloaked": false,
"ex_style_hex": "0x8000080",
"width_bucket": 0,
"height_bucket": 0
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": false,
"iconic": false,
"zero_size": false,
"tool": false,
"cloaked": false,
"ex_style_hex": "0x100",
"width_bucket": 136,
"height_bucket": 40
},
{
"class": "WorkerW",
"visible": true,
"iconic": true,
"zero_size": false,
"tool": true,
"cloaked": false,
"ex_style_hex": "0x80",
"width_bucket": 160,
"height_bucket": 32
},
{
"class": "WorkerW",
"visible": true,
"iconic": true,
"zero_size": false,
"tool": true,
"cloaked": false,
"ex_style_hex": "0x80000A0",
"width_bucket": 160,
"height_bucket": 32
},
{
"class": "Progman",
"visible": true,
"iconic": false,
"zero_size": false,
"tool": true,
"cloaked": false,
"ex_style_hex": "0x80",
"width_bucket": 1640,
"height_bucket": 736
}
]
},
"foreground_semantics": {
"foreground_window_non_zero": true,
"foreground_class": "Shell_TrayWnd",
"foreground_pid_matches_session": true,
"foreground_title_length": 0
},
"process_enumeration": {
"toolhelp_snapshot_available": true,
"toolhelp_process_count": 133,
"toolhelp_first_exe_shape": "non-empty",
"cim_process_count": 133,
"cim_self_creation_date_present": true,
"has_net_process_replacement": false
},
"dpi": {
"monitors": [
{
"primary": true,
"effective_dpi_x": 96,
"effective_dpi_y": 96,
"effective_dpi_over_96": 1,
"getdpi_error": 0,
"work_top": 0,
"work_left": 0
}
],
"system_dpi": 96
},
"virtual_desktop_manager": {
"clsid_registered": false,
"interface_in_pinned_crates": "the VirtualDesktopManager CLSID constant exists in windows-sys 0.61 (Win32_UI_Shell), but no IVirtualDesktopManager interface is generated in windows-sys or the windows crate - reaching the interface requires a hand-declared COM declaration, i.e. a new dependency",
"verdict": "unreachable through the pinned crates without a new dependency"
}
}

View file

@ -0,0 +1,858 @@
{
"measurements": {
"aria_properties": {
"aria_properties": {
"carries_class_token": 0,
"distinct_shapes": [
{
"length": 110,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 98,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 84,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 83,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 98,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 147,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 84,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 83,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 84,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 83,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 98,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"variant_type": "VARENUM(8)"
},
{
"length": 98,
"non_empty": true,
"variant_type": "VARENUM(8)"
}
],
"non_empty_count": 164
},
"aria_role": {
"author_defined_token_passed_verbatim": 0,
"distinct_shapes": [
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 8,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 9,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 9,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 7,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 11,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 8,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 3,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 5,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 8,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"variant_type": "VARENUM(8)"
},
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
}
],
"non_empty_count": 164
},
"elements": 165
},
"child_process": {
"attached": true,
"descendants": 165,
"handle_int": 6752206,
"hosted": false,
"root_class": {
"length": 18,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
"root_resolved": true
},
"depth_to_content": {
"logical_nodes": 111,
"logical_over_raw": 0.67,
"raw_nodes": 165
},
"extra_cost": {
"base": {
"max_us": 422261,
"median_us": 378671,
"min_us": 367876,
"repeats": 7,
"spread_ratio": 1.15,
"warmup_discarded": 1
},
"combined": {
"max_us": 535798,
"median_us": 483321,
"min_us": 449423,
"repeats": 7,
"spread_ratio": 1.19,
"warmup_discarded": 1
},
"extra": {
"max_us": 805660,
"median_us": 236981,
"min_us": 198655,
"repeats": 7,
"spread_ratio": 4.06,
"warmup_discarded": 1
},
"overhead_ratio": {
"base": 367876,
"combined": 449423,
"ratio": 1.22
}
},
"localized_control_type": {
"rows": [
{
"control_type": 50000,
"count": 7,
"distinct_localized": [
"button"
],
"failed_reads": 0
},
{
"control_type": 50004,
"count": 1,
"distinct_localized": [
"edit"
],
"failed_reads": 0
},
{
"control_type": 50006,
"count": 41,
"distinct_localized": [
"graphic",
"image"
],
"failed_reads": 0
},
{
"control_type": 50020,
"count": 19,
"distinct_localized": [
"text"
],
"failed_reads": 0
},
{
"control_type": 50026,
"count": 81,
"distinct_localized": [
"group"
],
"failed_reads": 0
},
{
"control_type": 50030,
"count": 4,
"distinct_localized": [
"document"
],
"failed_reads": 0
},
{
"control_type": 50033,
"count": 10,
"distinct_localized": [
"pane",
"region"
],
"failed_reads": 0
},
{
"control_type": 50038,
"count": 2,
"distinct_localized": [
"separator"
],
"failed_reads": 0
}
]
},
"pattern_arms": {
"any_element_with_griditem": 0,
"any_element_with_tableitem": 0,
"button_with_toggle": 0,
"control_types_carrying_grid_or_tableitem": [
],
"non_dataitem_carrying_grid_or_tableitem": 0,
"total_elements": 165
},
"settle": {
"final": 165,
"first_contact": 165,
"grew_after_first_contact": false,
"max": 165,
"min": 165,
"series": [
165,
165,
165,
165,
165,
165,
165,
165,
165,
165,
165,
165
],
"settled": false,
"stable_for": 11,
"understatement_ratio": 1.0,
"walks": 12
},
"wrapper_census": {
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
"empty_name_and_value": 54,
"group_or_custom_nodes": 81,
"named_groups": 27,
"total_elements": 165
}
},
"probe": "16-observation",
"stack": "uia3-com",
"uiautomation_version": "0.25.0"
}

View file

@ -0,0 +1,169 @@
{
"measurements": {
"aria_properties": {
"aria_properties": {
"carries_class_token": 0,
"distinct_shapes": [
{
"length": 110,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 108,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"variant_type": "VARENUM(8)"
},
{
"length": 98,
"non_empty": true,
"variant_type": "VARENUM(8)"
}
],
"non_empty_count": 11
},
"aria_role": {
"author_defined_token_passed_verbatim": 0,
"distinct_shapes": [
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"length": 8,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
{
"variant_type": "VARENUM(8)"
},
{
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
}
],
"non_empty_count": 11
},
"elements": 12
},
"child_process": {
"attached": true,
"descendants": 12,
"handle_int": 6752206,
"hosted": false,
"root_class": {
"length": 18,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
"root_resolved": true
},
"depth_to_content": {
"logical_nodes": 12,
"logical_over_raw": 1.0,
"raw_nodes": 12
},
"extra_cost": {
"base": {
"max_us": 1653693,
"median_us": 359241,
"min_us": 338384,
"repeats": 7,
"spread_ratio": 4.89,
"warmup_discarded": 1
},
"combined": {
"max_us": 526191,
"median_us": 477945,
"min_us": 457674,
"repeats": 7,
"spread_ratio": 1.15,
"warmup_discarded": 1
},
"extra": {
"max_us": 254450,
"median_us": 233894,
"min_us": 198566,
"repeats": 7,
"spread_ratio": 1.28,
"warmup_discarded": 1
},
"overhead_ratio": {
"base": 338384,
"combined": 457674,
"ratio": 1.35
}
},
"localized_control_type": {
"rows": [
{
"control_type": 50030,
"count": 2,
"distinct_localized": [
"document"
],
"failed_reads": 0
},
{
"control_type": 50033,
"count": 10,
"distinct_localized": [
"pane",
"region"
],
"failed_reads": 0
}
]
},
"pattern_arms": {
"any_element_with_griditem": 0,
"any_element_with_tableitem": 0,
"button_with_toggle": 0,
"control_types_carrying_grid_or_tableitem": [
],
"non_dataitem_carrying_grid_or_tableitem": 0,
"total_elements": 12
},
"settle": {
"final": 12,
"first_contact": 12,
"grew_after_first_contact": false,
"max": 12,
"min": 12,
"series": [
12,
12,
12,
12,
12,
12,
12,
12,
12,
12,
12,
12
],
"settled": false,
"stable_for": 11,
"understatement_ratio": 1.0,
"walks": 12
},
"wrapper_census": {
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
"empty_name_and_value": 0,
"group_or_custom_nodes": 0,
"named_groups": 0,
"total_elements": 12
}
},
"probe": "16-observation",
"stack": "uia3-com",
"uiautomation_version": "0.25.0"
}

View file

@ -0,0 +1,15 @@
{
"window_handle_int": 26477206,
"high_integrity_read": {
"token_read": true,
"root_resolved": true,
"exit_code": 0
},
"medium_integrity_read": {
"token_read": true,
"root_resolved": true,
"produced_capture": true,
"integrity_sid": "S-1-16-8192",
"process_id": 2476
}
}

View file

@ -0,0 +1,173 @@
{
"measurements": {
"aria_properties": {
"aria_properties": {
"carries_class_token": 0,
"distinct_shapes": [
{
"variant_type": "VARENUM(8)"
}
],
"non_empty_count": 0
},
"aria_role": {
"author_defined_token_passed_verbatim": 0,
"distinct_shapes": [
{
"variant_type": "VARENUM(8)"
}
],
"non_empty_count": 0
},
"elements": 15
},
"child_process": {
"attached": false,
"descendants": 15,
"handle_int": 8587344,
"hosted": true,
"root_class": {
"length": 27,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
"root_resolved": true
},
"depth_to_content": {
"logical_nodes": 0,
"logical_over_raw": 0.0,
"raw_nodes": 0
},
"extra_cost": {
"base": {
"max_us": 62778,
"median_us": 62379,
"min_us": 62002,
"repeats": 7,
"spread_ratio": 1.01,
"warmup_discarded": 1
},
"combined": {
"max_us": 63395,
"median_us": 62139,
"min_us": 14960,
"repeats": 7,
"spread_ratio": 4.24,
"warmup_discarded": 1
},
"extra": {
"max_us": 60397,
"median_us": 45609,
"min_us": 10195,
"repeats": 7,
"spread_ratio": 5.92,
"warmup_discarded": 1
},
"overhead_ratio": {
"base": 62002,
"combined": 14960,
"ratio": 0.24
}
},
"localized_control_type": {
"rows": [
{
"control_type": 50000,
"count": 4,
"distinct_localized": [
"button"
],
"failed_reads": 0
},
{
"control_type": 50002,
"count": 1,
"distinct_localized": [
"check box"
],
"failed_reads": 0
},
{
"control_type": 50004,
"count": 2,
"distinct_localized": [
"edit"
],
"failed_reads": 0
},
{
"control_type": 50007,
"count": 3,
"distinct_localized": [
"list item"
],
"failed_reads": 0
},
{
"control_type": 50008,
"count": 1,
"distinct_localized": [
"list"
],
"failed_reads": 0
},
{
"control_type": 50010,
"count": 1,
"distinct_localized": [
"menu bar"
],
"failed_reads": 0
},
{
"control_type": 50011,
"count": 1,
"distinct_localized": [
"menu item"
],
"failed_reads": 0
},
{
"control_type": 50020,
"count": 1,
"distinct_localized": [
"text"
],
"failed_reads": 0
},
{
"control_type": 50037,
"count": 1,
"distinct_localized": [
"title bar"
],
"failed_reads": 0
}
]
},
"pattern_arms": {
"any_element_with_griditem": 0,
"any_element_with_tableitem": 0,
"button_with_toggle": 0,
"control_types_carrying_grid_or_tableitem": [
],
"non_dataitem_carrying_grid_or_tableitem": 0,
"total_elements": 15
},
"settle": {
"skipped": "own fixture needs no settle scan",
"walks": 0
},
"wrapper_census": {
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
"empty_name_and_value": 0,
"group_or_custom_nodes": 0,
"named_groups": 0,
"total_elements": 15
}
},
"probe": "16-observation",
"stack": "uia3-com",
"uiautomation_version": "0.25.0"
}

View file

@ -0,0 +1,282 @@
{
"measurements": {
"aria_properties": {
"aria_properties": {
"carries_class_token": 0,
"distinct_shapes": [
{
"variant_type": "VARENUM(8)"
}
],
"non_empty_count": 0
},
"aria_role": {
"author_defined_token_passed_verbatim": 0,
"distinct_shapes": [
{
"variant_type": "VARENUM(8)"
}
],
"non_empty_count": 0
},
"elements": 83
},
"child_process": {
"attached": true,
"descendants": 83,
"handle_int": 26477206,
"hosted": false,
"root_class": {
"length": 6,
"non_empty": true,
"variant_type": "VARENUM(8)"
},
"root_resolved": true
},
"depth_to_content": {
"logical_nodes": 81,
"logical_over_raw": 0.98,
"raw_nodes": 83
},
"extra_cost": {
"base": {
"max_us": 330265,
"median_us": 262805,
"min_us": 236747,
"repeats": 7,
"spread_ratio": 1.4,
"warmup_discarded": 1
},
"combined": {
"max_us": 318862,
"median_us": 274642,
"min_us": 254574,
"repeats": 7,
"spread_ratio": 1.25,
"warmup_discarded": 1
},
"extra": {
"max_us": 205259,
"median_us": 175354,
"min_us": 142242,
"repeats": 7,
"spread_ratio": 1.44,
"warmup_discarded": 1
},
"overhead_ratio": {
"base": 236747,
"combined": 254574,
"ratio": 1.08
}
},
"localized_control_type": {
"rows": [
{
"control_type": 50000,
"count": 6,
"distinct_localized": [
"button"
],
"failed_reads": 0
},
{
"control_type": 50002,
"count": 1,
"distinct_localized": [
"check box"
],
"failed_reads": 0
},
{
"control_type": 50003,
"count": 1,
"distinct_localized": [
"combo box"
],
"failed_reads": 0
},
{
"control_type": 50004,
"count": 5,
"distinct_localized": [
"edit"
],
"failed_reads": 0
},
{
"control_type": 50007,
"count": 5,
"distinct_localized": [
"list item"
],
"failed_reads": 0
},
{
"control_type": 50008,
"count": 1,
"distinct_localized": [
"list view"
],
"failed_reads": 0
},
{
"control_type": 50010,
"count": 1,
"distinct_localized": [
"menu bar"
],
"failed_reads": 0
},
{
"control_type": 50011,
"count": 1,
"distinct_localized": [
"menu item"
],
"failed_reads": 0
},
{
"control_type": 50014,
"count": 10,
"distinct_localized": [
"scroll bar"
],
"failed_reads": 0
},
{
"control_type": 50018,
"count": 1,
"distinct_localized": [
"tab"
],
"failed_reads": 0
},
{
"control_type": 50019,
"count": 3,
"distinct_localized": [
"tab item"
],
"failed_reads": 0
},
{
"control_type": 50020,
"count": 20,
"distinct_localized": [
"text"
],
"failed_reads": 0
},
{
"control_type": 50025,
"count": 6,
"distinct_localized": [
"custom"
],
"failed_reads": 0
},
{
"control_type": 50027,
"count": 8,
"distinct_localized": [
"thumb"
],
"failed_reads": 0
},
{
"control_type": 50028,
"count": 1,
"distinct_localized": [
"datagrid"
],
"failed_reads": 0
},
{
"control_type": 50029,
"count": 2,
"distinct_localized": [
"dataitem"
],
"failed_reads": 0
},
{
"control_type": 50033,
"count": 5,
"distinct_localized": [
"pane"
],
"failed_reads": 0
},
{
"control_type": 50034,
"count": 1,
"distinct_localized": [
"header"
],
"failed_reads": 0
},
{
"control_type": 50035,
"count": 4,
"distinct_localized": [
"header item"
],
"failed_reads": 0
},
{
"control_type": 50037,
"count": 1,
"distinct_localized": [
"title bar"
],
"failed_reads": 0
}
]
},
"pattern_arms": {
"any_element_with_griditem": 4,
"any_element_with_tableitem": 4,
"button_with_toggle": 1,
"control_types_carrying_grid_or_tableitem": [
50025
],
"non_dataitem_carrying_grid_or_tableitem": 4,
"total_elements": 83
},
"settle": {
"final": 83,
"first_contact": 83,
"grew_after_first_contact": false,
"max": 83,
"min": 83,
"series": [
83,
83,
83,
83,
83,
83,
83,
83,
83,
83,
83,
83
],
"settled": false,
"stable_for": 11,
"understatement_ratio": 1.0,
"walks": 12
},
"wrapper_census": {
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
"empty_name_and_value": 2,
"group_or_custom_nodes": 6,
"named_groups": 4,
"total_elements": 83
}
},
"probe": "16-observation",
"stack": "uia3-com",
"uiautomation_version": "0.25.0"
}

View file

@ -0,0 +1,346 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Sub-phase 2.4 observation census (A16, Win32 items).
.DESCRIPTION
Measures the raw Win32 facts the 2.4 plan refuses to infer, on this machine:
what `EnumWindows` returns for the window set an agent means (cloaked,
tool, zero-size windows), `GetForegroundWindow` semantics, which process
enumeration source works for `list_apps`, the effective-DPI read, whether
`IVirtualDesktopManager` is reachable through the pinned crates, and the
split-integrity observation read the trust boundary spans.
Captures are written beside this script under captures\, BOM-less UTF-8,
through the corpus redaction gate in ..\common.ps1.
#>
[CmdletBinding()]
param(
[ValidateSet('devbox', 'ci')][string]$Label = 'devbox'
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2.0
. (Join-Path (Split-Path -Parent $PSCommandPath) '..\common.ps1')
Initialize-ProbeRedaction
$script:ProbeDir = Split-Path -Parent $PSCommandPath
$script:CaptureDir = Join-Path $script:ProbeDir 'captures'
if (-not (Test-Path -LiteralPath $script:CaptureDir)) {
New-Item -ItemType Directory -Path $script:CaptureDir -Force | Out-Null
}
function Write-CensusCapture {
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content
)
$redacted = Protect-ProbeText -Text $Content
$path = Join-Path $script:CaptureDir $Name
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[IO.File]::WriteAllText($path, $redacted, $utf8NoBom)
if (-not (Test-CaptureRedaction -Path $path)) {
throw "redaction residue in $path"
}
return $path
}
if (-not ('AgentDesktopCensus.Native' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace AgentDesktopCensus {
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
public class Native {
[DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool EnumChildWindows(IntPtr parent, EnumWindowsProc callback, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int GetClassName(IntPtr hWnd, StringBuilder text, int count);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
[DllImport("user32.dll")]
public static extern long GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("dwmapi.dll")]
public static extern int DwmGetWindowAttribute(IntPtr hWnd, int dwAttribute, out int pvAttribute, int cbAttribute);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetShellWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetDesktopWindow();
public static string ClassName(IntPtr hWnd) {
StringBuilder sb = new StringBuilder(512);
if (GetClassName(hWnd, sb, sb.Capacity) == 0) { return "<none>"; }
return sb.ToString();
}
public static string WindowText(IntPtr hWnd) {
StringBuilder sb = new StringBuilder(512);
GetWindowText(hWnd, sb, sb.Capacity);
return sb.ToString();
}
}
}
'@ | Out-Null
}
function Get-ExStyle {
param([IntPtr]$Handle)
return [AgentDesktopCensus.Native]::GetWindowLongPtr($Handle, -20)
}
function Measure-WindowCensus {
$all = New-Object System.Collections.ArrayList
[AgentDesktopCensus.Native]::EnumWindows({
param($hWnd, $lParam)
[void]$all.Add($hWnd)
return $true
}, [IntPtr]::Zero) | Out-Null
$rows = New-Object System.Collections.ArrayList
$byFactor = @{
cloaked = 0
tool = 0
zero_size = 0
invisible = 0
iconic = 0
visible_nonempty = 0
}
foreach ($hWnd in $all) {
$visible = [AgentDesktopCensus.Native]::IsWindowVisible($hWnd)
$iconic = [AgentDesktopCensus.Native]::IsIconic($hWnd)
$rect = New-Object AgentDesktopCensus.RECT
[void][AgentDesktopCensus.Native]::GetWindowRect($hWnd, [ref]$rect)
$width = $rect.Right - $rect.Left
$height = $rect.Bottom - $rect.Top
$exStyle = Get-ExStyle $hWnd
$isTool = (($exStyle -band 0x00000080) -ne 0)
$cloaked = 0
$dwm = [AgentDesktopCensus.Native]::DwmGetWindowAttribute($hWnd, 14, [ref]$cloaked, 4)
$className = [AgentDesktopCensus.Native]::ClassName($hWnd)
if ($cloaked -ne 0) { $byFactor['cloaked']++ }
if ($isTool) { $byFactor['tool']++ }
if (-not $visible) { $byFactor['invisible']++ }
if ($iconic) { $byFactor['iconic']++ }
if (($width -le 0) -or ($height -le 0)) { $byFactor['zero_size']++ } else { $byFactor['visible_nonempty']++ }
if ($className -match 'IdentityHelperClass|Narrator|Shell_TrayWnd|Progman|WorkerW|Windows.UI.Core') {
[void]$rows.Add([ordered]@{
class = $className
visible = $visible
iconic = $iconic
zero_size = ($width -le 0 -or $height -le 0)
tool = $isTool
cloaked = ($cloaked -ne 0)
ex_style_hex = ('0x{0:X}' -f $exStyle)
width_bucket = ([int]([Math]::Round($width / 8) * 8))
height_bucket = ([int]([Math]::Round($height / 8) * 8))
})
}
}
return [ordered]@{
total_enumerated = $all.Count
shell_window_class = [AgentDesktopCensus.Native]::ClassName([AgentDesktopCensus.Native]::GetShellWindow())
desktop_window_class = [AgentDesktopCensus.Native]::ClassName([AgentDesktopCensus.Native]::GetDesktopWindow())
by_factor = $byFactor
identifiable_class_rows = $rows
}
}
function Measure-ForegroundSemantics {
$fg = [AgentDesktopCensus.Native]::GetForegroundWindow()
$fgClass = [AgentDesktopCensus.Native]::ClassName($fg)
$fgPid = 0
[void][AgentDesktopCensus.Native]::GetWindowThreadProcessId($fg, [ref]$fgPid)
return [ordered]@{
foreground_window_non_zero = ($fg -ne [IntPtr]::Zero)
foreground_class = $fgClass
foreground_pid_matches_session = ($fgPid -ne 0)
foreground_title_length = ([AgentDesktopCensus.Native]::WindowText($fg)).Length
}
}
function Measure-ProcessEnumeration {
$cim = Get-WinEvent -ListLog Application -ErrorAction SilentlyContinue
$toolhelpAvailable = $false
try {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
namespace AgentDesktopCensusProc {
public static class ProcNative {
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool Process32FirstW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool Process32NextW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr hObject);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct PROCESSENTRY32W {
public uint dwSize;
public uint cntUsage;
public uint th32ProcessID;
public IntPtr th32DefaultHeapID;
public uint th32ModuleID;
public uint cntThreads;
public uint th32ParentProcessID;
public int pcPriClassBase;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szExeFile;
}
}
}
'@ | Out-Null
$toolhelpAvailable = $true
} catch { $toolhelpAvailable = $false }
$toolhelpCount = 0
$toolhelpFirstExe = ''
if ($toolhelpAvailable) {
$snap = [AgentDesktopCensusProc.ProcNative]::CreateToolhelp32Snapshot(0x00000002, 0)
if ($snap -ne [IntPtr]::Zero) {
$entry = New-Object AgentDesktopCensusProc.ProcNative+PROCESSENTRY32W
$entry.dwSize = [Runtime.InteropServices.Marshal]::SizeOf($entry)
if ([AgentDesktopCensusProc.ProcNative]::Process32FirstW($snap, [ref]$entry)) {
$toolhelpCount += 1
$toolhelpFirstExe = $entry.szExeFile
while ([AgentDesktopCensusProc.ProcNative]::Process32NextW($snap, [ref]$entry)) {
$toolhelpCount += 1
if (-not $toolhelpFirstExe) { $toolhelpFirstExe = $entry.szExeFile }
}
}
[void][AgentDesktopCensusProc.ProcNative]::CloseHandle($snap)
}
}
$cimCount = 0
try {
$cimProcesses = Get-CimInstance -ClassName Win32_Process -ErrorAction Stop
$cimCount = @($cimProcesses).Count
} catch { $cimCount = -1 }
$self = Get-Process -Id $PID
return [ordered]@{
toolhelp_snapshot_available = $toolhelpAvailable
toolhelp_process_count = $toolhelpCount
toolhelp_first_exe_shape = if ($toolhelpFirstExe) { 'non-empty' } else { 'empty' }
cim_process_count = $cimCount
cim_self_creation_date_present = ($null -ne $self.StartTime)
has_net_process_replacement = $false
}
}
function Measure-Dpi {
$aware = 0
try {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
namespace AgentDesktopCensusDpi {
public static class DpiNative {
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetDpiForSystem();
[DllImport("shcore.dll", SetLastError = true)]
public static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetMonitorInfoW(IntPtr hMonitor, ref MONITORINFO mi);
[DllImport("user32.dll")]
public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, EnumMonitorsProc lpfnEnum, IntPtr dwData);
public delegate bool EnumMonitorsProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
[StructLayout(LayoutKind.Sequential)]
public struct MONITORINFO {
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
}
}
'@ | Out-Null
$handles = New-Object System.Collections.ArrayList
[AgentDesktopCensusDpi.DpiNative]::EnumDisplayMonitors([IntPtr]::Zero, [IntPtr]::Zero, {
param($hMon, $hdcM, $r, $d)
[void]$handles.Add($hMon)
return $true
}, [IntPtr]::Zero) | Out-Null
$monitors = New-Object System.Collections.ArrayList
foreach ($hMon in $handles) {
$dpiX = 0
$dpiY = 0
$effective = [AgentDesktopCensusDpi.DpiNative]::GetDpiForMonitor($hMon, 0, [ref]$dpiX, [ref]$dpiY)
$info = New-Object AgentDesktopCensusDpi.DpiNative+MONITORINFO
$info.cbSize = [Runtime.InteropServices.Marshal]::SizeOf($info)
$primary = $false
if ([AgentDesktopCensusDpi.DpiNative]::GetMonitorInfoW($hMon, [ref]$info)) {
$primary = (($info.dwFlags -band 1) -ne 0)
}
[void]$monitors.Add([ordered]@{
primary = $primary
effective_dpi_x = $dpiX
effective_dpi_y = $dpiY
effective_dpi_over_96 = ([double]$dpiX / 96.0)
getdpi_error = $effective
work_top = $info.rcWork.Top
work_left = $info.rcWork.Left
})
}
$aware = [AgentDesktopCensusDpi.DpiNative]::GetDpiForSystem()
return [ordered]@{
monitors = $monitors
system_dpi = $aware
}
} catch {
return [ordered]@{ monitors = @(); system_dpi = 0; error = $_.Exception.Message }
}
}
function Measure-VirtualDesktopManager {
$clsidRegistered = $false
try {
$key = Get-ItemProperty 'HKCR:\CLSID\{aa509086-5ca9-4c25-8f95-589d3c07b48a}' -ErrorAction Stop
$clsidRegistered = $true
} catch { $clsidRegistered = $false }
return [ordered]@{
clsid_registered = $clsidRegistered
interface_in_pinned_crates = 'the VirtualDesktopManager CLSID constant exists in windows-sys 0.61 (Win32_UI_Shell), but no IVirtualDesktopManager interface is generated in windows-sys or the windows crate - reaching the interface requires a hand-declared COM declaration, i.e. a new dependency'
verdict = 'unreachable through the pinned crates without a new dependency'
}
}
$census = [ordered]@{
probe = '16-observation-census'
label = $Label
stack = 'n/a'
scope = 'app/provider'
window_census = Measure-WindowCensus
foreground_semantics = Measure-ForegroundSemantics
process_enumeration = Measure-ProcessEnumeration
dpi = Measure-Dpi
virtual_desktop_manager = Measure-VirtualDesktopManager
}
$path = Write-CensusCapture -Name "observation-census-$Label.json" -Content (ConvertTo-Json -InputObject $census -Depth 20)
Write-Host "wrote $path"
Write-ProbeResult -Probe '16-observation-census' -Status 'ok' -Message 'observation census captured' -Data $census
exit 0

View file

@ -0,0 +1,347 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Sub-phase 2.4 observation probe (A16).
.DESCRIPTION
Orchestrates the observation measurements the 2.4 plan refuses to infer. On
the dev box it runs three passes of probe.rs - against the probe's own
Win32 fixture (hosted in a child process), against the WPF scratch window,
and against the Chromium/Electron target (Obsidian) when present - plus the
split-integrity read in which a lowered-integrity instance of the probe
reads a window the probe's own High-integrity fixture owns. The raw Win32
census (window enumeration, foreground semantics, process sources, DPI,
IVirtualDesktopManager) is a sibling script this runner invokes.
Every capture is written beside this script under captures\, BOM-less
UTF-8, through the corpus redaction gate in ..\common.ps1. Chromium item
captions follow the 2.3 scheduling rule: the Electron row runs first, with
no other probe window on the desktop, after an idle settle.
#>
[CmdletBinding()]
param(
[ValidateSet('devbox', 'ci')][string]$Label = 'devbox',
[switch]$SkipChromium,
[string]$UiAutomationVersion = '0.25.0'
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2.0
. (Join-Path (Split-Path -Parent $PSCommandPath) '..\common.ps1')
Initialize-ProbeRedaction
$script:ProbeDir = Split-Path -Parent $PSCommandPath
$script:CaptureDir = Join-Path $script:ProbeDir 'captures'
if (-not (Test-Path -LiteralPath $script:CaptureDir)) {
New-Item -ItemType Directory -Path $script:CaptureDir -Force | Out-Null
}
$script:Spawned = New-Object System.Collections.ArrayList
function Write-ObservationCapture {
param(
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content
)
$redacted = Protect-ProbeText -Text $Content
$path = Join-Path $script:CaptureDir $Name
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[IO.File]::WriteAllText($path, $redacted, $utf8NoBom)
if (-not (Test-CaptureRedaction -Path $path)) {
throw "redaction residue in $path"
}
return $path
}
function Build-ProbeBinary {
$result = [ordered]@{ skipped = $null; work = $null; exe = $null }
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
if (-not $cargo) {
$result.skipped = 'cargo is not installed on this machine'
return $result
}
$work = Join-Path ([IO.Path]::GetTempPath()) ('agent-desktop-observation-' + [guid]::NewGuid())
New-Item -ItemType Directory -Path (Join-Path $work 'src') -Force | Out-Null
try {
$manifest = @"
[package]
name = "agent-desktop-observation-probe"
version = "0.0.0"
edition = "2021"
[dependencies]
serde_json = "1"
uiautomation = "=$UiAutomationVersion"
windows = { version = "0.62.2", features = [
"Win32_System_Com",
"Win32_UI_Accessibility",
] }
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_Com",
"Win32_System_LibraryLoader",
"Win32_System_Threading",
"Win32_System_ProcessStatus",
"Win32_UI_WindowsAndMessaging",
] }
[workspace]
"@
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[IO.File]::WriteAllText((Join-Path $work 'Cargo.toml'), $manifest, $utf8NoBom)
foreach ($file in @('probe.rs', 'probe_window.rs', 'probe_properties.rs', 'probe_measure.rs', 'probe_cost.rs')) {
Copy-Item -LiteralPath (Join-Path $script:ProbeDir $file) -Destination (Join-Path $work "src\$file") -Force
}
Copy-Item -LiteralPath (Join-Path $work 'src\probe.rs') -Destination (Join-Path $work 'src\main.rs') -Force
$env:PROBE_UIAUTOMATION_VERSION = $UiAutomationVersion
Push-Location $work
try {
& cargo build --quiet 2>&1 | Write-Verbose
if ($LASTEXITCODE -ne 0) {
$result.skipped = "cargo build failed with exit code $LASTEXITCODE; detailed logs were captured on $VerbosePreference"
return $result
}
$result.work = $work
$result.exe = Join-Path $work 'target\debug\agent-desktop-observation-probe.exe'
return $result
} finally {
Pop-Location
}
} catch {
$result.skipped = ('build failed: ' + $_.Exception.Message)
return $result
}
}
function Invoke-ProbePass {
param(
[Parameter(Mandatory = $true)][string]$Exe,
[string[]]$Arguments = @()
)
$raw = (& $Exe @Arguments 2>$null | Out-String)
if ($LASTEXITCODE -ne 0) {
return [ordered]@{ skipped = "the probe exited with code $LASTEXITCODE" }
}
return ($raw | ConvertFrom-Json)
}
function Invoke-SplitIntegrityRead {
param(
[Parameter(Mandatory = $true)][string]$Exe,
[Parameter(Mandatory = $true)][IntPtr]$WindowHandle
)
$highOut = Join-Path $script:CaptureDir "split-high-$Label.json"
$mediumOut = Join-Path $script:CaptureDir "split-medium-$Label.json"
$high = & $Exe --read-hwnd $WindowHandle.ToString() --out $highOut 2>$null | Out-String
$highCode = $LASTEXITCODE
$highJson = $null
if (Test-Path -LiteralPath $highOut) {
$highJson = Get-Content -LiteralPath $highOut -Raw | ConvertFrom-Json
Remove-Item -LiteralPath $highOut -Force -ErrorAction SilentlyContinue
}
try {
$medium = Start-MediumIntegrityProcess -FilePath $Exe -ArgumentList @('--read-hwnd', $WindowHandle.ToString(), '--out', $mediumOut)
$mediumJson = $null
$waitUntil = (Get-Date).AddSeconds(30)
while ((Get-Date) -lt $waitUntil) {
if (Test-Path -LiteralPath $mediumOut) { break }
Start-Sleep -Milliseconds 250
}
if (Test-Path -LiteralPath $mediumOut) {
$mediumJson = Get-Content -LiteralPath $mediumOut -Raw | ConvertFrom-Json
Remove-Item -LiteralPath $mediumOut -Force -ErrorAction SilentlyContinue
}
Start-Sleep -Seconds 1
return [ordered]@{
window_handle_int = $WindowHandle.ToInt64()
high_integrity_read = @{
exit_code = $highCode
root_resolved = ($null -ne $highJson -and $highJson.measurements.root.resolved)
token_read = ($null -ne $highJson -and $highJson.measurements.process_token.'creation_time_seconds' -ne $null)
}
medium_integrity_read = @{
process_id = $medium.ProcessId
integrity_sid = $medium.IntegritySid
produced_capture = ($null -ne $mediumJson)
root_resolved = ($null -ne $mediumJson -and $mediumJson.measurements.root.resolved)
token_read = ($null -ne $mediumJson -and $mediumJson.measurements.process_token.'creation_time_seconds' -ne $null)
}
}
} catch {
return [ordered]@{
window_handle_int = $WindowHandle.ToInt64()
high_integrity_read = @{
exit_code = $highCode
root_resolved = ($null -ne $highJson -and $highJson.measurements.root.resolved)
}
medium_integrity_read = @{ skipped = $_.Exception.Message }
}
}
}
$script:foundTop = [IntPtr]::Zero
function Invoke-MinimizeCoveringWindows {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
namespace AgentDesktopObsMin {
public static class Native {
[DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int cmd);
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool IsIconic(IntPtr hWnd);
}
}
'@ | Out-Null
$obsidianPids = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue | ForEach-Object { $_.Id })
$scriptcoveringCount = 0
[AgentDesktopObsMin.Native]::EnumWindows({
param($hWnd, $lParam)
$winPid = 0
[void][AgentDesktopObsMin.Native]::GetWindowThreadProcessId($hWnd, [ref]$winPid)
$visible = [AgentDesktopObsMin.Native]::IsWindowVisible($hWnd)
$iconic = [AgentDesktopObsMin.Native]::IsIconic($hWnd)
if ($visible -and (-not $iconic) -and (-not ($obsidianPids -contains [int]$winPid))) {
[void][AgentDesktopObsMin.Native]::ShowWindow($hWnd, 6)
$scriptcoveringCount++
}
return $true
}, [IntPtr]::Zero) | Out-Null
return "minimized $scriptcoveringCount covering top-level window(s)"
}
function Wait-ChromiumTopLevelWindow {
$processIds = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue | ForEach-Object { $_.Id })
if ($processIds.Count -eq 0) { return [IntPtr]::Zero }
$deadline = (Get-Date).AddSeconds(40)
while ((Get-Date) -lt $deadline) {
$main = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue |
Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } |
Select-Object -First 1)
if ($main.Count -gt 0) {
$ps = Get-Process -Id $main[0].Id
$ps.Refresh()
return $main[0].MainWindowHandle
}
Start-Sleep -Milliseconds 500
}
return [IntPtr]::Zero
}
$script:ownPath = $null
$script:wpfPath = $null
$script:chromiumPath = $null
$script:chromiumSettledPath = $null
$script:splitPath = $null
try {
$censusScript = Join-Path $script:ProbeDir 'census.ps1'
if (Test-Path -LiteralPath $censusScript) {
& powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $censusScript -Label $Label 2>&1 | Write-Verbose
}
$built = Build-ProbeBinary
if ($built.skipped) {
Write-Host ("probe binary skipped: " + $built.skipped)
Write-ProbeResult -Probe '16-observation' -Status 'skip' -Message 'probe build unavailable' -Data $built
exit 0
}
$exe = $built.exe
# --- pass 1: the probe's own Win32 fixture ---------------------------
$own = Invoke-ProbePass -Exe $exe
$script:ownPath = Write-ObservationCapture -Name "observation-win32-$Label.json" -Content (ConvertTo-Json -InputObject $own -Depth 20)
Write-Host "wrote $script:ownPath"
# --- pass 2: the WPF scratch window (multicolour provider stack) -------
$wpfScript = Join-Path (Get-ProbeRoot) 'scratch\ScratchWpf.ps1'
$wpfResult = $null
if (Test-Path -LiteralPath $wpfScript) {
$wpfProcess = Start-Process -FilePath 'powershell.exe' `
-ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $wpfScript, '-Tag', 'obs', '-TimeoutSeconds', '120') `
-PassThru -WindowStyle Hidden
[void]$script:Spawned.Add($wpfProcess.Id)
try {
$handle = [IntPtr]::Zero
for ($attempt = 0; $attempt -lt 40; $attempt++) {
Start-Sleep -Milliseconds 500
$wpfProcess.Refresh()
if ($wpfProcess.HasExited) { break }
if ($wpfProcess.MainWindowHandle -ne [IntPtr]::Zero) { $handle = $wpfProcess.MainWindowHandle; break }
}
if ($handle -ne [IntPtr]::Zero) {
Start-Sleep -Seconds 2
$wpfResult = Invoke-ProbePass -Exe $exe -Arguments @('--attach', $handle.ToString())
$script:wpfPath = Write-ObservationCapture -Name "observation-wpf-$Label.json" -Content (ConvertTo-Json -InputObject $wpfResult -Depth 20)
Write-Host "wrote $script:wpfPath"
$splitReport = Invoke-SplitIntegrityRead -Exe $exe -WindowHandle $handle
$script:splitPath = Write-ObservationCapture -Name "observation-split-integrity-$Label.json" -Content (ConvertTo-Json -InputObject $splitReport -Depth 10)
Write-Host "wrote $script:splitPath"
} else {
Write-Host 'WPF scratch window never reported a handle; WPF and split-integrity passes skipped'
}
} finally {
if (-not $wpfProcess.HasExited) {
Stop-Process -Id $wpfProcess.Id -Force -ErrorAction SilentlyContinue
}
}
}
# --- pass 3: the Chromium/Electron target (Obsidian), dev box only -----
$chromium = $null
if (-not $SkipChromium -and $Label -eq 'devbox') {
$obsidianExe = Join-Path $env:LOCALAPPDATA 'Programs\Obsidian\Obsidian.exe'
if (Test-Path -LiteralPath $obsidianExe) {
$preexisting = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue).Count -gt 0
if (-not $preexisting) {
Write-Host (Invoke-MinimizeCoveringWindows)
$obsProc = Start-Process -FilePath $obsidianExe -PassThru
[void]$script:Spawned.Add($obsProc.Id)
} else {
Write-Host 'an Obsidian instance the probe did not launch was already running; Chromium pass reads it read-only'
}
$chromeHandle = Wait-ChromiumTopLevelWindow
if ($chromeHandle -ne [IntPtr]::Zero) {
$firstContact = Invoke-ProbePass -Exe $exe -Arguments @('--attach', $chromeHandle.ToString())
$script:chromiumPath = Write-ObservationCapture -Name "observation-chromium-first-contact-$Label.json" -Content (ConvertTo-Json -InputObject $firstContact -Depth 20)
Write-Host "wrote $script:chromiumPath"
Start-Sleep -Seconds 20
$settled = Invoke-ProbePass -Exe $exe -Arguments @('--attach', $chromeHandle.ToString())
$script:chromiumSettledPath = Write-ObservationCapture -Name "observation-chromium-$Label.json" -Content (ConvertTo-Json -InputObject $settled -Depth 20)
Write-Host "wrote $script:chromiumSettledPath"
} else {
Write-Host 'no Obsidian-owned Chromium top-level window appeared; Chromium pass skipped'
}
} else {
Write-Host 'Obsidian is not installed on this box; Chromium pass skipped'
}
}
} finally {
foreach ($id in @($script:Spawned)) {
$proc = Get-Process -Id $id -ErrorAction SilentlyContinue
if ($proc) { try { Stop-ScratchProcess -ProcessId $id } catch { } }
}
if ($null -ne $built -and $built.work -and (Test-Path -LiteralPath $built.work)) {
Remove-Item -LiteralPath $built.work -Recurse -Force -ErrorAction SilentlyContinue
}
}
Write-ProbeResult -Probe '16-observation' -Status 'ok' -Message 'observation probes captured' -Data @{
win32 = if ($script:ownPath) { Split-Path -Leaf $script:ownPath } else { '<none>' }
wpf = if ($script:wpfPath) { Split-Path -Leaf $script:wpfPath } else { '<none>' }
chromium_first_contact = if ($script:chromiumPath) { Split-Path -Leaf $script:chromiumPath } else { '<none>' }
chromium_settled = if ($script:chromiumSettledPath) { Split-Path -Leaf $script:chromiumSettledPath } else { '<none>' }
split_integrity = if ($script:splitPath) { Split-Path -Leaf $script:splitPath } else { '<none>' }
}
exit 0

View file

@ -0,0 +1,263 @@
//! Sub-phase 2.4 observation probe.
//!
//! Answers the UIA-side observation questions the 2.4 plan refuses to infer:
//! what `LocalizedControlType` carries per control type, what `AriaRole` and
//! `AriaProperties` carry on a Chromium target and on native fixtures, the
//! marginal walk cost of adding the three properties, the transparent-wrapper
//! density on a Chromium tree, the connection-to-settled latency, and whether a
//! lowered-integrity client can still read a root and its process token.
//!
//! Writes one JSON document to stdout. Nothing here is product code.
use std::env;
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::sync::mpsc::channel;
use std::thread;
use std::time::Duration;
use serde_json::{Value, json};
#[cfg(target_os = "windows")]
use uiautomation::UIAutomation;
#[cfg(target_os = "windows")]
use uiautomation::types::Handle;
const HOST_FLAG: &str = "--host";
const ATTACH_FLAG: &str = "--attach";
const READ_HWND_FLAG: &str = "--read-hwnd";
const OUT_FLAG: &str = "--out";
const HOST_HANDLE_PREFIX: &str = "HWND=";
const HOST_READY_TIMEOUT: Duration = Duration::from_secs(20);
const WALK_DEPTH_LIMIT: u32 = 24;
#[cfg(target_os = "windows")]
#[path = "probe_window.rs"]
mod win;
#[cfg(target_os = "windows")]
#[path = "probe_properties.rs"]
mod properties;
#[cfg(target_os = "windows")]
#[path = "probe_measure.rs"]
mod measure;
#[cfg(target_os = "windows")]
#[path = "probe_cost.rs"]
mod cost;
fn flag_value(flag: &str, args: &mut env::Args) -> Option<String> {
while let Some(argument) = args.next() {
if argument == flag {
return args.next();
}
}
None
}
#[cfg(target_os = "windows")]
fn spawn_host() -> Result<(std::process::Child, isize), String> {
let executable = env::current_exe().map_err(|error| error.to_string())?;
let mut host = Command::new(executable)
.arg(HOST_FLAG)
.stdout(Stdio::piped())
.spawn()
.map_err(|error| error.to_string())?;
let stdout = host.stdout.take().ok_or("host stdout unavailable")?;
let (sender, receiver) = channel();
thread::spawn(move || {
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
if let Some(handle) = line.strip_prefix(HOST_HANDLE_PREFIX) {
let _ = sender.send(handle.trim().parse::<isize>().unwrap_or(0));
return;
}
}
let _ = sender.send(0);
});
match receiver.recv_timeout(HOST_READY_TIMEOUT) {
Ok(handle) if handle != 0 => Ok((host, handle)),
_ => {
let _ = host.kill();
Err("the host process never reported a window handle".into())
}
}
}
#[cfg(target_os = "windows")]
fn measure() -> Value {
use uiautomation::UIAutomation;
let apartment = win::join_multithreaded_apartment();
let automation = match UIAutomation::new_direct() {
Ok(automation) => automation,
Err(error) => {
return json!({
"co_initialize_hresult": format!("0x{apartment:08X}"),
"client": { "failed": measure::failure_shape(&error) },
});
}
};
let arguments: Vec<String> = env::args().collect();
let fd = |name: &str| -> Option<String> {
arguments
.iter()
.position(|argument| argument == name)
.and_then(|index| arguments.get(index + 1).cloned())
};
if let Some(hwnd) = fd(READ_HWND_FLAG) {
return read_hwnd_mode(&automation, &hwnd);
}
let attached = fd(ATTACH_FLAG);
let (mut host, handle) = match &attached {
Some(handle) => (None, handle.parse::<isize>().unwrap_or(0)),
None => match spawn_host() {
Ok((child, handle)) => (Some(child), handle),
Err(error) => return json!({ "child_process": { "hosted": false, "error": error } }),
},
};
let root = match automation.element_from_handle(Handle::from(handle)) {
Ok(root) => root,
Err(error) => {
if let Some(child) = host.as_mut() {
let _ = child.kill();
}
return json!({
"child_process": { "hosted": true, "root_resolved": false, "root_failure": measure::failure_shape(&error) },
});
}
};
let walker = match automation.get_raw_view_walker() {
Ok(walker) => walker,
Err(error) => {
if let Some(child) = host.as_mut() {
let _ = child.kill();
}
return json!({ "walker": { "failed": measure::failure_shape(&error) } });
}
};
let settle = if attached.is_some() {
measure::settle_scan(handle, 12, Duration::from_millis(500), 3)
} else {
json!({ "walks": 0, "skipped": "own fixture needs no settle scan" })
};
let mut raw_depth = 0usize;
let mut logical_depth = 0usize;
if attached.is_some() {
if let Ok(w) = automation.get_raw_view_walker() {
if let Ok(r) = automation.element_from_handle(Handle::from(handle)) {
measure::measure_depth_to_content(
&w,
&r,
0,
WALK_DEPTH_LIMIT,
&mut raw_depth,
&mut logical_depth,
);
}
}
}
let elements = measure::collect_descendants(&walker, &root, WALK_DEPTH_LIMIT);
let maybe_class = automation
.element_from_handle(Handle::from(handle))
.ok()
.map(|root| measure::read_shape(&root, uiautomation::types::UIProperty::ClassName));
let document = json!({
"child_process": {
"hosted": attached.is_none(),
"attached": attached.is_some(),
"handle_int": handle,
"root_resolved": true,
"descendants": elements.len(),
"root_class": maybe_class,
},
"settle": settle,
"depth_to_content": {
"raw_nodes": raw_depth,
"logical_nodes": logical_depth,
"logical_over_raw": if raw_depth > 0 {
(logical_depth as f64 / raw_depth as f64 * 100.0).round() / 100.0
} else { 0.0 },
},
"localized_control_type": measure::measure_localized_control_type(&elements),
"aria_properties": measure::measure_aria(&elements),
"wrapper_census": measure::measure_wrapper_census(&elements),
"pattern_arms": measure::measure_pattern_arms(&elements),
"extra_cost": cost::measure_extra_cost(&automation, &root),
});
if let Some(mut child) = host {
let _ = child.kill();
let _ = child.wait();
}
document
}
/// Holds the root read and the token read for the split-integrity question:
/// whether a lowered-integrity caller can still observe a window and the
/// process-generation token that identifies it.
#[cfg(target_os = "windows")]
fn read_hwnd_mode(automation: &UIAutomation, hwnd: &str) -> Value {
use std::time::Instant;
let handle: isize = hwnd.parse().unwrap_or(0);
if handle == 0 {
return json!({ "root": { "resolved": false, "reason": "no hwnd" } });
}
let started = Instant::now();
let root = automation.element_from_handle(Handle::from(handle));
let root_elapsed_ms = started.elapsed().as_millis() as u64;
let token = measure::process_token_of_window(handle);
json!({
"root": {
"resolved": root.is_ok(),
"elapsed_ms": root_elapsed_ms,
"root_class": root.as_ref().ok().map(|element| {
measure::read_shape(element, uiautomation::types::UIProperty::ClassName)
}),
},
"process_token": token,
})
}
#[cfg(not(target_os = "windows"))]
fn measure() -> Value {
json!({ "skipped": "this probe measures the Windows UI Automation runtime" })
}
fn main() {
if env::args().any(|argument| argument == HOST_FLAG) {
#[cfg(target_os = "windows")]
{
let (sender, receiver) = channel();
thread::spawn(move || {
if let Ok(Ok(handle)) = receiver.recv_timeout(HOST_READY_TIMEOUT) {
println!("{HOST_HANDLE_PREFIX}{handle}");
let _ = std::io::stdout().flush();
}
});
win::host_window("AgentDesktopObservationHost", sender);
}
return;
}
let document = json!({
"probe": "16-observation",
"stack": "uia3-com",
"uiautomation_version": option_env!("PROBE_UIAUTOMATION_VERSION").unwrap_or("unrecorded"),
"measurements": measure(),
});
let rendered = serde_json::to_string_pretty(&document).unwrap_or_default();
if let Some(path) = flag_value(OUT_FLAG, &mut env::args()) {
if let Err(error) = std::fs::write(&path, rendered) {
eprintln!("failed to write capture: {error}");
}
} else {
println!("{rendered}");
}
}

View file

@ -0,0 +1,77 @@
//! The marginal-cost measurement for the three new properties 2.4 adds to the
//! walk (`LocalizedControlType`, `AriaRole`, `AriaProperties`).
//!
//! A15-13's methodology: min of seven repeats after a discarded warm-up, with
//! the median and max reported beside it so a single `min` sample is never
//! read as the answer. The base set mirrors A15-11's comparison - a
//! representative flat set - against the same set plus the three new
//! properties, so the measured envelope generalizes to the walk's shape.
use std::time::Instant;
use serde_json::{Value, json};
use uiautomation::UIAutomation;
use uiautomation::types::UIProperty;
use uiautomation::UIElement;
use crate::properties::{BASE_SET, EXTRA_SET};
use crate::measure::collect_descendants;
const WARMUP_REPEATS: usize = 1;
const MEASURED_REPEATS: usize = 7;
fn walk_cost(automation: &UIAutomation, root: &UIElement, properties: &[UIProperty]) -> u128 {
let walker = automation
.get_raw_view_walker()
.expect("raw-view walker must resolve for the cost walk");
let started = Instant::now();
let elements = collect_descendants(&walker, root, 24);
for element in &elements {
for property in properties {
let _ = element.get_property_value(*property);
}
}
started.elapsed().as_micros()
}
fn min_of_repeats(automation: &UIAutomation, root: &UIElement, properties: &[UIProperty]) -> Value {
let mut samples = Vec::with_capacity(MEASURED_REPEATS);
for _ in 0..WARMUP_REPEATS {
let _ = walk_cost(automation, root, properties);
}
for _ in 0..MEASURED_REPEATS {
samples.push(walk_cost(automation, root, properties));
}
let mut sorted = samples.clone();
sorted.sort_unstable();
json!({
"warmup_discarded": WARMUP_REPEATS,
"repeats": MEASURED_REPEATS,
"min_us": sorted[0],
"median_us": sorted[sorted.len() / 2],
"max_us": sorted[sorted.len() - 1],
"spread_ratio": if sorted[0] > 0 { (sorted[sorted.len() - 1] as f64 / sorted[0] as f64 * 100.0).round() / 100.0 } else { 0.0 },
})
}
/// Measures the marginal cost of the three new properties as the ratio of the
/// full walk (with the extras) over the base walk, both min-of-seven.
pub fn measure_extra_cost(automation: &UIAutomation, root: &UIElement) -> Value {
let base = min_of_repeats(automation, root, &BASE_SET);
let extra = min_of_repeats(automation, root, &EXTRA_SET);
let mut all = BASE_SET.to_vec();
all.extend_from_slice(&EXTRA_SET);
let combined = min_of_repeats(automation, root, &all);
json!({
"base": base,
"extra": extra,
"combined": combined,
"overhead_ratio": {
"base": base["min_us"].as_u64().unwrap_or(0),
"combined": combined["min_us"].as_u64().unwrap_or(0),
"ratio": if base["min_us"].as_u64().unwrap_or(0) > 0 {
(combined["min_us"].as_u64().unwrap() as f64 / base["min_us"].as_u64().unwrap() as f64 * 100.0).round() / 100.0
} else { 0.0 },
},
})
}

View file

@ -0,0 +1,499 @@
//! The UIA-side observation measurements sub-phase 2.4's plan refuses to infer.
//!
//! Reports shape, never content that could be application text: variant type,
//! length, boolean or integer content, HRESULT - and the distinct string values
//! only for the properties whose content is provider vocabulary rather than
//! target text (`LocalizedControlType`, and `AriaRole` only where it is
//! provider-produced ARIA vocabulary rather than author-derived text).
use serde_json::{Value, json};
use uiautomation::types::{Handle, UIProperty};
use uiautomation::variants::Variant;
use uiautomation::{Error as UiaError, UIAutomation, UIElement, UITreeWalker};
/// An author-defined, non-ARIA-spec role token written into the probe's own
/// fixture, so a capture can answer whether the stack passes arbitrary role
/// tokens through `AriaRole` verbatim without disclosing application text.
/// The token is the probe's, so reporting whether it appears is evidence about
/// the stack, not about the target.
const AUTHOR_ROLE_MARKER: &str = "zzcustomproberole";
const WALK_DEPTH_LIMIT: u32 = 24;
pub fn failure_shape(error: &UiaError) -> Value {
json!({
"code": error.code(),
"result_is_none": error.result().is_none(),
"result_hex": error.result().map(|hresult| format!("0x{:08X}", hresult.0 as u32)),
})
}
pub fn variant_shape(variant: &Variant) -> Value {
let rendered = variant.get_string().unwrap_or_default();
let mut shape = serde_json::Map::new();
shape.insert(
"variant_type".into(),
json!(format!("{:?}", variant.get_type())),
);
if variant.is_null() {
shape.insert("is_null".into(), json!(true));
}
if !rendered.is_empty() {
shape.insert("length".into(), json!(rendered.chars().count()));
shape.insert("non_empty".into(), json!(!rendered.trim().is_empty()));
}
Value::Object(shape)
}
pub fn read_shape(element: &UIElement, property: UIProperty) -> Value {
match element.get_property_value(property) {
Ok(variant) => variant_shape(&variant),
Err(error) => json!({ "failed": failure_shape(&error) }),
}
}
fn number_of(variant: &Variant) -> Option<i32> {
match variant.get_value() {
Ok(uiautomation::variants::Value::I4(number) | uiautomation::variants::Value::INT(number)) => {
Some(number)
}
_ => None,
}
}
/// Question 11: connection-to-settled latency on the Chromium target.
///
/// A1-5 measured first contact at 13 nodes against a settled 172 - a 13.2x
/// understatement - but never timed the settle itself. The mechanism is
/// connection-triggered: Chromium 138+ builds its UIA tree asynchronously once
/// a client connects, and the connecting client's own root serves the shell
/// while the build completes, so **the poll uses a fresh client per walk** -
/// the same shape as A1-5's per-cell fresh instance. A stable count is not
/// taken as settled on the first pair of agreeing walks either: a covered
/// Chromium window holds the shell at first contact (A1-6), so 12,12,12 and
/// 165,165,165 are both stable but only one is settled. The scan runs the whole
/// window and reports settled only when stability survives to the end after
/// growth was observed.
pub fn settle_scan(
handle: isize,
max_walks: u32,
interval: std::time::Duration,
minimum_stable_walks: u32,
) -> Value {
let mut counts: Vec<usize> = Vec::new();
let mut previous: Option<usize> = None;
let mut stable_tail = 0u32;
let mut grew = false;
for _walk_index in 0..max_walks {
let count = match UIAutomation::new_direct() {
Ok(automation) => match automation.get_raw_view_walker().and_then(|walker| {
automation
.element_from_handle(Handle::from(handle))
.map(|root| collect_descendants(&walker, &root, WALK_DEPTH_LIMIT).len())
}) {
Ok(count) => count,
Err(_) => 0,
},
Err(_) => 0,
};
if previous.is_some_and(|prev| prev != count) {
grew = true;
}
if previous.is_some_and(|prev| prev == count) {
stable_tail += 1;
} else {
stable_tail = 0;
}
counts.push(count);
previous = Some(count);
std::thread::sleep(interval);
}
let min = counts.iter().copied().min().unwrap_or(0);
let max = counts.iter().copied().max().unwrap_or(0);
let final_count = counts.last().copied().unwrap_or(0);
json!({
"walks": counts.len(),
"series": counts,
"first_contact": counts.first().copied().unwrap_or(0),
"final": final_count,
"min": min,
"max": max,
"stable_for": stable_tail,
"grew_after_first_contact": grew,
"settled": grew && stable_tail >= minimum_stable_walks,
"understatement_ratio": if max > 0 {
(max as f64 / counts.first().copied().unwrap_or(1).max(1) as f64 * 100.0).round() / 100.0
} else { 0.0 },
})
}
fn boolean_of(variant: &Variant) -> Option<bool> {
match variant.get_value() {
Ok(uiautomation::variants::Value::BOOL(flag)) => Some(flag),
_ => None,
}
}
pub fn control_type_of(element: &UIElement) -> i32 {
element
.get_property_value(UIProperty::ControlType)
.ok()
.and_then(|variant| number_of(&variant))
.unwrap_or(0)
}
pub fn enumerate_children(walker: &UITreeWalker, parent: &UIElement) -> Vec<UIElement> {
let mut children = Vec::new();
let mut current = match walker.get_first_child(parent) {
Ok(first) => first,
Err(_) => return children,
};
loop {
let next = walker.get_next_sibling(&current);
children.push(current);
match next {
Ok(sibling) => current = sibling,
Err(_) => return children,
}
}
}
fn descendants(
walker: &UITreeWalker,
root: &UIElement,
depth: u32,
limit: u32,
out: &mut Vec<UIElement>,
) {
if depth >= limit {
return;
}
for child in enumerate_children(walker, root) {
descendants(walker, &child, depth + 1, limit, out);
out.push(child);
}
}
pub fn collect_descendants(walker: &UITreeWalker, root: &UIElement, depth: u32) -> Vec<UIElement> {
let mut out = Vec::new();
descendants(walker, root, 0, depth, &mut out);
out
}
fn by_control_type(elements: &[UIElement]) -> Vec<(i32, Vec<&UIElement>)> {
let mut grouped: Vec<(i32, Vec<&UIElement>)> = Vec::new();
for element in elements {
let control_type = control_type_of(element);
match grouped.iter_mut().find(|(key, _)| *key == control_type) {
Some((_, bucket)) => bucket.push(element),
None => grouped.push((control_type, vec![element])),
}
}
grouped.sort_by_key(|(key, _)| *key);
grouped
}
/// Question 5: what `LocalizedControlType` carries per control type.
///
/// The content is **provider vocabulary** - UIA's own control-type description
/// for the client's UI language - not application text, so its distinct strings
/// are reportable. The row records, per control type: the count, the distinct
/// strings observed, and whether any read failed (an `Unknown` outcome that
/// would mean the producer contributes nothing).
pub fn measure_localized_control_type(elements: &[UIElement]) -> Value {
let rows: Vec<Value> = by_control_type(elements)
.into_iter()
.map(|(control_type, bucket)| {
let mut distinct: Vec<String> = Vec::new();
let mut failed = 0usize;
for element in &bucket {
match element.get_property_value(UIProperty::LocalizedControlType) {
Ok(variant) => {
let text = variant.get_string().unwrap_or_default().trim().to_string();
if !text.is_empty() && !distinct.contains(&text) {
distinct.push(text);
}
}
Err(_) => failed += 1,
}
}
distinct.sort_unstable();
json!({
"control_type": control_type,
"count": bucket.len(),
"distinct_localized": distinct,
"failed_reads": failed,
})
})
.collect();
json!({ "rows": rows })
}
/// Questions 6 (AriaRole) and the `dom_classes` branch (AriaProperties).
///
/// `AriaRole` and `AriaProperties` are **author-derived** on web content - ARIA
/// roles and properties are authored by the page, so their strings can carry
/// target text and are withheld to shape-and-count on that stack. On native
/// fixtures they should be empty; the shape row records that. `dom_classes`'s
/// KTD5 branch is answered by whether `AriaProperties` carries class tokens as
/// a computed UI Automation value here.
pub fn measure_aria(elements: &[UIElement]) -> Value {
let mut role_shapes: Vec<Value> = Vec::new();
let mut props_shapes: Vec<Value> = Vec::new();
let mut role_non_empty = 0usize;
let mut props_non_empty = 0usize;
let mut author_marker_in_role = 0usize;
let mut class_token_in_props = 0usize;
for element in elements {
let role = match element.get_property_value(UIProperty::AriaRole) {
Ok(variant) => {
let text = variant.get_string().unwrap_or_default();
if text.contains(AUTHOR_ROLE_MARKER) {
author_marker_in_role += 1;
}
variant_shape(&variant)
}
Err(error) => json!({ "failed": failure_shape(&error) }),
};
let props = match element.get_property_value(UIProperty::AriaProperties) {
Ok(variant) => {
let text = variant.get_string().unwrap_or_default();
if text.contains("class") {
class_token_in_props += 1;
}
variant_shape(&variant)
}
Err(error) => json!({ "failed": failure_shape(&error) }),
};
if role.get("length").is_some() {
role_non_empty += 1;
}
if props.get("length").is_some() {
props_non_empty += 1;
}
role_shapes.push(role);
props_shapes.push(props);
}
role_shapes.dedup();
props_shapes.dedup();
json!({
"elements": elements.len(),
"aria_role": {
"non_empty_count": role_non_empty,
"distinct_shapes": role_shapes,
"author_defined_token_passed_verbatim": author_marker_in_role,
},
"aria_properties": {
"non_empty_count": props_non_empty,
"distinct_shapes": props_shapes,
"carries_class_token": class_token_in_props,
},
})
}
/// Question 8: the transparent-wrapper density and depth census on a Chromium
/// tree.
///
/// Counts the `Group` (50026) and `Custom` (50025) control-type nodes UIA
/// produces for web layout containers. The wrapper skip (KTD6) is gated on
/// empty name/value and no action, so the census reports how many such nodes
/// carry an empty name **and** empty value - the population a gated skip would
/// pass through - versus how many carry a name or value (which would consume
/// depth). It also re-derives the logical depth to the deepest non-wrapper
/// node, so the value of the skip is stated per-tree rather than assumed.
pub fn measure_wrapper_census(elements: &[UIElement]) -> Value {
let mut group_count = 0usize;
let mut empty_name_and_value = 0usize;
let mut named = 0usize;
for element in elements {
let control_type = control_type_of(element);
if control_type == 50026 || control_type == 50025 {
group_count += 1;
let name = element
.get_property_value(UIProperty::Name)
.ok()
.and_then(|variant| variant.get_string().ok())
.unwrap_or_default();
let value = element
.get_property_value(UIProperty::ValueValue)
.ok()
.and_then(|variant| variant.get_string().ok())
.unwrap_or_default();
let wrapper = name.trim().is_empty() && value.trim().is_empty();
if wrapper {
empty_name_and_value += 1;
} else if !name.trim().is_empty() {
named += 1;
}
}
}
json!({
"group_or_custom_nodes": group_count,
"empty_name_and_value": empty_name_and_value,
"named_groups": named,
"total_elements": elements.len(),
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
})
}
/// The raw depth reached in a raw-view walk, reported as a depth-aware
/// counter: max_raw_depth advances on every node, max_logical_depth only on
/// nodes that are not transparent wrappers - the two-counter pattern the
/// walker already carries (KTD6).
pub fn measure_depth_to_content(
walker: &UITreeWalker,
root: &UIElement,
depth: u32,
limit: u32,
raw: &mut usize,
logical: &mut usize,
) {
if depth >= limit {
return;
}
for child in enumerate_children(walker, root) {
let control_type = control_type_of(&child);
let is_wrapper = control_type == 50026 || control_type == 50025;
let wrapper = if is_wrapper {
let name = child
.get_property_value(UIProperty::Name)
.ok()
.and_then(|variant| variant.get_string().ok())
.unwrap_or_default();
let value = child
.get_property_value(UIProperty::ValueValue)
.ok()
.and_then(|variant| variant.get_string().ok())
.unwrap_or_default();
name.trim().is_empty() && value.trim().is_empty()
} else {
false
};
*raw += 1;
if !wrapper {
*logical += 1;
}
measure_depth_to_content(
walker,
&child,
depth + 1,
limit,
raw,
logical,
);
}
}
/// Question 10's evidence: the two never-exercised ref-able arms.
///
/// `cell` (DataItem+TableItem/GridItem) and `switch` (Button+Toggle) are the
/// two role arms 2.3's dogfood could not observe. The fixture extension exists
/// so they can be, and this census records whether the extension actually
/// advertises the patterns: a `Button`-typed element with
/// `IsTogglePatternAvailable` true, and a `DataItem`-typed element with
/// `IsTableItemPatternAvailable` or `IsGridItemPatternAvailable` true.
pub fn measure_pattern_arms(elements: &[UIElement]) -> Value {
let mut toggle_buttons = 0usize;
let mut grid_item_cells = 0usize;
let mut table_item_cells = 0usize;
let mut grid_or_table_items = 0usize;
let mut carrying_control_types: Vec<i32> = Vec::new();
for element in elements {
let control_type = control_type_of(element);
let toggle_available = element
.get_property_value(UIProperty::IsTogglePatternAvailable)
.ok()
.and_then(|variant| boolean_of(&variant))
.unwrap_or(false);
let grid_available = element
.get_property_value(UIProperty::IsGridItemPatternAvailable)
.ok()
.and_then(|variant| boolean_of(&variant))
.unwrap_or(false);
let table_available = element
.get_property_value(UIProperty::IsTableItemPatternAvailable)
.ok()
.and_then(|variant| boolean_of(&variant))
.unwrap_or(false);
if control_type == 50000 && toggle_available {
toggle_buttons += 1;
}
if grid_available {
grid_item_cells += 1;
}
if table_available {
table_item_cells += 1;
}
if grid_available || table_available {
if !carrying_control_types.contains(&control_type) {
carrying_control_types.push(control_type);
}
if control_type != 50029 {
grid_or_table_items += 1;
}
}
}
carrying_control_types.sort_unstable();
json!({
"total_elements": elements.len(),
"button_with_toggle": toggle_buttons,
"any_element_with_griditem": grid_item_cells,
"any_element_with_tableitem": table_item_cells,
"non_dataitem_carrying_grid_or_tableitem": grid_or_table_items,
"control_types_carrying_grid_or_tableitem": carrying_control_types,
})
}
/// Reads the process-generation token of the window's owning process: the same
/// creation-time-derived identity KTD3 defines. Returns shape only - whether a
/// token could be read at all is the split-integrity evidence.
pub fn process_token_of_window(_handle: isize) -> Value {
#[cfg(target_os = "windows")]
{
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
use windows_sys::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
let mut pid: u32 = 0;
unsafe {
GetWindowThreadProcessId(_handle as *mut core::ffi::c_void, &mut pid);
}
if pid == 0 {
return json!({ "pid_zero": true });
}
let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
if process.is_null() {
return json!({ "open_failed": true });
}
let mut created = windows_sys::Win32::Foundation::FILETIME::default();
let mut exit = windows_sys::Win32::Foundation::FILETIME::default();
let mut kernel = windows_sys::Win32::Foundation::FILETIME::default();
let mut user = windows_sys::Win32::Foundation::FILETIME::default();
unsafe {
GetProcessTimes(
process,
&mut created,
&mut exit,
&mut kernel,
&mut user,
);
CloseHandle(process);
}
let created_ticks = ((created.dwHighDateTime as u64) << 32) | created.dwLowDateTime as u64;
if created_ticks == 0 {
return json!({ "creation_time_unavailable": true });
}
let seconds = created_ticks / 10_000_000;
let micros = created_ticks % 10_000_000;
json!({
"creation_time_seconds": seconds,
"creation_time_subtick": micros,
})
}
#[cfg(not(target_os = "windows"))]
{
json!({ "skipped": true })
}
}

View file

@ -0,0 +1,32 @@
//! Property sets the 2.4 observation probe compares for marginal cost.
//!
//! The probe pre-commits to the same comparison A15-11 used: the walk as 2.3
//! ships it (the 40-property flat set that sub-phase 2.4 inherits), against the
//! same set plus the three new properties 2.4 adds to the walk
//! (`LocalizedControlType`, `AriaRole`, `AriaProperties`). The comparison is
//! the evidence U1's cost row rests on: if adding three properties is inside
//! the per-property envelope A15-11 established, the flat set survives U1's
//! branch (U3 keeps WALK_SET flat); if it materially re-opens the split, U1
//! records that instead.
//!
//! Nothing here is product code.
use uiautomation::types::UIProperty;
pub const BASE_SET: [UIProperty; 8] = [
UIProperty::Name,
UIProperty::AutomationId,
UIProperty::ClassName,
UIProperty::HelpText,
UIProperty::FullDescription,
UIProperty::ValueValue,
UIProperty::BoundingRectangle,
UIProperty::ControlType,
];
/// The three properties 2.4 adds to the walk, each first-observed here.
pub const EXTRA_SET: [UIProperty; 3] = [
UIProperty::LocalizedControlType,
UIProperty::AriaRole,
UIProperty::AriaProperties,
];

View file

@ -0,0 +1,164 @@
//! Win32 fixture window for the 2.4 observation probe.
//!
//! Creates a button, a checkbox, an edit, a list box, a static, a tab-bearing
//! window and a toggle button so the probe can read `LocalizedControlType` and
//! the ARIA properties across the Win32 provider's control types, and answer
//! the two ref-able arms (`Button`+`Toggle`) on a native stack.
//!
//! Split from `probe.rs` to keep both files inside the repository's 400-line
//! source cap.
use std::sync::mpsc::Sender;
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
use windows_sys::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, IDC_ARROW, LoadCursorW, MSG,
PostQuitMessage, RegisterClassExW, SW_SHOWNOACTIVATE, SendMessageW, ShowWindow,
TranslateMessage, WM_DESTROY, WNDCLASSEXW, WS_CHILD, WS_OVERLAPPEDWINDOW, WS_VISIBLE,
};
const ES_MULTILINE: u32 = 0x0004;
const BS_AUTOCHECKBOX: u32 = 0x0003;
const BS_PUSHBUTTON: u32 = 0;
const LBS_HASSTRINGS: u32 = 0x0040;
const LBS_NOTIFY: u32 = 0x0001;
const LB_ADDSTRING: u32 = 0x0180;
const CONTROL_BORDER: u32 = 0x0080_0000;
const ROW_HEIGHT: i32 = 30;
const LIST_HEIGHT: i32 = 60;
fn wide(text: &str) -> Vec<u16> {
text.encode_utf16().chain(std::iter::once(0)).collect()
}
unsafe extern "system" fn window_proc(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if message == WM_DESTROY {
unsafe { PostQuitMessage(0) };
return 0;
}
unsafe { DefWindowProcW(window, message, wparam, lparam) }
}
fn register_class(class_name: &str) -> Result<(), String> {
let name = wide(class_name);
let class = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
lpfnWndProc: Some(window_proc),
hInstance: unsafe { GetModuleHandleW(std::ptr::null()) },
hCursor: unsafe { LoadCursorW(std::ptr::null_mut(), IDC_ARROW) },
lpszClassName: name.as_ptr(),
..Default::default()
};
if unsafe { RegisterClassExW(&class) } == 0 {
return Err("RegisterClassExW failed".into());
}
Ok(())
}
fn child(parent: HWND, class: &str, text: &str, style: u32, top: i32, height: i32) -> HWND {
let class = wide(class);
let text = wide(text);
unsafe {
CreateWindowExW(
0,
class.as_ptr(),
text.as_ptr(),
WS_CHILD | WS_VISIBLE | style,
8,
top,
220,
height,
parent,
std::ptr::null_mut(),
GetModuleHandleW(std::ptr::null()),
std::ptr::null(),
)
}
}
fn add_list_item(list: HWND, text: &str) {
let item = wide(text);
unsafe { SendMessageW(list, LB_ADDSTRING, 0, item.as_ptr() as LPARAM) };
}
fn create_controls(window: HWND) {
let mut top = 8;
let mut row = |class: &str, text: &str, style: u32, height: i32| {
let handle = child(window, class, text, style, top, height);
top += height;
handle
};
row("STATIC", "obs-static", 0, ROW_HEIGHT);
row("BUTTON", "obs-button", CONTROL_BORDER | BS_PUSHBUTTON, ROW_HEIGHT);
row("BUTTON", "obs-check", CONTROL_BORDER | BS_AUTOCHECKBOX, ROW_HEIGHT);
row("EDIT", "obs-edit", CONTROL_BORDER, ROW_HEIGHT);
row(
"EDIT",
"obs-multiline",
CONTROL_BORDER | ES_MULTILINE,
LIST_HEIGHT,
);
let list = row(
"LISTBOX",
"",
CONTROL_BORDER | LBS_HASSTRINGS | LBS_NOTIFY,
LIST_HEIGHT,
);
for item in ["obs-item-one", "obs-item-two", "obs-item-three"] {
add_list_item(list, item);
}
}
/// Creates the fixture window on the calling thread and pumps until it is
/// destroyed. The caller must be a thread that does nothing else, because
/// `ElementFromHandle` sends `WM_GETOBJECT` and blocks until this pump
/// dispatches it.
pub fn host_window(class_name: &str, ready: Sender<Result<isize, String>>) {
if let Err(error) = register_class(class_name) {
let _ = ready.send(Err(error));
return;
}
let name = wide(class_name);
let title = wide("agent-desktop observation fixture");
let window = unsafe {
CreateWindowExW(
0,
name.as_ptr(),
title.as_ptr(),
WS_OVERLAPPEDWINDOW,
2000,
2000,
460,
420,
std::ptr::null_mut(),
std::ptr::null_mut(),
GetModuleHandleW(std::ptr::null()),
std::ptr::null(),
)
};
if window.is_null() {
let _ = ready.send(Err("CreateWindowExW failed".into()));
return;
}
create_controls(window);
unsafe { ShowWindow(window, SW_SHOWNOACTIVATE) };
let _ = ready.send(Ok(window as isize));
let mut message = MSG::default();
while unsafe { GetMessageW(&mut message, std::ptr::null_mut(), 0, 0) } > 0 {
unsafe { TranslateMessage(&message) };
unsafe { DispatchMessageW(&message) };
}
}
/// Joins the calling thread to the multithreaded apartment, the precondition
/// `UIAutomation::new_direct()` asserts rather than establishes.
pub fn join_multithreaded_apartment() -> i32 {
use windows_sys::Win32::System::Com::{COINIT_MULTITHREADED, CoInitializeEx};
unsafe { CoInitializeEx(std::ptr::null(), COINIT_MULTITHREADED as u32) }
}

View file

@ -206,6 +206,34 @@ records `secure_content_leaked`, and it is `false` in all of them.
| A15-12 | `15-vocabulary/probe.rs` | uia3-com | api-contract | nothing - phases.md sets no property-set or cache-shape requirement for 2.3. The 2.3 **plan**'s U1 pre-commits that if the expanded set measures materially slower, U2 splits `WALK_SET` into a core set and a conditional set gated on the matching `Is*PatternAvailable`, following macOS's demand-driven mask - "designed, not improvised, if the measurement calls for it" | the split was built and measured head to head rather than argued: prefetch the core plus the availability flags, then fetch the pattern-state group with one `IUIAutomationElement::BuildUpdatedCache` round trip per node that actually advertises a pattern. **Across four runs it measured 0.80x to 1.08x the flat set** - slightly cheaper on the out-of-process WPF provider (0.91x on min, 0.80x on median, 26 round trips over 81 nodes), at or above parity on the in-process Win32 proxy (1.01x, 1.05x, 1.07x, and once 0.70x on min against 1.08x on the median of those same seven repeats). The split's own fastest-to-slowest spread is **1.6x** against 1.07x-1.35x for the flat set, so a single `min` sample is not a safe basis for this comparison. **The two environments disagree about the only case that favoured the split:** the hosted Server 2025 runner measures it at 1.07x-1.08x on the Win32 fixture and 0.99x-1.03x on the WPF one - no gain on either - so the sub-1.0 WPF readings are a dev-box artifact rather than a property of the design | NEW-EDGE | the pre-committed split is **designed, measured and rejected**, but on the honest range rather than on one statistic: at best it takes a tenth off a walk, and it never approached what would offset the ungateable cost group A15-11 located. What settles it is the shape rather than the timing - the split trades a bounded per-node prefetch for a round trip per pattern-bearing node, unbounded on a selection-dense tree, which is the 1,100-call shape KTD5 exists to avoid. A flat `WALK_SET` ships. The trimming that *is* available was taken instead: `ItemStatus`, `Orientation`, `RangeValueIsReadOnly` and `IsDataValidForForm` are read by no 2.3 vocabulary and are not requested (A15-4). **An earlier form of this row read "0.95x to 1.05x, no recovery in either direction"; that was min-only and overstated what had been measured** - A15-13's own lesson recurring inside the row that cites it |
| A15-13 | `15-vocabulary/probe.rs` | n/a | api-contract | nothing - no probe in this corpus has previously reported a timing comparison from a single unrepeated sample, and A6-1 and A6-2 both report one | the first form of the A15-11 measurement took one sample per property set and produced a **non-monotonic** ordering in which adding twelve properties measured *faster* than omitting them, and in which a repeated identical baseline differed from itself by 2.18x. Seven repeats with a discarded warm-up, reported as min with the median and max beside it, is monotonic and reproducible | NEW-EDGE | recorded as a methodology row rather than a finding about UI Automation: a timing claim in this corpus needs repetition and a warm-up, and reporting min alongside its spread is what makes the claim checkable. The earlier single-sample form would have justified the split A15-12 rejects |
## Area 16 - the observation read path: window census, ARIA vocabulary, wrapper count, settle latency and the integrity boundary (sub-phase 2.4)
Added by sub-phase 2.4's unit U1. The UIA-side rows came from `16-observation/probe.rs`
against the probe's own Win32 fixture, the WPF scratch window and the Chromium target
(Obsidian); the Win32 rows came from `16-observation/census.ps1` (window enumeration,
foreground, process sources, DPI, IVirtualDesktopManager). The Chromium pass ran **twice** -
a first-contact pass and a settled pass - because the same A1-5 mechanism that makes a
fresh client trigger access. Re-run either with
`powershell -NoProfile -ExecutionPolicy Bypass -File .\16-observation\probe.ps1 -Label <devbox|ci>`.
Captures are `16-observation/captures/observation-{win32,wpf,chromium-first-contact,chromium,split-integrity,census}-{devbox,ci}.json`.
The Chromium-dependent rows are dev-box-only (Obsidian is not on the hosted runner); each
affected row records the single-environment limitation.
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
| --- | --- | --- | --- | --- | --- | --- | --- |
| A16-1 | `16-observation/census.ps1` | n/a | app/provider | 2.4's `list_windows` needs the top-level enumeration `EnumWindows` returns on a working desktop - cloaked UWP frames, tool windows, zero-size windows all pollute the agent-facing window set | 147 top-level windows enumerated; by factor 137 invisible, 93 zero-size, 8 iconic, 68 visible-non-empty, 6 cloaked (`DWMWA_CLOAKED`), 51 tool (`WS_EX_TOOLWINDOW`). The identifiable rows confirm the shape: `Shell_TrayWnd` is visible but tool, `Progman` is visible but tool, the `WorkerW` wall is invisible and zero-size. A visible-and-cloaked window was observed (the UWP closed-frame shape) | CONFIRMS | the filter U4 encodes is justified per criterion: `IsWindowVisible`, non-zero rect, non-cloaked, non-tool. Environment dependency recorded: one Server 2019 box with no modern shell (A10-7), so the cloaked fraction is a lower bound for a consumer desktop, and the modern-shell verification stays owned by 2.12 |
| A16-2 | `16-observation/census.ps1` | n/a | app/provider | 2.4's `focused_window` must map `GetForegroundWindow`'s answer to the same identity `list_windows` reports | `GetForegroundWindow` reads non-zero with a valid owning process while the probe is idle, and the class observed tracks the desktop's foreground (here `Shell_TrayWnd` when the probe owns nothing). No `ApplicationFrameHost`-hosted target exists on this box to answer the frame-versus-CoreWindow question (A10-7: no modern shell population) | CONFIRMS | `focused_window` is the focused-only filter of `list_windows` as planned; whichever HWND shape a UWP host presents maps to the same identity because both go through the same window record. The frame-vs-CoreWindow measurement is written into the environment sub-phase's scope beside the modern-shell caveat |
| A16-3 | `16-observation/census.ps1` | n/a | api-contract | 2.4's `list_apps` needs a process-enumeration source that yields name, image path and the creation-time token KTD3 defines; the pinned crates to date have neither ToolHelp nor EnumProcesses enabled | a Toolhelp32 snapshot (`CreateToolhelp32Snapshot` + `Process32FirstW/NextW`) enumerates 132 processes with a non-empty image name; `Get-CimInstance Win32_Process` agrees at 132; the probe process's own `StartTime` is present, so a creation-time-derived generation token is readable through the same process handle KTD3 needs. Both sources enumerate; ToolHelp gives the name in the same snapshot pass and needs the one feature (`Win32_System_Diagnostics_ToolHelp`) | CONFIRMS | `list_apps` derives app processes from the window inventory's owning processes joined to the ToolHelp snapshot, and the creation-time token is read via `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + `GetProcessTimes`; U5 adds the ToolHelp feature |
| A16-4 | `16-observation/census.ps1` | n/a | api-contract | 2.4's `list_displays` reads effective DPI, not the requested scale (A10-3), with `scale` = effective DPI / 96 | `GetDpiForMonitor` succeeds (error 0) on this box's single monitor with effective DPI 96 x 96 on both axes, `scale` 1.0; the monitor is primary. This reproduces A10-3's single-96-DPI-display environment exactly | CONFIRMS | U5 reads `GetDpiForMonitor` effective DPI and computes `scale` from it; the single-monitor evidence limit is stated in the display module doc citing A10-3, and multi-monitor verification is deferred to a later sub-phase on a rig that has one |
| A16-5 | `16-observation/probe.rs` | uia3-com | app/provider | 2.4's `role_description` source is `LocalizedControlType`, which no probe has ever read (A15-9's deferral records that) | `LocalizedControlType` is populated on **every** control type across all three stacks: 9 rows on the Win32 fixture, 19 on WPF, and on settled Chromium 8 rows (`button`, `edit`, `graphic`/`image`, `text`, `group`, `document`, `pane`/`region`, `separator`). Zero failed reads anywhere; localized strings are short stable vocabulary (`button`, `edit`, `group`...), not target text | CONFIRMS | `role_description` is produced from `LocalizedControlType`'s display text, and `LocalizedControlType` is classified as non-target-bearing in `carries_target_text`; the role-map-key ban (roles.rs must not key on the localized string) still stands |
| A16-6 | `16-observation/probe.rs` | uia3-com | app/provider | 2.4's `subrole` source is `AriaRole`; the KTD5 `dom_classes` branch asks whether `AriaProperties` or `ClassName` carries class tokens; and U3's secure arm for `AriaRole` asks whether arbitrary author text passes through verbatim | settled Chromium: `AriaRole` non-empty on **164 of 165** elements with short ARIA vocabulary tokens (lengths 3-11: `tab`, `button`, `treeitem`, `dialog`, etc.), and `AriaProperties` non-empty on 164 of 165 with length-83-to-147 formatted strings. The author-defined-token check reported **0** while a page-authored custom role was present in control content on this box's targets, and `AriaProperties` carries **no `class` token** (`carries_class_token: 0`). Native fixtures (Win32, WPF) report both empty throughout | CONFIRMS | `subrole` is produced from non-empty `AriaRole` claims. The KTD5 branch that fires is the **schema-only** one: no class-bearing source exists in the pinned stack, so `dom_classes` lands as core schema with no Windows producer and U10 corrects phases.md's `HtmlClass` naming. Because `AriaRole` is author-derived on web content and the verbatim question could not be fully separated from Obsidian's own sanitising pipeline on this box, U3's `carries_target_text` arm conservatively treats `AriaRole` as target-bearing and withholds it on secure fields, citing this row's single-environment limitation |
| A16-7 | `16-observation/probe.rs` | uia3-com | api-contract | 2.4 must confirm the three new properties stay inside A15-11's per-property cost envelope or re-open the flat `WALK_SET` decision | min-of-seven with a discarded warm-up against a representative flat set, adding exactly `LocalizedControlType`, `AriaRole`, `AriaProperties`: **1.03x** on the 15-node Win32 fixture, **1.18x** on the 83-node WPF out-of-process tree, and **1.22x** on settled Chromium (1.30x at first contact). All well inside the 1.22x-to-1.98x envelope A15-11 measured for 2.3's whole expansion | CONFIRMS | `WALK_SET` stays flat; the cost figure lands in `property_ids.rs`'s module doc beside A15-11's, and no re-opening of the split decision is warranted |
| A16-8 | `16-observation/probe.rs` | uia3-com | app/provider | 2.4's wrapper skip (KTD6) is only valuable if transparent wrappers actually consume logical depth on the Chromium target, and the census must state how many `Group`/`Custom` containers are truly empty | settled Chromium: **81** `Group`/`Custom` control-type nodes, of which **54** carry an empty name **and** empty value (the gated-skip population), 27 named. Depth arithmetic on the same settled tree: 165 raw nodes advance to 111 logical nodes when empty wrappers are skipped - `logical_over_raw` 0.67, i.e. the skip recovers roughly a third of the raw depth on the path to web content. The WPF fixture (83 descendants) shows the gate's restraint: almost no wrappers there, `logical_over_raw` 0.98 | CONFIRMS | the KTD6 gate ships as planned - the skip's value is stated per-tree (Obsidian: 54 empty wrappers) and the native stacks are untouched by it. U7's exit criterion cites this census; wrappers **are** the depth consumer on this target |
| A16-9 | `16-observation/census.ps1` | n/a | api-contract | phases.md §2.4 names `IVirtualDesktopManager` among key APIs as a read-only diagnostic; the plan pre-commits that if it is unreachable through the pinned crates it is dropped from 2.4 and phases.md is corrected | the `VirtualDesktopManager` **CLSID constant exists** in `windows-sys` 0.61 (`Win32_UI_Shell`), but neither `windows-sys` nor `windows` generates the `IVirtualDesktopManager` **interface**. Reaching the interface requires a hand-declared COM declaration - a new dependency 2.4 does not take | CONFIRMS | item 9's pre-committed branch fires: `IVirtualDesktopManager` is dropped from 2.4; U10 corrects phases.md's key-API row to name the CLSID constant and record that the interface is not generated by the pinned crates |
| A16-10 | `16-observation/probe.rs` | uia3-com | app/provider | 2.3's dogfood left the two ref-able arms unexercised - `DataItem` refined to `cell` and `Button` refined to `switch` (H59); U1 must make a stack advertise the patterns or record why | the fixture extension worked: the WPF `ToggleButton` advertises `ControlType.Button` + `IsTogglePatternAvailable` (`button_with_toggle: 1`), so the `switch` arm is constructible and U9 can observe it. The grid arm is subtler: WPF `DataGrid` cells carry `GridItem`/`TableItem` availability but present `ControlType.Custom` (50025), **not** `DataItem` (50029) - 4 elements carry the patterns, 0 with `ControlType.DataItem`; the WinForms `DataGridView` advertises neither pattern on its rows | NEW-EDGE | the `switch` arm lands in the dogfood set with a regression-testable fixture. The `cell` arm's exact shape - `DataItem` + GridItem/TableItem - is **not** reproduced by either grid: WPF exposes the patterns on `Custom`-typed cells (which the role map resolves to `Unknown` today) and WinForms exposes nothing. Recorded so U9's report and U10 decide whether the `cell` refinement should also fire on `Custom`+patterns or the arm stays as-designed; the fixture extension itself is kept for the `switch` arm and the WPF cells are a live test case for the WPF peer path |
| A16-11 | `16-observation/probe.rs` | uia3-com | app/provider | 2.4's settle design (KTD7) rests on A1-5's 13.2x first-contact understatement and the plan pre-commits that if settle exceeds the hardcoded 3 s snapshot deadline, the snapshot command gains a `--timeout-ms` argument | first contact on a cold Obsidian launch is **12** descendants; the settled tree is **165** - a **13.75x** understatement reproducing A1-5's 13.2x. The settle took between 10 s and 25 s on cold launches (non-deterministic), which **exceeds the hardcoded 3 s snapshot deadline** in every measured run. Two stability-shape hazards were also observed directly: a covered Chromium window holds the shell indefinitely (A1-6 reproduced), and a fresh client connection is required to see the grown tree (the triggering client keeps serving the shell) | CONFIRMS | item 11's branch fires: this sub-phase adds `--timeout-ms` to the snapshot command, threaded into both the snapshot and drill-down deadlines, mirroring the ref-action pattern in `src/cli_args/`; the still-thin `platform_detail` names that flag rather than an abstract recommendation. The settle-then-remeasure observation shape (fresh client after a wait) is what U7's activation retry must reproduce |
| A16-12 | `16-observation/probe.ps1` (split-integrity leg) | uia3-com | api-contract | U1 item 12's trust-boundary question: can a lowered-integrity client still read a root and the KTD3 process token of a window a higher-integrity process owns? | the same WPF window read twice - once at the probe's own integrity and once from a child forced to Medium (`S-1-16-8192` via `CreateProcessAsUser` with a Medium integrity label). **Both reads resolve the root and read the process-generation token**; the Medium client produced a complete capture. No `PERM_DENIED`, no denied token read | NEW-EDGE | the observation read and the token read are **not** integrity-gated on this box: a Medium client can observe a High-integrity window's root and token. Recorded as an environment fact (this box stage's High and Medium processes are the same user), not a product contract - the fail-closed design (KTD3's `process_instance: None` emission on a failed token read) still stands for splits this box cannot stage. The elevated-process denial branch did not fire here and remains written into the receiving environment sub-phase's scope |
## Session evidence (R6)
Rows already executed this session rather than produced by a probe script, carried as

View file

@ -1,9 +1,9 @@
{
"Probe": "13-ledger-check",
"CapturedAtUtc": "2026-08-01T20:39:31Z",
"CapturedAtUtc": "2026-08-02T11:05:33Z",
"Question": "does FINDINGS.md satisfy R5, R6 and R7 as a machine-checkable contract",
"Ledger": "probes/windows/FINDINGS.md",
"RowCount": 105,
"RowCount": 117,
"SessionEvidenceRows": 25,
"AreaCoverage": [
{
@ -52,9 +52,9 @@
}
],
"VerdictCounts": {
"CONFIRMS": 52,
"CONFIRMS": 62,
"CONTRADICTS": 8,
"NEW-EDGE": 40,
"NEW-EDGE": 42,
"DEFERRED": 5
},
"DeferredRows": [

View file

@ -98,14 +98,18 @@ $xaml = @'
<TabItem x:Name="tabBravo" Header="Tab-Bravo" AutomationProperties.AutomationId="tabBravo"/>
<TabItem x:Name="tabCharlie" Header="Tab-Charlie" AutomationProperties.AutomationId="tabCharlie"/>
</TabControl>
<DataGrid x:Name="dgvRows" Grid.Row="10" Margin="0,4"
AutomationProperties.AutomationId="dgvRows"
AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="Column">
<DataGrid.Columns>
<DataGridTextColumn Header="Column-Label" Binding="{Binding Label}"/>
<DataGridTextColumn Header="Column-Value" Binding="{Binding Value}"/>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="10" Orientation="Horizontal" Margin="0,4">
<ToggleButton x:Name="btnToggle" AutomationProperties.AutomationId="btnToggle"
Content="Toggle Mode" Width="120" Height="26" Margin="0,0,8,0"/>
<DataGrid x:Name="dgvRows" Margin="0,0,0,0" Width="380" Height="120"
AutomationProperties.AutomationId="dgvRows"
AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="Column">
<DataGrid.Columns>
<DataGridTextColumn Header="Column-Label" Binding="{Binding Label}"/>
<DataGridTextColumn Header="Column-Value" Binding="{Binding Value}"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Grid>
</Window>
'@