Conversation
… (objectui#7974) `SwipeGestureConfig.direction` is declared `SpecSwipeDirection[]` — a set — and `useSpecGesture` now honours the whole of it: it recognizes the any-direction move past the threshold and fires only when the DETECTED direction is a member of the declared set. It used to fuse `direction[0]` into one direction-specific recognizer, so every element after the first was declared and never honoured, and an empty or absent set silently defaulted to a left swipe. The scalar branch and its `as string` cast are gone, and nothing replaces them (AGENTS.md #0.1): the declared type rejects a scalar and no runtime path re-admits one. The shipped `@example` moves to `direction: ['left']` in the same change, because repairing either half alone leaves the other teaching the wrong contract. The `@example` gate's ledger row is RE-DERIVED rather than deleted — the repair paid off its TS2322 half and left the TS1108 hook-body excerpt its two siblings in this package are declared for. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
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.
Part of #7974
Executes the maintainer ruling of decision batch #70 (issue comment
5565631072, verbatim 「同意」):SwipeGestureConfig.directionstays as declared, and the implementation aligns to it — the swipe fires when the detected direction is a member of the declared set, the scalar branch with itsas stringcast is removed (AGENTS.md #0.1), and the shipped@examplemoves to the array form. Both halves, one change.Half 1 — the
@exampleBefore:
After:
Half 2 — the lenient cast, removed with nothing in its place
Three separate leniencies lived in those four lines: a scalar admitted through a cast that the declared array type rejects;
direction[0], which honours one element of a declared many; and a?? 'swipe-left'default that recognized a direction nobody declared. All three are gone and none is replaced.What replaces them reads the set as a set. Recognition is the any-direction move past the threshold — the recognizer
useGesturealready implements — and the callback fires only when the detected direction is a member ofconfig.swipe.direction:This does not collapse
swipeintopan:panfires for every direction,swipefires for the declared members only. Sharing the recognizer is what lets one configuration honour several directions at once, which a direction-fused recognizer cannot do by construction.Three consequences worth reading as behaviour changes rather than refactors, all of them the ruling applied:
direction: ['left', 'up']used to fire on a left swipe only.swipesub-object now fires for nothing, where it used to fall back to a left-swipe recognizer.onGesturefallback payload now reportstype: 'swipe'— the spec gesture — where it used to report the recognizer's own name. The recognizer'stypetravels inside the context object at runtime, so the previous spread order let it overwrite the declaredtype; with the shared recognizer it would have saidpan.typenow goes last. Pinned.SWIPE_DIRECTION_MAP(direction to recognizer) is deleted with its last caller. TheSPEC_GESTURE_TYPE_MAPentry forswipeis unchanged in value and its comment now says what it is: a placeholder, because no single recognizer name carries "a set of directions". That map's own JSDoc, which routedswipethrough the deleted map, is repaired in the same edit.The precondition: scalar callers = 0, with a firing control
The ruling asked for the count and for a probe demonstrated to find a scalar caller when one exists. Both numbers:
The probe walks 5,354 files under
packages/,apps/andexamples/, matches everyswipe: { ... }literal plus every.swipe.direction =assignment, and classifies the value by shape. The firing control planted one file containingswipe: { direction: 'right', threshold: 80 }, proved it reached disk with agrep -c, and the probe's SCALAR score moved 1 to 2 and named the planted file. The control file was then removed and the worktree proved clean before the count was taken again.The one pre-fix SCALAR was the
@examplethis card repairs — not a call site. So: zero scalar callers, and the zero is a reading of the tree, not of the pattern.The probe's blind spot, closed by hand rather than assumed away: a
SwipeGestureConfigbuilt as a standalone annotated literal is invisible to aswipe:block pattern. A census of everySwipeGestureConfigmention under those three roots returns the interface declaration, two type re-exports, four CHANGELOG paragraphs, and — after this change — the deliberate rejection case in the new test. No annotated literal anywhere else.The membership test
packages/mobile/src/__tests__/spec-gesture-direction-set.test.tsx, five pins, run against the realuseGesturethrough synthetic touch events, because the recognizer choice and the membership filter are two halves of one behaviour and mocking the recognizer pins only the half that lives in the hook:@ts-expect-erroron a scalardirectionThat last one is a type-level pin and it is really checked:
packages/mobile'stsconfig.test.jsonincludes the file (confirmed with--listFiles) and resolves@object-ui/typesthrough the built declaration, not through source. Control run, with the directive stripped:the same code the
@exampleledger row used to declare, now pinned as an expected rejection.Ablation
Fix committed first, then
packages/mobile/src/useSpecGesture.tsrestored to its pre-change blob, both legs verified bygit hash-objectagainst the recorded blob hashes rather than by an exit code.SWIPE_DIRECTION_MAP4, scalar example 1,declaredDirections0SWIPE_DIRECTION_MAP0, scalar example 0,declaredDirections2The four that go red without it:
with the discriminating failure reading:
"does not fire for a direction the set does not declare" passes on both sides and is kept anyway: the pre-fix recognizer refused a non-member for the wrong reason (it was fused to one direction), so that assertion cannot tell the two implementations apart. Said here rather than left for a reader to discover.
The
@examplegate's ledger row is RE-DERIVED, not deletedscripts/check-doc-example-types.mjsalready compiles this block, and its row for it said "Delete this row when that card lands". That instruction is falsified by measurement. The repair pays off the row's TS2322 half; the block still returns outside any function, so TS1108 remains and the block is still a fragment by shape — exactly the row the two other hook-body excerpts in this package carry. Deleting the row would have turned the block into an undeclared failure, which is red for a different reason. The row is nowcodes: [1108],card: null, with the siblings' wording; the gate's own header paragraph, which described the card as open, is rewritten to record what actually happened. The gate agrees:pnpm check:doc-examplesexits 0, "Every covered@examplecompiles, or fails exactly as its ledger row declares."Verification
pnpm exec vitest run packages/mobile/pnpm exec vitest run scripts/__tests__/check-doc-example-types.test.ts--build-filter, 35 tasks)pnpm check:doc-examplespnpm check:doc-snippetspnpm --filter @object-ui/mobile type-checknode scripts/check-changeset-presence.mjsnode scripts/check-new-cross-file-line-citations.mjsnode scripts/check-control-bytes.mjsnode scripts/check-governed-queue-guard.mjs --test(6 paths)eslintover the six changed filesBuild and gates ran serialized through the shared heavy-verify lock; the repo-wide
pnpm lintand the full suite are CI's.Changeset
Followed the gate rather than guessed.
node scripts/check-changeset-presence.mjsscored two published source files of one released package and refused the change without a declaration;.changeset/7974-swipe-direction-honours-declared-set.mddeclares@object-ui/mobile: minor(nevermajor— fixed group) and the gate now exits 0. The body states the two breaking narrowings and records that the caller scan found none to migrate.Scope
The declared type is untouched —
packages/types/src/mobile.tshas no edit in this diff, per the ruling and around live work in that file. No gate for JSDoc@exampleblocks was built: one already exists and this change only re-derives its ledger row. Clause ②:no—^\+exportscores 0 over the diff and no key is added to any published payload.Generated by Claude Code