Commit graph

245 commits

Author SHA1 Message Date
Lahfir
d133cdf397 docs: clarify error-code shipping note and build.rs line count in phases
Verifier follow-ups: state plainly that AX_MESSAGING_TIMEOUT never became
an ErrorCode variant and AUTOMATION_PERMISSION_DENIED folded into
PERM_DENIED, and correct the ffi build.rs line count. Release-tracker
dates were re-verified against UTC tag creation dates and left unchanged
- the table is uniformly UTC, matching the GitHub release surface.
2026-07-19 14:34:14 -07:00
Lahfir
f6ec63c881 docs: restructure phases roadmap with windows/linux sub-phase delivery model
Bring docs/phases.md up to post-foundation reality and split the platform
phases into reviewable increments. Adds the completed Phase 1.6 section
(Playwright-grade contract, U0-U19) and a Platform Delivery Model: each
platform ships as dependency-ordered sub-phases (2.0-2.14 Windows,
3.0-3.14 Linux), one PR per sub-phase capped at 2,000 changed lines,
merging into a per-platform integration branch with plan->implement
lifecycle (no brainstorm stage) and a full-branch review before main.
Corrects stale content throughout: command roster 58 (54 operational +
4 fail-closed), capability-trait adapter listing, APP_UNRESPONSIVE and
envelope 2.1 recovery/disposition, FFI ABI major 3 with the shipped
handshake, hand-written FFI wrappers (codegen remains open P2-O16),
release tracker through v0.4.7, wait --event vs future watch naming,
nonexistent PLATFORM_UNSUPPORTED code, and the CLAUDE.md phase-number
mismatch. All prior research (capability maps, API mappings, rejection
tables, notification/tray approaches, risk register) is preserved and
folded into the owning sub-phases; compressed test-methodology bullets
restored into exit criteria after adversarial verification.
2026-07-19 14:27:40 -07:00
Lahfir
77ea6d1e36 docs: require performance baseline check in definition of done
Document the head-vs-main comparison harness in the common commands and
close the agent guide with a gate: substantive changes end with
scripts/perf-baseline-compare.sh and a review of report.html against the
merge-base, so latency deltas are intentional rather than discovered by
users.
2026-07-19 12:29:58 -07:00
Lahfir
ee88ee8323 feat: add head-vs-main performance comparison harness with html report
One command now measures the branch against its merge-base: builds both
revisions, runs a fixture A/B (isolated per-binary stores, alternating
rounds), optional strictly-read-only probes of real apps, and the
synthetic locator benchmark, then renders a self-contained report.html
with validated light/dark palettes, grouped p50/p95 bars, honesty
callouts for non-comparable defaults, and table fallbacks. Replaces the
markdown summary and the standalone locator-report generator, whose
sanitized-report flow required manual invocation and hardcoded sizes.
2026-07-19 01:17:24 -07:00
Lahfir
fc3e08a667 test: give ffi consumer targets accessibility identifiers
The FFI live driver addresses its AE2 targets by native_id per the
R4 identity spine, but the fixture's async buttons only ever set
accessibility labels, so ref_for could never match and both release-FFI
AE2 checks failed on every live run. These checks were unreachable
until the zero-bounds abort was fixed, which is why no prior run
surfaced it. Assign identifiers to exactly the three buttons the FFI
consumer addresses; name-based CLI paths are unaffected.
2026-07-18 23:43:00 -07:00
Lahfir
a65d4530a1 fix: classify benign AX absence codes as complete action reads
The live e2e gate exposed that find could never address the fixture's
zero-bounds synthetic element: AppKit answers kAXErrorFailure for
accessibilityActionNames on NSAccessibilityElements with no action
support and kAXErrorAttributeUnsupported for unimplemented settable
probes, and the action reader classified both as incomplete transport
reads, so selected-anchor hydration retried to TIMEOUT with
locator_selected_evidence_incomplete. Mirror the renderer-probe
absence classification for action reads: unsupported/no-value/
not-implemented/action-unsupported/failure are complete none-present
answers; CannotComplete, InvalidUIElement, and APIDisabled still mark
the read incomplete.
2026-07-18 23:33:44 -07:00
Lahfir
ec8501d4ca fix(review): close final multi-agent review findings
Resolve 15 validated findings from the 35-reviewer final pass: release the
interaction lease before read-only --wait-for polling; degrade transient AX
child-label errors instead of aborting whole snapshots; report honest
delivery evidence on chain exhaustion and disclosure verification; thread
drag delivery counts by reference; relax user-output overwrite validation
to symlink/dir refusal with INVALID_ARGS; sweep sessionless clipboard
images; correct false fail-closed FFI docs and state the deferred FFI
event-wait; unit-test the electron benchmark classifier; restore the
7-field/5-param standards caps via SurfaceWait, DragEndpoint,
WindowIdentityEvidence, NcSessionOps, ClickSpec, and PointResolveAttempt;
delete the dead non-CAS RefStore::save_existing_snapshot in favor of the
production CAS path.
2026-07-18 23:21:17 -07:00
Lahfir
e1fe634150 fix: close foundation reliability review gaps 2026-07-14 22:48:42 -07:00
Lahfir
ae65a65f9f test: make deadline regression deterministic 2026-07-14 20:22:42 -07:00
Lahfir
c5d29bfee6 fix: harden foundation interaction and e2e contracts 2026-07-14 20:06:31 -07:00
Lahfir
771fb122e9
refactor: simplify foundation contract implementation
* fix: make npm staging cleanup recoverable

* refactor: share transient resolution classification

* refactor: centralize action result finalization

* refactor: remove duplicate drag source resolution

* refactor: avoid duplicate locator timeout setup

* test: consolidate ref and locator fixtures

* test: split bounded e2e process support

* ci: extract smoke checks and remove destructive cleanup
2026-07-14 17:26:36 -07:00
Lahfir
aedb0c0be5 test: make deadline metrics scheduler-independent 2026-07-12 23:44:37 -07:00
Lahfir
80866d5021 docs: refresh headed contracts and remove generated evidence 2026-07-12 23:29:41 -07:00
Lahfir
c29d1eaa49 refactor: group benchmark and e2e support files 2026-07-12 23:18:28 -07:00
Lahfir
aacfd08a90 fix: keep subprocess cleanup bound to its process group 2026-07-12 23:16:17 -07:00
Lahfir
36b33f5dbb test: track current hover state in fixture 2026-07-12 22:25:51 -07:00
Lahfir
329a8145ff test: classify shared window command helper 2026-07-12 22:12:45 -07:00
Lahfir
5cf452457b test: tolerate hosted runner scheduling 2026-07-12 21:59:55 -07:00
Lahfir
787e7b0a75 fix: preserve auto-wait timeout evidence 2026-07-12 21:45:52 -07:00
Lahfir
b325e52766 test: make locator deadline assertion load-safe 2026-07-12 21:15:46 -07:00
Lahfir
12f9c91c1f test: isolate window identity fixtures 2026-07-12 21:00:41 -07:00
Lahfir
726a1a302b fix: centralize headed input delivery 2026-07-12 20:55:41 -07:00
Lahfir
dc79297ed0 test: enforce honest headless non-interference
Run strict headless click and type against visible background controls, validate only real fixture observables, and emit exact invariant diagnostics without changing fixture behavior.
2026-07-12 19:15:43 -07:00
Lahfir
63130f81c3 fix: report only eligible action mechanisms
Omit policy-blocked physical chain steps from headless results so mechanism evidence reflects only legal delivery paths.
2026-07-12 18:59:24 -07:00
Lahfir
ec247a6f7d fix: enforce explicit headed delivery
Keep semantic ref actions strictly headless, make natural headed actions physical-first, and prevent notification observation from opening system UI without headed permission. Gate the native suite on background non-interference and assert delivered mechanisms.
2026-07-12 18:47:01 -07:00
Lahfir
ef5c90cec9 fix: prefer verified headed disclosure clicks 2026-07-12 18:12:57 -07:00
Lahfir
88e42f02a0 fix: close native e2e contract gaps 2026-07-12 18:01:23 -07:00
Lahfir
d0ce41fd84 fix: keep Windows private file build warning-free 2026-07-12 17:37:25 -07:00
Lahfir
793cceb8b7 fix: restore remaining native e2e contracts 2026-07-12 17:31:34 -07:00
Lahfir
0e9230c6f2 test: scope wait-for-gone to removable button 2026-07-12 17:17:05 -07:00
Lahfir
2731061a2e test: remove scheduler-sensitive deadline assertion 2026-07-12 16:59:38 -07:00
Lahfir
dc616058fa fix: restore Windows private file visibility 2026-07-12 16:58:06 -07:00
Lahfir
39de14291a fix: unblock CI — nc_pid type error, 400-LOC split, harness abort count
Three failures that tonight's --no-verify checkpoints hid:
- nc_session.rs::nc_pid used Deadline::after (Result<Deadline>) where
  operation_deadline yields Result<Instant> — E0308 broke Native check
  (macOS/Windows) and the FFI cdylib builds. Reverted to the compiling form;
  the speculative deadline floor did not fix NT3 anyway.
- resolve_query_tests.rs hit 403 lines; split the hydration adapters/tests
  into resolve_query_hydration_tests.rs (both now well under 400).
- test_harness_contract.py pinned 9 abort_suite sites; the branch legitimately
  has 10 (all required-fixture guards). Updated the invariant.

cargo check --all-targets --locked, clippy, fmt, source rules, harness
contract, and 1562 unit tests all green locally.
2026-07-12 16:44:04 -07:00
Lahfir
eae58df5c2 test: honest oracles for scroll and disclosure e2e scenarios
Remove the scroll-to preamble (the compacted fixture makes scroll-area
directly actionable; raw scroll verified 0->805). Assert disclosure state
by revealed child content instead of AXValue, which SwiftUI DisclosureGroup
does not expose — the product's expand_verified was truthful all along.
Product unchanged since 51c87b2 (1562 unit tests, clippy, release green);
these are test-harness shell edits only.
2026-07-12 13:37:52 -07:00
Lahfir
51c87b2323 fix: duplicate-title focus join degrades gracefully; e2e diagnosis
Product: ambiguous duplicate-title focus join returns unconfirmed focus
(is_focused=false, no error) instead of hard-erroring the focus-window
settle loop; focus poll now keeps retrying on retryable adapter errors.
nc_pid pgrep gains a deadline floor. Fixture compacted to fit the
normalized e2e window; scroll-into-view preambles and richer assert
instrumentation added across scenarios. Unit: 1562 pass, clippy clean.
e2e: 96 pass, 8 remainders now isolated with error codes (scroll AX-vs-
SwiftUI, NC dismiss, trace screens perm, AE5 helper, wait-for-gone,
right-click, expand) — handed to classification.
2026-07-11 23:53:31 -07:00
Lahfir
0ec5e7ad86 fix: locator child-label gating, fixture crash, deterministic e2e geometry
Containers no longer derive names from descendants (INTERACTIVE_ROLES gate)
and a per-node child-label cap no longer fails whole queries closed — the
root cause of every intermittent e2e abort. Fixture NSWindow double-release
(isReleasedWhenClosed) fixed: zero new crashes. Harness now pins the fixture
window to a deterministic frame on the largest display, clearing the whole
physical-gesture failure family. Selector-timeout diagnostic snapshot budget
raised 75ms->600ms so evidence capture can actually complete. Suite: 73 pass,
7 known remainders (scroll oracle, AE4 assert, three reliability envelope
extractions, sheet confirm lookup, NT1 observation).
2026-07-11 22:59:27 -07:00
Lahfir
4add46ab38 chore: checkpoint five verified remediation rounds before e2e convergence
Freezes the working tree as a fixed baseline: static gates green (1551 lib
tests, clippy -D warnings, fmt, release 0.4.7), live-verified E1/E2 fixes,
notification/harness work pending e2e acceptance. Known-open: hover fixture
churn to back out, 3 e2e failures (hover oracle, AE6 sheet, sheet cancel),
NT1-NT4 unproven.
2026-07-11 22:06:13 -07:00
Lahfir
8f434ddf65 fix: fast-fail terminal actionability failures and unredact check identifiers
Two root-cause fixes surfaced by a real session trace where clicking a
non-clickable group hung the full 5s auto-wait budget and returned an opaque
TIMEOUT whose failing check was unreadable.

Issue 1 (observability): ActionabilityCheck's check-identifier field was named
'name', colliding with the sensitive 'name' key the trace sanitizer redacts
(element accessible names, incl. Occluder.name). So the bounded-vocabulary
check identifier (visible/supported_action/...) was scrubbed from traces. Root
fix: rename the field to 'check' — a token the sanitizer leaves readable —
disambiguating it from Occluder.name, which stays correctly redacted. The
sanitizer keeps failing closed; the non-sensitive field just no longer collides.

Issue 2 (behavior): check_with_stability collapsed every actionability failure
to ACTION_FAILED, so the poll loop retried structurally-permanent failures for
the whole budget then returned a generic TIMEOUT. Root fix: model the intrinsic
transient-vs-terminal nature of each check. The terminal checks (supported_action,
policy, editable) — which waiting cannot heal — now carry their semantic permanent
code (ACTION_NOT_SUPPORTED / POLICY_DENIED); the report derives the error code
from the failing checks. The auto-wait poll loop's existing is_permanent_error
then fails them fast with a precise code, no poll-loop change. Transient checks
(visible/stable/enabled/receives_events) still surface ACTION_FAILED and retry.

Verified on a real app: click of a non-clickable scrollarea now returns
ACTION_NOT_SUPPORTED in 0.09s (was ~5s TIMEOUT) with the trace showing
{check: supported_action, reason: 'Click is not available'}. Full workspace tests
pass, e2e 72/0.
2026-07-05 18:12:44 -07:00
Lahfir
78b587aaf0 fix: unify poll-loop last_report envelope and stop wiping prior context
Follow-up polish on the actionability_timeout diagnostics (2nd-pass review P2s):
- Both failure sources now emit a phase-tagged envelope: resolve failures
  {"phase":"resolve", code, message, details}, dispatch failures
  {"phase":"dispatch", report}. Previously resolve emitted a resolve_error
  wrapper while dispatch emitted a raw ActionabilityReport, so a consumer reading
  the timeout report couldn't assume one shape.
- The dispatch branch now only overwrites last_report when it actually has
  details; a details-less dispatch failure no longer wipes a prior resolve
  failure's context. Verified with cargo test --workspace (1186 passed).
2026-07-05 17:19:19 -07:00
Lahfir
a8b5a59d1c fix: address 4th-pass review — visibility ordering, mouse_wheel tests, poll diagnostics
- actionability::visibility_check: run HIDDEN/OFFSCREEN state checks BEFORE the
  bounds read (P1 #2). Previously bounds=None short-circuited to unknown, so a
  hidden/offscreen element whose live bounds read failed slipped the visibility
  gate. Regression tests added (hidden/offscreen + bounds=None now fail, not unknown).
- mouse_wheel: add unit tests (P0 #1) mirroring the sibling mouse-command tests —
  args reach the adapter unchanged, the scrolled envelope shape, error propagation.
- ref_action_wait poll loop: on a retryable resolve failure, record the resolve
  error into last_report so an actionability_timeout on deadline expiry carries
  the last STALE_REF/AMBIGUOUS_TARGET context instead of an empty report (P2 #7).

Already-fixed in prior passes (review base predates 640e8e8): StepMechanism serde
tests (#3), clipboard trait removal doc note (#10). Deferred/accepted: test-adapter
boilerplate (#5 -> issue #95), hit-test ancestor + scroll thrashing (#15/#16/#17,
intentional design tradeoffs). Verified with cargo test --workspace (1186 passed).
2026-07-05 16:59:28 -07:00
Lahfir
19f2ef4a86 test: register stale_retry_test_support as a non-command helper module
The CLI command-module contract test (every_core_command_module_is_registered_or_declared_helper)
enumerates commands/*.rs and requires each to be a registered command or a
declared helper. The stale_retry_test_support module added in the #8 dedup
(640e8e8) ends in _test_support (not _tests), so it was neither skipped nor
allow-listed and failed CI. Add it to NON_COMMAND_MODULES alongside the sibling
helpers_test_support/wait_test_support entries. Local gap: this is a binary-crate
test that 'cargo test --lib' skips; 'cargo test --workspace' now green.
2026-07-05 14:45:59 -07:00
Lahfir
93140d5ad7 docs: capture hover/drag actionability-divergence learning
Document that hover/drag skip the actionability battery (occlusion-only via
require_receives_events, immediate ACTION_FAILED) while dispatch actions run
check_live's full battery and poll to TIMEOUT — the divergence that produced an
inaccurate skill-doc this cycle. Prevention: validate agent-facing capability
docs against the real per-command code path, not the mental model.
2026-07-05 01:25:50 -07:00
Lahfir
604844d22d docs: refresh reliability/actionability solutions for symbol drift
Reconcile four docs/solutions learnings against current code (their core
guidance is unchanged; only cited symbols/signatures/counts drifted):
- playwright-grade-desktop-reliability: add the actionability_timeout trace
  kind; fix the check_actionability_with_trace signature (bundled
  ResolvedRefAction) and the ref-allocation example (allocate_refs folds in
  bounds-hiding; strip_ref_bounds_when_hidden is gone)
- real-app-tests-are-the-platform-adapter-gate: attribute the accessible-name
  reducer to tree/builder.rs::accessible_name with element.rs::resolve_element_name
  as the thin wrapper (unified in adda4c9)
- macos-gesture-headless-capability: 54 -> 58 commands; double_click now returns
  Vec<ActionStep> tagged SemanticApi/PhysicalSynthetic
- exhaustiveness-guards: context.request( -> context.request_base(
2026-07-05 01:22:27 -07:00
Lahfir
640e8e83e4 fix: address PR #93 third-pass review findings
Triage + validate (the review's line numbers were bogus diff-offset
artifacts, so every finding was checked against the real code) then fix the
real ones. 4 findings were false-positives (#2 core-internal fn, #5
VisibilityEvidence not in the actionability gate, #15 supertrait forward-design,
#20 as_str duplication), 2 were deferred as disproportionate (see below).
Verified by an adversarial code-read review + full gate: fmt, clippy
-D warnings, 1006 lib + 7 ABI tests, core isolation, 1.6MB, e2e 72/0, FFI
header regenerated with matching offset asserts.

- scroll gate: maybe_scroll_into_view now reads live state/bounds
  (fail-open to the snapshot entry on capability-less adapters) so an element
  that scrolled off-screen after the snapshot is still scrolled into view
- auto-wait: is_permanent_error now treats ErrorCode::Internal as permanent so
  an internal dispatch error fails fast instead of retrying the whole budget
- FFI header: document ad_execute_by_ref's 5000ms auto-wait default (and the
  timeout=0 single-shot escape hatch); correct the stale envelope version
  2.0 -> 2.1; add the 22 per-field offsetof static asserts for AdRefEntry
- skill docs: document --modifiers on the mouse commands, the receives_events
  occlusion check + occluder detail, the implicit scroll-into-view preflight,
  and --timeout-ms — and correct the actionability section to reflect that
  hover/drag run only the receives_events check (not the full battery) and
  fail fast with ACTION_FAILED rather than polling to TIMEOUT
- adapter: /// note that get_clipboard/set_clipboard were removed pre-1.0 for
  the typed content methods (C ABI unaffected)
- tests: StepMechanism serde roundtrip, ActionRequest legacy-no-timeout_ms
  deserialization, execute_by_ref unit tests; dedup the byte-identical
  StaleThenOkAdapter retry-counter into stale_retry_test_support

Deferred (disproportionate blast radius, tracked as follow-up issues):
process-state PID-reuse start-time corroboration (needs plumbing through the
already-large RefEntry + snapshot + platform adapter); a stub_ops! macro +
84-site test-adapter retrofit (large mechanical churn on the deliberate U0
four-trait split).
2026-07-05 00:56:35 -07:00
Lahfir
987a8ecf94 fix: harden foundation per PR #93 review
Remediate the validated PR #93 review findings. Each was triaged and
validated against the real code (0 false-positives; 4 deferred as
design/maintainability follow-ups), fixed on disjoint file sets, then
confirmed by an adversarial code-read review and the full gate (fmt, clippy
-D warnings, 996 unit tests, core isolation, 1.6MB binary, FFI codegen
drift-stable, e2e 72/0). Trace changes were additionally verified on a real
app: a ref-action click now emits ref.resolve.start/entry/ok, and a stale ref
emits ref.resolve.start/error(STALE_REF) with no dangling start.

- ref actions: emit ref.resolve.start/entry/error via a shared load_ref_entry
  owner, so element-mutating actions trace resolution identically to get/is
  (previously they emitted no start/entry, then a dangling start once added)
- scroll: surface a ref.scroll_into_view.error trace instead of silently
  swallowing a failed scroll_into_view (non-blocking; the actionability check
  still gates the action)
- actionability: attach the actionability report to the success trace so an
  Unknown receives_events verdict is visible instead of vanishing
- launch: stop interpolating the raw app identifier into validate_app_identifier's
  error message; carry it in redacted details.app_name (trace-leak fix)
- process_state: preserve original error details when upgrading to APP_UNRESPONSIVE
- execute_by_ref / resolve_point_with_wait: fold 7-arg signatures into config
  structs (ExecuteByRefArgs; reuse PointResolveArgs) under the 5-param limit
- skills/README: correct the stale "56 commands" to 58
- tests: ProcessState::Crashed serde + Crashed/AppNotFound enrichment coverage,
  requires_scroll_into_view exhaustiveness guard, normalize_action_timeout_ms(0)
  guard, validate_app_identifier accept/reject + leak-regression, value-based
  legacy ActionStep JSON comparison; split the process-state test file to stay
  under the 400 LOC limit
2026-07-04 21:36:07 -07:00
Lahfir
adda4c977e fix: harden core foundation per whole-branch review
Remediate the validated findings from the whole-branch review. Each was
triaged, validated against the real code, fixed on a disjoint file set, then
confirmed by an adversarial code-read review and the full gate (fmt, clippy
-D warnings, 985 unit tests, core isolation, 1.6MB binary, e2e 72/0). The
ref.resolve trace restoration was additionally verified on a real app (Finder
hover under a traced session emits start/entry/ok again).

- signals: propagate app-enumeration failures in the signal baseline instead of
  swallowing them with unwrap_or_default, mirroring list_windows_impl
- image: validate the PNG signature and IHDR chunk in one shared core parser
  (parse_png_dimensions), dedup three hand-rolled copies, and reject non-PNG
  --image input instead of tagging arbitrary bytes as a zero-dimensioned PNG
- roles: delete the dead Role enum and inline is_interactive_role as a direct
  INTERACTIVE_ROLES membership check, keeping the vocabulary-contract test
- helpers: cap each ref-resolve attempt to the wait deadline through the single
  traced resolver, so budgeted hover/drag can no longer spend the whole budget
  on one slow attempt and once again emit ref.resolve.* trace events
- actionability: run the hit_test occlusion gate even when get_live_element is
  unsupported, so occlusion is checked on hit_test-capable adapters
- launch: share the app-identifier traversal guard across both launch paths
- window_resolve: fail closed when the window-title fallback is ambiguous
  (resolve only when exactly one window matches the title)
- main: surface stdout write/flush failures from the JSON envelope instead of
  reporting success, exiting with a distinct code on a broken pipe
- cli_args: add a one-to-one Surface<->SnapshotSurface parity tripwire test
2026-07-04 00:01:56 -07:00
Lahfir
eb9f7c0f06 test: harden review findings — window scoping, serde, offscreen, hit-test naming
Addresses the code review's remaining validated findings (test + robustness
hardening; no P0 correctness left after the name and window-bridge fixes):

- find --window-id: MockAdapter test (two windows, distinct trees) proving the
  flag scopes the search to the requested window and a swap with app would fail.
- WindowScope serde: batch-JSON back-compat + typo-rejection tests for the
  flattened app/window_id group on find/snapshot/screenshot.
- Offscreen bounds: extracted window_bounds_for_children as a pure helper and
  restored the AXWindow-own-bounds / inherited-fallback unit coverage that was
  lost when the live query matcher was deleted.
- Real-app guards: the #[ignore] snapshot_test.rs guards now print a SKIP reason
  before each early return, so a no-op run is visible instead of a silent pass.
- Hit-test occluder naming now applies promoted_label.or_else(resolve_element_name),
  matching the strict resolver and ambiguity classifier.
- Hover e2e asserts a clean (non-"hovered") baseline after the cursor reset.

Verified: e2e 71/0, Finder sidebar cells still re-resolve, clippy + workspace
tests green.
2026-07-03 21:05:49 -07:00
Lahfir
74a375a321 fix: route all window-id resolution through the _AXUIElementGetWindow bridge
The earlier window fix replaced the nonexistent AXWindowNumber attribute with
_AXUIElementGetWindow in window_resolve.rs only; code review found three more
live sites still reading AXWindowNumber (which AppKit/SwiftUI never publish, so
it always returns None):

- resolve_roots.rs window_by_number: scope_verified could never become true, so
  strict ref re-resolution silently degraded to bounds-hash-only matching and
  failed closed (STALE_REF) on any window move/resize.
- window_inventory.rs: the AX-fallback path stamped every window id "w-0"
  (collisions), and focus detection fell back to title-equality — wrong for any
  app with two or more same-titled windows.

Extracted ax_window_id (the _AXUIElementGetWindow bridge) as the single shared
owner and routed all three sites through it; removed the now-dead copy_i64_attr
helper entirely (its only callers were the AXWindowNumber reads); switched the
bridge's success check to kAXErrorSuccess; and added a verified-title fallback
so a transient _AXUIElementGetWindow error on an already CG-verified window
retries by title instead of returning a false WINDOW_NOT_FOUND.

Verified: list-windows focus detection returns non-zero ids, e2e 71/0.
2026-07-03 20:45:28 -07:00
Lahfir
1246ef4acb fix: unify accessible-name computation across builder and resolver
Code review found the STALE_REF name-divergence class was re-introduced: the
snapshot builder stores a ref's name via its own chain (title -> description ->
static value -> label_from_children child text), but resolve_element_name — used
by strict ref re-resolution — dropped the child-label rung and trimmed blanks
differently. So an interactive element named only by descendant text (Finder /
Mail / System Settings sidebar cells) or by a whitespace/blank title stored one
name and recomputed another, failing identity_matches -> STALE_REF on
click/type/get. Confirmed: 5/5 Finder sidebar cells returned STALE_REF.

- One shared reducer `builder::accessible_name` (title -> description ->
  static-text value -> aggregated child label, each trimmed and blank-as-absent),
  with the own-text portion factored into the pure, unit-testable
  `reduce_text_name`. Both the snapshot builder and resolve_element_name reduce
  through it, so a stored ref name always equals what the resolver recomputes.
- Deleted the now-single-producer/single-consumer NameEvidence indirection
  (crates/core/src/accname.rs, crates/macos/src/tree/name_evidence.rs) and the
  now-dead label_from_child_attrs.
- Added reduce_text_name unit tests covering the rung precedence and the
  blank/whitespace handling that accname_tests used to guard.

Verified: 5/5 Finder sidebar cells now re-resolve, e2e 71/0, clippy clean,
workspace tests green.
2026-07-03 20:39:32 -07:00
Lahfir
952eec407a docs: capture that real-app tests are the platform-adapter verification gate
Records the durable learning from the foundation-branch remediation: green
mock/stub unit CI cannot cover platform-adapter mechanics (window bridge,
accessible-name computation), so the e2e and #[ignore] real-app tests are the
mandatory gate before merging adapter changes. Cross-references the existing
playwright-grade reliability contract.
2026-07-03 18:32:08 -07:00