feat(ios): split raw and regular snapshot projections behind one capture hint (#1797) - #1926
Merged
Merged
Conversation
…ure hint The private-AX backend interpreted `--raw` as the regular projection: it folded the viewport and scroll clips and dropped sub-pixel decorations before returning, so a raw capture that recovered onto it answered with viewport-pruned nodes labeled raw (#1797 D4). Nothing related the two copies of that decision. Presentation now exposes the two projections it always implied. `presentRegular` folds visibility, eligibility, scope and scroll hints; `presentRaw` is the acquired tree, normalized, narrowed only by a scope or depth the request asked for. Acquisition reads one derived `CaptureHint` instead of the request itself, so what a capture may skip is stated once, beside the reason skipping it keeps the projection complete: scope and its relative depth never narrow, raw depth does (raw depth is traversal depth), and the raw projection never carries `interactiveOnly` — `--raw -i` is the acquired tree. Two structural rules replace the hand-synchronized ones. The raw plan is derived from `SnapshotBackendKind.supportsRawProjection`, so the query sweep — an interactive element query with no hierarchy to return — cannot be planned for a raw request. And presentation compares the requested projection with the hint the acquisition was captured under, dropping that tier with a structured `IOS_SNAPSHOT_PROJECTION_MISMATCH` failure rather than presenting it under the requested label. Declared residue: a regular `--depth` request still cuts the traversal at that depth while regular presentation emits collapsed depth, so a node that would present within the limit can be dropped. The cut is what keeps `--depth 1` probes cheap; making it complete is the open visible-depth frontier obligation. Refs #1797 (migration step 3, raw-projection delta).
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Member
Author
|
Reviewed the acquire/present D4 delta at 0425726. The projection is now carried through a single capture hint, raw plans only select hierarchy-capable backends, and a backend cannot relabel a regular capture as raw. The targeted XCTest regressions, live private-AX differential, and all required CI lanes are green. Merge-ready. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Next semantic delta of #1797 (migration step 3, after eligibility #1850 and scope #1855): the two projections presentation always implied, and one derived input for acquisition.
D4, the bug this closes. The private-AX backend interpreted
--rawas the regular projection.appendPrivateAXNodefolded the viewport and scroll clips and dropped sub-pixel decorations during acquisition, so asnapshot --rawthat recovered onto that backend — the route an app whose XCTest tree capture fails takes — returned viewport-pruned nodes labeled raw. The one view whose purpose is showing what the pruned view hid was itself pruned, by a second copy of a decision nothing related to the first.What replaces it.
SnapshotPresentationnow exposespresentRegular(clip-folded eligibility, scope, scroll hints) andpresentRaw(the acquired tree, normalized, narrowed only by a scope or depth the request asked for). Acquisition backends no longer readPresentationOptions; they read aCaptureHintderived once bySnapshotPresentation.captureHint, so what a capture may skip is stated in one place beside the reason skipping it keeps the projection complete:projectiondepthdepthunder--scopeinteractiveOnlycustomActionsTwo structural rules replace the hand-synchronized ones. The raw plan is now derived from
SnapshotBackendKind.supportsRawProjectioninstead of a second hand-maintained list, so the query sweep — an interactive element query with no hierarchy to return — cannot be planned for a raw request. And presentation compares the requested projection against the hint the acquisition was captured under, dropping that tier with a structuredIOS_SNAPSHOT_PROJECTION_MISMATCHfailure (classified as a runner bug, deliberately not an AX failure, so it cannot take the rethrow/fail-closed terminal route) rather than presenting a capture under a label it does not deserve.Behavior changes
snapshot --rawrecovered onto private-AX returns the acquired tree: offscreen and clipped subtrees, structural wrappers, and decorations are all present, at traversal depth.--depthstill narrows it.snapshot --raw -ireturns the acquired tree instead of an interactive-filtered one.-inarrows the regular projection; combined with--rawit used to produce a third membership rule that differed per backend (the tree backend filtered, private-AX did not) — exactly the divergence class this issue exists to remove. Same direction as Android's C3 change in refactor(android): raw is the acquired tree; one presentation for dialog recovery; residues declared (#1832 C3) #1865.-ioutput are unchanged on every backend.Evidence
Unit (17 targeted runner tests green, iPhone 16 Pro / iOS 26.2), 6 of them new, added to the iOS PR lane list;
pnpm check:xctest-selectiongreen (165 declared, 0 unreachable).Non-vacuity, each proven by rebuilding the runner with the production code mutated:
testPrivateAXRawProjectionKeepsEveryAcquiredNodeandtestPrivateAXRawProjectionAppliesRequestedTraversalDepthfail (["Application", "ScrollView", "Button"]vs["Application", "ScrollView", "Button", "Image", "Button"]); both regular private-AX tests still pass.testPresentationRefusesAnAcquisitionCapturedForTheOtherProjectionfails, and the failure message shows the raw request being answered with the regular capture's two nodes: the D4 shape, reproduced.querySweep.supportsRawProjectiontotrue→testRawDiagnosticPlanCarriesOnlyBackendsThatCanServeRawfails withqueriesin the raw plan.Live differential, Settings on iPhone 16 Pro / iOS 26.2, this branch's runner vs a runner built from
main, both with the raw plan pinned to private-AX so the recovery tier serves--rawdeterministically (scaffolding, not in the diff), each variant on its own state dir and daemon:snapshot73 = 73 identical, tree-backedsnapshot -i18 = 18 identical; private-AXsnapshot -i --actions20 = 20 identical, private-AXsnapshot --actionson a scrolled screen 59 = 59 identical.snapshot --rawon private-AX: 158 → 167 nodes on two independent runs, 0 nodes lost. All 9 gained nodes are unlabeled (named nodes 20 → 20, unlabeled 112 → 121) — the 1pt separators and structural wrappers the regular projection folds away. Settings' private-AX tree is mostly on screen, so this is the small end of the delta; the clipped-subtree case is what the fixtures pin.--actionspair differed (57 vs 58 nodes) because a Settings follow-up banner ("Ready for Apple Intelligence") appeared in the later run and shifted every row by 35pt — device state, not projection: the same pair's-icapture is identical, and the earlier same-state pair is identical too.Sessions closed, daemons stopped, throwaway simulator shut down.
Repo gates:
pnpm check:affected --rungreen ("all runnable checks passed"),pnpm check:xctest-selectiongreen, oxfmt/oxlint clean.One lane I could not run locally: the macOS host lane never starts on this machine —
AgentDeviceRunnerUITests-Runner … Early unexpected exit, operation never finished bootstrapping (Test crashed with signal kill before establishing connection), 0 tests executed. A runner built frommainfails identically on the same machine, so it is environmental, not this change; the same tests run in the iOS simulator lane above and in CI.Docs
ADR 0004 gains the acquire/present third layer (two projections, the hint's conservatism table, the two structural rules) and the declared depth residue;
CONTEXT.mdgains capture hint; CHANGELOG andwebsite/docs/docs/commands.mddescribe iOS--rawas the acquired tree next to Android's.What #1797 still needs after this
hittablewith thehittable-consumer inventory — 118 production references today.treepin + penalty-clear hook, per-PR fixture differentials, nightly live differentials, parity ratchets.--depthrequest still cuts the traversal, while regular presentation emits collapsed depth.Refs #1797.