Ranking a mutating find's candidates asked the whole tree the same three
questions once per candidate: same-rect descendants filtered every node,
the nearest hittable ancestor rebuilt a full index map, and the
overly-broad-ancestor check re-filtered every node for viewport roots.
With m matches over an n-node capture that is O(m x n) full-tree work
before find can act or refuse.
buildActionableTouchTopology reads those three collections in one pass
(nodes by index, children by parent index, normalized viewport-root
rects). preferOnscreenMatches builds exactly one per multi-match pass and
threads it through every score; resolveActionableTouchResolution takes it
as an optional argument so one-off interaction callers keep the cheap
two-argument shape. findNearestAncestor gained the same optional prebuilt
map its snapshot-presentation sibling already accepted, and
classifyActionableTouchCandidates now reuses one topology instead of
building a bare index map and re-resolving per candidate.
Score inputs, ranked order, area and input-order tie-breaks, ambiguity
refusal, and --first/--last are unchanged; only the derivation is shared.
Observed red first: with the wiring hunk removed, the new ranking
regression reports builder calls 0 (expected 1) and 64 filter + 32 map
whole-tree scans (expected 0) over 32 candidates.
The topology docstring records two seams the reviewer asked for. #1690
names src/snapshot/snapshot-processing.ts as findNearestAncestor's home;
that path is gone and packages/contracts/src/snapshot-tree.ts is the seam
that replaced it, so the issue's file list is drifted rather than a second
site to change. And viewportRootRects is not interchangeable with
snapshot-visibility's precomputedViewportRects: normalizeRect drops
negative width/height where hasValidRect keeps them, which changes which
rect wins pickLargestRect.
Refs #1690
Closes #1690.
Summary
Find ranking rebuilt per-candidate state on any ≥2 raw matches: a fresh O(n) ancestor map per candidate,
nodes.filterper same-rect step, and a viewport re-filter per candidate — plus the same resolution re-run again inclassifyActionableTouchCandidates, and replay'sresolveRecordedTargetdouble-scanning the tree on both resolved and unresolved legs.src/core/actionable-touch-topology.tsbuilds all three whole-tree lookups (nodes-by-index, children-by-parent, viewport-root rects) in one pass;resolveActionableTouchResolutionthreads it through all three previously-quadratic sites. Omitting it keeps the exact old scan path for one-off callers.find-match-ranking.ts;preferOnscreenMatchesis the single topology build site, with the<2and no-root-rect early returns preserved.findNearestAncestor(contracts) gains the optional prebuilt map its presentation sibling already had.resolveSelectorChainDomainreturns the matched-node set the deciding pass already collected;resolveSelectorChaindelegates to it and keeps its published shape;listSelectorChainMatchesstays for its three other callers.Validation
undefinedfor the builder turns the test red).resolveSelectorChainDomain. Review finding F1 (traversal counter blind toflatMap) addressed in-commit620a55e03; F2 (higher-traffic double scan inresolve-with-policy.ts) filed as selectors: resolveSelectorChainWithPolicy still double-scans the tree for disambiguate/fail-closed rows #1970.check:affectedgreen (602 files / 4770 tests at implementation head), fallow clean, format clean.Internal complexity improvement only — no user-facing behavior change; docs/skills deliberately untouched.