fix(scroll): observe the movement a directional scroll can claim - #2776
Conversation
Size Report
Startup median (7 runs, lower is better):
|
|
Reviewed f728698. One blocking finding and one design question.
Can this module be smaller? It is 552 lines, which is under the size threshold. But it adds a plan union, three reason unions and four diagnostic reporters. A separate loop from Smaller notes, not blocking. Any of them can wait for a follow-up:
CI was queued with no failures. Repo Guards may flag |
55a4fd2 to
18ef40f
Compare
18ef40f to
a26d175
Compare
|
I reviewed a26d175 as a follow-up to the earlier findings on f728698 (#2776 (comment)), and the code here is clean. I did not run the tests, so I have not reproduced the author's claim that removing the lineage gate turns the drift tests red. This layer is not reachable from the CLI, so live device evidence of the scroll route belongs to #2771/#2773 and I did not judge it here. The earlier non-blocking notes on cancellation in captureSurface and on changes outside the container counting as "moved" are outside this delta, so I have not re-raised them; whether they matter depends on the wiring in #2771. All 16 checks were still queued when I looked, and none had failed yet. Repo Guards (fallow unused exports) overlaps this diff: planScrollMovement, observeScrollMovement, and reportScrollMovementNotApplicable have no production importer until #2771 wires them in, so that check may flag them as unused. Handle that at the stack level once #2771 lands rather than adding an allowlist entry here. |
a26d175 to
0482831
Compare
|
All three due items are in, on
The type surface is smaller. Gone: Why two reporters remain. They describe two different response shapes, and collapsing them would merge two facts that the response keeps apart: Your note 2 is a real defect, and I could reproduce it. The comparator is in Reasons are now asserted, not implied. The diagnostics sink is mocked in Not taken this round, on purpose: the bare Module is 591 lines (was 552): +39 for the confinement, which is the part the device evidence demanded. CI: 🤖 Generated with Apex by Callstack |
0482831 to
d97aba9
Compare
|
The confinement gap I flagged in f728698 (#2776 (comment)) is still open at 0482831.
Not blocking: the confinement in I did not run tests, tsc, or fallow, and did not reproduce the author's red/green stub claim. The two-scroller mechanism above comes from reading how The head has since moved to d97aba9, a rebase onto main in which this commit is patch-identical, so the review above still applies. |
|
What
The observation itself, as one module:
src/daemon/scroll-movement.ts. It reads the difference a directional scroll can actually prove — the tree the session already held before the gesture, and one tree after it — and answers what that evidence supports. #2771 wiresscroll <direction>to it; nothing here changes an answer yet, which is why it is a separate layer: the rule and its 13 pinned behaviors can be reviewed apart from the dispatch seam, the runtime-use declaration, and the response shape that consume them.moved— one capturescroll_no_progresswith the container frame and the recovery hintat-edge, no distance claimedunchanged— the measurement, not a guess about which of the two it wasunobservedThe three rules that make the claim worth having
presentationKeyand the comparison key are checked on every capture, before the changed/unchanged classifier runs — so a pair from different capture lineages (#1569's XCTest-channel fallback swapping the producer mid-request, or asnapshot -ibaseline against this command's broad read) cannot producemovedeither. The deferred loop adopts a new producer and keeps polling, because its question is whether the surface settles; this one compares against a pre-gesture tree, so a drifted pair is withheld rather than re-based. Pinned by a test whose frames genuinely differ, so the gate is proven on the path that would otherwise claim movement.session.snapshotis not automatically the screen as it stands. ADR 0014 already encodes the question — the ref frame staysactivewhile no device side effect has crossed since the last publication — so after atap,scrollanswersunobservedand spends zero captures rather than crediting its own swipe with what the tap changed (stored-surface-not-current). Same for a gesture nobody has read yet (prior-gesture-unsettled).warnand answersunobserved— refusing there would fail a working scroll over a busy screen.Evidence rules are imported, not restated: the signature, the subset-tolerant baseline classifier and the strict both-directions no-effect bar all come from
interaction-outcome-policy.ts, so "that scroll did nothing" means one thing in this daemon (#1601 review P1). Where a capture cannot be trusted,deferred-interaction-outcome.tsand this module now read the samesnapshotSurfaceComparisonKey— that owner moved here from the former private copy, and both callers were repointed in the same commit.What this round changed
movedis now confined to the scroller the swipe ran in. Review note 2 was right and it is measurable: on the Android tester's/catalog, changing only a system status-bar icon (dumpsys battery set level) turned the pair into "changed" over a list whose own scroll-state label never moved — the module answeredmovement: 'moved',attempts: 1. A whole-surface difference is not the gesture's doing when it lives outside the container, soclaimMovedresolves the container from the tree it already read (no extra capture) and asks the shared comparator whether the region changed. It did not →movement: 'unobserved'with the typed reasonchange-outside-container. With no container to confine to (no scroller resolved, or the horizontal axis the analyzer does not read) the whole-surface answer stands. The comparator,discriminatingSurfaceChangedWithinRect, lives ininteraction-outcome-policy.tsbeside the strict bar it reuses, and is tested there as well as through the module.The plan/reporter surface is smaller.
ScrollMovementDecline,FrozenScrollSurfaceandScrollMovementBlindReasonare gone:ScrollMovementPlanis the only plan union (its declined arm names its ownExclude),freezeScrollSurfaceBaselinereturns the plan, andreportBlind+reportScrollMovementUnobservableare onereportScrollMovementUnobserved(direction, reason, evidence)over one exported reason union. Two reporters remain rather than one, because the response shapes they describe are different facts:reportScrollMovementNotApplicableaccompanies nomovementfield at all (this command owed no observation), whilereportScrollMovementUnobservedaccompaniesmovement: 'unobserved'(it owed one and could not make it). Collapsing them would put an absent field and a present-but-blind field in one log phase.verticalEdgeForis imported, not copied — frompackages/capture-kit/src/snapshot/scroll-edge-state.ts, whereScrollEdgelives, next to the axis-honestformatScrollEdgeMessagechange in #2770.scroll-until.tsimports it too, so--untiland the observation cannot drift apart about which directions have an end signal.CI's
Repo Guardsfinding is resolved at the owner, not suppressed.planScrollMovementand the two reporters were flagged as unused exports because nothing imported them until #2771; they are now exercised here — the four plan arms and both reporters are asserted, which also closes "the tests assertunobservedwithout checking the reason": the reason and phase are read out of a mockedemitDiagnosticin every withheld case.Not taken this round, deliberately: the poll's bare
catchandsleepstill ignore request cancellation, which needs anAbortSignalplumbed from the request scope rather than a local fix — the same gap the other polling loops in this daemon have.Validation
pnpm check:affected --run→ all runnable checks passed on the stack head (882256121): lint, format, typecheck, layering, fallow, 585 files / 4,356 tests. At this layer's own head (d97aba9fb): 24 module tests + 21 policy tests,tsc, andpnpm check:fallow --base fix/scroll-surface-vocabularyclean.movedvs a surface still in motion;movedvs a change that never touched the container; the strict-bar case (a toast over an unchanged list is not a no-op); budget expiry asserted ascalls > 2so it cannot pass by accidentally forming a quiet pair; a capture that throws answersunobservedinstead of failing the scroll; every withheld answer asserts its reason.change-outside-containertest red; the capture-count assertions go red if the fast path stops being one capture.No device run is owed by this layer — it is unreachable from the CLI until #2771, which carries the live evidence (including the Android measurement that produced the container confinement above).
🤖 Generated with Apex by Callstack