Skip to content

feat(ios): move the regular-projection clip fold into snapshot presentation (#1797) - #1929

Merged
thymikee merged 2 commits into
mainfrom
claude/1797-clip-fold
Aug 21, 2026
Merged

feat(ios): move the regular-projection clip fold into snapshot presentation (#1797)#1929
thymikee merged 2 commits into
mainfrom
claude/1797-clip-fold

Conversation

@thymikee

Copy link
Copy Markdown
Member

Summary

Next #1797 delta (after the raw/regular split, #1926): the clip fold moves into presentation. Both iOS backends carried their own copy of the visibility fold — the tree walker and the private-AX serializer each computed viewport∩scroll-clip intersection, ancestor projection, hidden-content hints, and collapsed depth during acquisition. Hand-synchronized copies of that interpretation are the mechanism behind the scroll-overflow leak class (#1784 was fixed by making one copy call the other's helpers — correct, but nothing stopped the next drift), and C1 (fact-availability neutrality) cannot hold while acquisition decides what a screen shows.

After this PR, acquisition backends are fact serializers. Every traversed node is emitted at raw traversal depth with its reported frame; SnapshotAcquisition carries the viewport; presentRegular runs the one fold for every backend:

  • viewport ∩ scroll-container clip, with the ancestor projection cursor (an out-of-clip Cell/scroll container hides descendants whose clamped frames would otherwise leak back into the viewport — fix: stabilize private AX settle snapshots #1784's shape, now owned by one implementation),
  • the sub-pixel decoration rule,
  • hidden-content hints booked onto scroll anchors,
  • reparenting of survivors with collapsed depth,
  • and a new invariant: presented hittable is narrowed to the clip — nothing outside its clip, and nothing without geometry, is ever hittable in a regular snapshot, whatever the backend reported.

Platform differences enter as a SnapshotFoldPolicy input to the shared algorithm (iOS cursor-projected; macOS/tvOS plain viewport intersection) — per the issue's rule that macOS is a platform policy, never a backend exception. The private-AX backend collapses to one serializer for both projections, and the flat filter-decision family (FlatSnapshotFilterNode/FlatSnapshotVisibilityPolicy) is deleted with the acquisition gates it fed.

Behavior

Live A/B, byte-identical. Settings on iPhone 16 Pro / iOS 26.2, this branch's runner vs a runner built from main, each on its own state dir and daemon, compared on full node signatures (type, label, identifier, value, rect, depth, parentIndex, hittable, both hidden-content hints):

projection main branch
tree snapshot 73 73 identical
tree snapshot -i 18 18 identical
private-AX snapshot --actions 58 58 identical
private-AX snapshot -i --actions 20 20 identical

Three intentional edge deltas, each replacing a per-backend rule with one backend-neutral rule (none fired on the live screen above):

  1. Sub-pixel content-free decorations now drop on every backend (was private-AX only). Only observable for interactive-typed contentless degenerate nodes on the tree backend.
  2. Labeled offscreen Application/Window carriers now survive on every backend (was tree only) — still never hittable.
  3. A query-sweep recovery snapshot without -i is now viewport-folded like everything else (it used to list offscreen elements with hittable: false).

Declared acquisition residues (budget/fidelity, not membership): the traversal-depth cut (regular presents collapsed depth — the open visible-depth frontier obligation, unchanged), the sweep's frameless-element drop (a flat query has no hierarchy for geometryless semantics to attach to), and the private-AX bridge's device-side node cap. Two mechanical notes: the private-AX backend-level sparse gate (nodes.count <= 1) now counts serialized rather than folded nodes, so a fully-clipped tree surfaces through the plan's sparse classifier on the presented payload instead of a backend nil — same terminal outcome, slightly different verdict path; and children of a dropped duplicate inherit the ancestor cursor rather than the duplicate's own transition (observable only for a duplicate offscreen owned-container whose children aren't themselves duplicates).

Evidence

  • 23 targeted runner tests green (iPhone 16 Pro sim / iOS 26.2): 5 new fold tests (scroll-overflow clip + hint + reparenting, Window-carrier + hittable-outside-clip, sub-pixel rule, plain-viewport policy, type-name pinning) plus the rewritten private-AX presentation tests now routed through the production presentRegular, plus the feat(ios): split raw and regular snapshot projections behind one capture hint (#1797) #1926 suite. macOS bundle compiles with unit tests (host lane doesn't run on this machine — environmental, verified both ways on feat(ios): split raw and regular snapshot projections behind one capture hint (#1797) #1926; CI covers it).
  • Non-vacuity by mutation: with presentRegular bypassing foldRegularVisibility, exactly the six fold-dependent tests fail (both new fold tests and both private-AX regular tests — the drawer fixture reproduces the leak: "Admin settings" from the offscreen drawer is presented) while eligibility and scope tests stay green. Restored before commit.
  • pnpm check:xctest-selection green (172 declared, 0 unreachable); new tests added to the iOS PR lane; check:affected --run green; oxfmt/oxlint clean.

Docs

ADR 0004: the fold as the fourth semantic layer, the policy input, and the declared residues. CONTEXT.md: clip fold vocabulary. CHANGELOG: user-facing entry with the three edge deltas.

What #1797 still needs after this

  1. hittable = geometric actionability — the tree backend still computes its occlusion scan at acquisition (computedSnapshotHittable); moving occlusion wholly to the daemon annotator is the next delta, with the ~118-reference hittable-consumer inventory.
  2. Geometry carrier (effective rect on the wire) with the rect-consumer inventory — now unblocked, since presentation computes the clip.
  3. Step-4 enforcement: choke-point invariant, per-PR fixture differentials + nightly live tree-vs-private-AX differential, tree pin + penalty-clear hook, parity ratchets.
  4. Cross-runtime RawAXNode → Presented conformance suite (Android native-a11y → snapshot: conform to the acquire/present engine contracts (sibling of #1797) #1832 step 9).

Refs #1797 (migration step 3, clip-fold delta).

…tation

Both iOS snapshot backends carried their own copy of the visibility fold: the
tree walker and the private-AX serializer each computed viewport-and-scroll-clip
intersection, ancestor projection, hidden-content hints, and collapsed depth
during acquisition. Hand-synchronized copies of that interpretation are what
produced the scroll-overflow leak class (#1784), and C1 (fact-availability
neutrality) could not hold while acquisition decided what a screen shows.

Acquisition backends are now fact serializers: every traversed node is emitted
at raw traversal depth with its reported frame, and SnapshotAcquisition carries
the viewport. presentRegular runs the one clip fold for every backend --
viewport ∩ scroll clip, the ancestor cursor (an out-of-clip Cell or scroll
container hides its clamped descendants), the sub-pixel decoration rule,
scroll hints booked onto anchors, reparenting with collapsed depth -- and
narrows the emitted hittable to the clip: nothing outside its clip, and nothing
without geometry, is ever hittable, whatever the backend reported. Platform
differences are a SnapshotFoldPolicy input to the shared algorithm (iOS
cursor-projected; macOS/tvOS plain viewport), never a backend exception.

The private-AX backend collapses to ONE serializer for both projections, and
the flat filter-decision family dies with the acquisition gates it fed.

Three intentional edge deltas, each toward one backend-neutral rule: sub-pixel
content-free decorations now drop on every backend (was private-AX only);
labeled offscreen Application/Window carriers survive on every backend (was
tree only), never hittable; query-sweep regular without -i is viewport-folded.
Declared acquisition residues: the traversal-depth budget cut, the sweep's
frameless-element drop, the private-AX bridge's device-side cap.

Refs #1797 (migration step 3, clip-fold delta).
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.34 MB 2.34 MB 0 B
JS gzip 770.2 kB 770.2 kB 0 B
npm tarball 898.5 kB 897.9 kB -576 B
npm unpacked 3.13 MB 3.13 MB -5.1 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 23.8 ms 22.3 ms -1.4 ms
CLI --help 57.2 ms 55.6 ms -1.6 ms

Top changed chunks: no changes in the largest emitted chunks.

@thymikee

Copy link
Copy Markdown
Member Author

[P2] Please extract foldRegularVisibility into a dedicated snapshot-visibility-fold module. This addition takes RunnerTests+SnapshotPresentation.swift to 529 lines, past the repository�\s extract-before-500 rule, and the fold still relies on RunnerTests.* helpers/types scattered across +Snapshot.swift and +FlatSnapshotFiltering.swift. Put the fold�\s policy, transition, clipping, hint, and reparenting ownership behind one pure interface with its matching test file, then have SnapshotPresentation call it. That keeps presentation orchestration-only and makes the backend-neutral visibility invariant obvious in code.

@thymikee

Copy link
Copy Markdown
Member Author

Addressed the modularity review in e4b19df:

  • extracted the backend-neutral visibility fold into a 294-line SnapshotVisibilityFold deep module with a matching focused test file;
  • moved clipping, ancestor projection, reparenting, scroll anchors/hints, and actionability narrowing behind its single pure interface;
  • made SnapshotAcquisition.viewport mandatory so regular captures cannot silently bypass clipping through a default;
  • removed the superseded FlatSnapshotFiltering module and the unused private-AX semantic helper, and isolated private-AX element-type conversion;
  • added a presentation-boundary regression so bypassing the fold fails (verified red: the out-of-viewport node leaked; restored green);
  • updated XCTest selection for the moved/new tests.

Verification: pnpm check:affected --run; iOS and macOS XCTest runner builds; 8 focused iOS tests, 0 failures. This completes the clip-fold/modularity unit of #1797, not the entire issue: effective geometry/hittability ownership and cross-runtime enforcement remain separate semantic follow-ups.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed e4b19df6c955ee6c91c8ca70798c24b0f76269ce: the prior ownership/size concern is resolved. The clip fold is now the dedicated 294-line SnapshotVisibilityFold module, reached once through SnapshotPresentation.presentRegular; its 132-line sibling tests cover clipping, cursor policy, hints, reparenting, carrier hittability, and the type-vocabulary pin. The former flat-filter path and duplicate fold helpers are deleted, and the capture plan sends every backend through the shared presentation route.\n\nCode review is clean. iOS Smoke, host XCTests, static/layering/fallow, and Size are green; documented live A/B evidence covers the behavior. Coverage is the sole red check: an unrelated scripts/fuzz/corpus-replay.test.ts Vitest worker-termination timeout after 1051/1052 files passed.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 21, 2026
@thymikee
thymikee merged commit 73db7be into main Aug 21, 2026
43 of 44 checks passed
@thymikee
thymikee deleted the claude/1797-clip-fold branch August 21, 2026 09:27
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-21 09:27 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant