Repository navigation
Snapshot membership step 5: inventory suppressNode ejections and give them typed dispositions #2754
Description
Activity
- added a commit that references this issue
on Sep 22, 2026 Landed as a stacked pair: #2758 (steps 1–3) → #2759 (step 4). #2758 stacks on #2757, which retires the stale
#2747over-ceiling approval row that otherwise breaks the eager-closure gate on every branch offmain.Step 3 survey (wrapper / under-clip shapes). In every captured tree, a list's own indicator has a scroll-typed parent, so parent-edge and the old walk agree:
rows.fixtures.tsscroll-bar cells haveTableparents; the visibility/mobile engines' captured trees likewise. No real tree was found where a list's own indicator sits under a non-scroll wrapper — the under-clip shape is currently hypothetical. It is pinned as an accepted, safe direction (no band, rows survive) rather than wired to a speculative wrapper-tolerant outlet, which would re-introduce the inference ADR 0026 removes.Step 5 inventory (deferred). ~23
suppressNode(...)call sites across ~10 rules (noise-structural,web,noise-overlay,noise-redundancy,scroll,action-shelf,transitions,noise-viewport,rows, search/noise), with at least two also rewriting rects. This is the "redesign only what the inventory justifies" ledger and is not required by the observable completion conditions, so it is left as a separate sequenced follow-up.Completion-condition caveat. The issue's "TextView and WebView shapes fail on origin/main" is unsatisfiable as written: #2740 already stopped the old walk at
TextView, so theTextViewshape is green on both and stays as a preservation pin. The general class is proven red-on-main byWebView, pagedCell, and the wrapper under-clip cases.Step-3 counts from real runner captures (
snapshot -i --raw, pre-projection; iPhone simulator, iOS 26.2). Both Agent Device Tester sims were claimed by other worktrees, so this covers system surfaces that exercise the same path without an app build: nativeUITableViewlists and a realWKWebView.Tree Nodes Indicators Indicators with non-scroll parent Settings (main) 209 6 4 Settings › Privacy & Security 196 6 4 Safari WKWebView (iOS article) 554 13 10 Total — 25 18 Head-vs-main on the same acquisition: the two native lists are byte-identical (same band rect, same hidden-content hints, same rows). The
WKWebViewpage is the one real surface where ADR 0026's under-clip direction shows — the page scroller keeps its full extent instead of being banded to the walk-derived rect — and a reduced fixture for it is pinned in #2758.hiddenContentBelowsurvives on both sides; only the scroller rect differs.An Agent Device Tester
FlatListcapture and a mid-list RN capture are still outstanding (device was contended); the FlatList shape is the native-list case already shown identical, and theWKWebView-as-row shape is covered by the authored cases.- added 4 commits that reference this issue
on Sep 22, 2026 - changed the title
[-]Make snapshot membership loss attributable: scroll clip authority + ejection dispositions[/-][+]Snapshot membership step 5: inventory suppressNode ejections and give them typed dispositions[/+]on Oct 8, 2026 Retitled to the remaining work. Steps 1–4 are done: #2758 (steps 1–3, parent-edge ownership, with the step-3 survey recorded above), #2759 (step 4, eligibility parity and macOS
ScrollArea), and the step-2 runner differential via #2974, closed by #3009. What remains is step 5: inventory the ~23suppressNode(...)call sites across ~10 rules (a draft list is in the comment above), then make the ejections typed, redesigning only what the inventory justifies. The 'Required behavior' sections for steps 1–4 are historical.
Current continuation (2026-09-25)
Architecture umbrella: #2803. #2974 now owns the remaining implementation of step 2, the runner-presented interactive differential. Its self-contained contract supersedes this issue's older step-2 requirement that both authored TextView/WebView cases fail on current main: TextView is already a green control; the missing proof must kill the specified ancestor-ownership regression mutation. Do not duplicate this work or change the clipping policy to make the test pass.
#2755, #2758 and #2759 are merged. The original requirement that steps 1 and 2 land together is historical; it must not block this residual test work or imply its coverage already exists. The broader captured-tree survey and typed ejection-disposition work stay owned here; #2974 does not claim those complete. Close step 2 only after checking its merged PR and actual runner-stage coverage.
The original scope follows for context.
Purpose
snapshot -ilost every list row after a row holding selectable text (#2214 in 0.21.0, patched for thatone type by #2740). The type-specific fix closed one instance. The class stays open: presentation still
finds a scroll indicator's owner by walking up past its parent, so any host that scrolls but publishes
as a non-scroll type (
WebView, a map view, a paged cell) misattributes its indicator to the enclosinglist and clips that list to the host's band. Measured: a synthetic
Element(58)row with its own indicatordrops
Replyonorigin/main.Design record: ADR 0026 — Scroll Clip Authority: Ownership Is the Parent Edge (proposed, #2755).
Required behavior
Step 1 — ownership is read from the parent edge. The owner of an indicator is its parent, and a band
derives only when that parent is a scroll type. Delete the ancestor walk and
isTextViewTypefrompackages/capture-kit/src/ios-snapshot-engine/scroll.ts. Evidence already collected on a scratchbranch:
pnpm test:unit→ 10,959 passed / 1 skipped with this substitution, and theElement(58)shapeabove stops dropping rows. Land it with the red→green cases from step 2.
Step 2 — extend the differential to the runner stage.
assertProjectionSubsets(
packages/capture-kit/src/ios-snapshot-engine/properties.test.ts) asserts interactive ⊆ regular ⊆ rawbut builds acquired inputs only; the Swift differential drops interactive cases. Add a
stage: 'presented'arm plus authored cases for a
TextViewrow (#2740's shape) and aWebViewrow. Each must fail onorigin/mainfirst, perdocs/agents/testing.md. Assert source-level membership, not literallyinteractive ⊆ full: the full projection skips semantic compaction.Step 3 — survey real captured trees for under-clipping. Removing the walk can lose a band when a
real tree places an indicator under a labelled wrapper instead of directly under its scroll view, letting
content scrolled under the chrome survive — the leak class #1784/#1797 removed. Survey captured runner
trees before landing step 1, and record the count of indicators whose parent is not a scroll type. If the
shape is real, decide on evidence whether to skip unlabeled
Otherwrappers — do not add the tolerancespeculatively.
Step 4 — pair the two eligibility lists, and settle
ScrollAreafor macOS.REGULAR_ELIGIBLE_TYPES(ios-snapshot-engine/projection.ts) andeligibleInteractiveTypes(
apple/snapshot-presentation/Sources/AgentDeviceSnapshotPresentation/SnapshotPresentationProjection.swift)are one fact in two languages and no test currently references either. They are what makes the parent edge
survive projection: an ineligible wrapper is re-parented to the nearest presented ancestor, which is the
scroll host. Add a shared-table or dual-assertion guard. Note the pair excludes
scrollareawhileisScrollableSnapshotTypeincludes it: the iOS runner never emits that type, but macOS desktop capturereaches these rules (
snapshot-desktop-surface.ts→ios-snapshot-runtime.ts→publishIosSnapshot) andits helper emits
ScrollArea. Decide whether macOS needs it eligible before the rule is reused there —do not resolve it by editing a set for iOS's sake.
Step 5 — inventory ejections, then make them typed. ~23
suppressNodecall sites across ten rules(
rows,web,transitions,noise-search,noise-redundancy,noise-structural,noise-overlay,noise-viewport,action-shelf,scroll); at least two also rewrite rects. Extend the existingpresentedIndexesBySourceIndex(ios-snapshot-engine/semantic-index.ts) into a typed disposition — everysource ends presented with representatives, or removed with at least one reason. If
ejectrequires areason the ledger is complete by construction, so no separate gate is needed. Internal evidence only: no
suppression vocabulary on the snapshot wire.
Observable completion conditions
stage: 'presented'case for both theTextViewandWebViewrow shapes fails onorigin/mainandpasses with the change.
remains in
scroll.ts.carries a captured-tree fixture.
ScrollAreaquestion is answered formacOS surfaces — either decided with a case, or recorded as out of scope with the reason.
pnpm check:affected --runon the exact pushed head, plus the engine conformance and differential suites.New ownership cases join
contracts/fixtures/ios-snapshot-engine-conformance.json; they join the Swiftfixture only once Swift implements the same behavior.
Non-goals
TextView/WebViewto a universal scroll-container set.Dependencies and sequencing
runner-presentation.test.tspins the band contract, so the band must not be deleted to dodge theproblem.
Related: #2214 (introduced), #2740 (type-specific fix), ADR 0004 §host-side ownership boundary and the
#2638 regression note ("an absent fact is not a negative answer").