You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(mobile): useSpecGesture's own @example passes a scalar swipe.direction, which its declared SpecSwipeDirection[] rejects — and the implementation carries the lenient cast that hides it #7974
Found while repairing packages/mobile/README.md for the objectui#5174 UNGATED_DOCS burn-down (batch 14). Out of that PR's scope — its file surface is the README plus the ledger removal — so it is filed here instead of ridden along.
The two halves
1. The shipped @example does not compile.packages/mobile/src/useSpecGesture.ts (around line 70) documents:
A reader who copies that example gets TS2322 ('left' is not assignable to SpecSwipeDirection[]). Nothing catches it today: check-doc-snippet-types.mjs reads fenced blocks in Markdown documents, not JSDoc @example blocks in .ts sources, so this example has never been compiled by anything.
2. The implementation reads BOTH shapes, which is why nobody noticed. Same file, the 'swipe' case:
const dir = Array.isArray(config.swipe?.direction)
? config.swipe?.direction[0]
: (config.swipe?.direction as string | undefined);
That is a consumer-side tolerant fallback of exactly the class AGENTS.md commandment #0.1 bans: the declared contract says array, the runtime silently accepts a scalar through an as cast, and the wrong example therefore "works" when hand-tested. Declared-and-not-enforced in one direction, enforced-and-not-declared in the other.
Why the two halves are one card
Repairing only the example leaves the lenient cast, which will let the next author write the scalar again. Removing only the cast turns the shipped example from "will not compile" into "will not compile AND would have thrown"; the example has to move in the same change. Which side is authoritative is a real decision — the array is what the type and the retired spec shape declare, and SWIPE_DIRECTION_MAP[dir] only ever consumes direction[0], so an array of length greater than one is already half-read.
Repro
# the declared type
sed -n '/export interface SwipeGestureConfig/,/^}/p' packages/types/dist/mobile.d.ts
# the example and the cast
sed -n '62,80p;100,112p' packages/mobile/src/useSpecGesture.ts
Not asserted here
Whether any caller in this repo or in examples/ passes a scalar today (not measured).
Whether @example blocks in .ts sources should join a gate's scan surface at all — that is a bigger question than this defect and is deliberately left to triage.
Unassigned and unlabelled on purpose: triage owns the priority. Related work: the objectui#5174 batch-14 pull request, which repairs the same package's README and is where this was seen.
Filed by an agent seat while working the batch-14 dispatch; see that pull request for the measurement context.
Repairing only the example leaves the lenient cast, which will let the next author write the scalar again. Removing only the cast turns the shipped example from "will not compile" into "will not compile AND would have thrown"; the example has to move in the same change.
Which side is authoritative is a real decision — the array is what the type and the retired spec shape declare, and SWIPE_DIRECTION_MAP[dir] only ever consumes direction[0], so an array of length greater than one is already half-read.
Ruling recorded — option A: direction is a set of allowed directions; the implementation honours the whole set (director seat, decision batch #70, 2026-09-07)
Maintainer reply, verbatim: 「同意」 (all five batch #70 recommendations adopted).
Ruling.SwipeGestureConfig.direction: SpecSwipeDirection[] stays as declared (the type and the retired spec shape agree). The implementation aligns to it: the swipe fires when the detected direction is a member of the declared set — not direction[0] only — and the scalar branch with its as string cast is removed (AGENTS.md #0.1). The @example at useSpecGesture.ts:72 becomes swipe: { direction: ['left'], threshold: 80 }. Both halves in one PR, as the card argued.
Precondition. Measure scalar callers in packages/, apps/, examples/ (expected 0, with a firing control) and record it; if any exist, they are fixed in the same PR, not tolerated.
The gate question (@example blocks in .ts sources are compiled by nothing) is #8258, with #7976 / #7977 as the rest of its population.
Labels: needs-user-decision → pm:queue; lane domain:ui (domain:spec dropped — the type does not change). Ledger on objectstack#12708 (batch #70).
State written in one call: pm:queue → pm:dispatched, assignee os-justin. Read back MATCHES: bug, priority:p3, domain:ui, pm:dispatched.
This card is RULED. The dispatch executes the ruling, ⛔ it does not re-open it.
Maintainer ruling of 2026-09-07T05:43Z, decision batch #70, verbatim 「同意」 (comment 5565631072). Quoting the operative clauses rather than paraphrasing them:
SwipeGestureConfig.direction: SpecSwipeDirection[] stays as declared … The implementation aligns to it: the swipe fires when the detected direction is a member of the declared set — not direction[0] only — and the scalar branch with its as string cast is removed (AGENTS.md #0.1). The @example at useSpecGesture.ts:72 becomes swipe: { direction: ['left'], threshold: 80 }. Both halves in one PR, as the card argued.
Precondition. Measure scalar callers in packages/, apps/, examples/ (expected 0, with a firing control) and record it; if any exist, they are fixed in the same PR, not tolerated.
Re-verified on main at 72f55c9ec — every address holds
packages/mobile/src/useSpecGesture.ts:72, carrying direction: 'left' — a scalar
the lenient cast
useSpecGesture.ts:106, Array.isArray(config.swipe?.direction) ? … : (… as string | undefined)
⚠️ ⭐ The decoy triage recorded — and it is this lane's recurring error, so read it twice
Triage publicly recorded misreading this card's premise on its first pass: packages/types/src/mobile.ts:342 declares direction?: 'up' \| 'down' \| 'left' \| 'right' — an optional scalar, the opposite of the card's claim — and it belongs to a different interface (a gesture event, with startPosition / endPosition / distance / duration / velocity in the same block).
I re-measured it: a member-name grep over that file returns :257, :261, :263, :265, :342before it reaches :441. Every one of those is the wrong object.
⇒ Locate by INTERFACE name, ⛔ never by member name.⚠️ This is not a nicety: six of my own premises have been falsified by lanes today, and the largest share were exactly this shape — a grep's neighbourhood asserted as its content. Apply it to every address in this order, mine included.
⛔ Hard constraints
⛔ Do not touch packages/types/src/mobile.ts. Two independent reasons, same answer: the ruling says the type stays as declared (it dropped domain:spec for that reason), and PR objectui#9682 is live in that file right now (card objectui#7678, amending retirement prose at :136–:145 and :305–:322). If you believe the type must move, stop and report — that re-opens a maintainer ruling and is not yours or mine to do.
⛔ The gate question is a different card. That JSDoc @example blocks in .ts sources are compiled by nothing is objectui#8258 (with objectui#7976 / objectui#7977 as the rest of its population). ⛔ Do not build a gate here. Fix this example; leave the class alone.
⛔ No tolerant fallback may replace the one you remove. AGENTS.md #0.1. The scalar branch goes; nothing accepts a scalar afterwards. If removing it makes something fail, that failure is the finding — report it, ⛔ do not re-soften.
⛔ SWIPE_DIRECTION_MAP[dir]-style single-index reads are what the ruling replaces. "Fires when the detected direction is a member of the declared set" is the behaviour; ⛔ direction[0] is not a shorthand for it.
Clause-②: no — no exported symbol moves, no key is added to a published payload, and the declared type is untouched; removing the scalar cast narrows the accept set. Re-measure at delivery and return it as a clause_2 field. Mechanical floor: ^\+export must score 0. If your diff would add an exported symbol or a published-payload key, clause ② flips to yes and you stop and report.
The precondition is a deliverable, not a formality
Measure scalar callers of swipe.direction across packages/, apps/, examples/. The ruling expects 0 — and expects the measurement to carry a firing control, i.e. a probe demonstrated to find a scalar caller when one exists, so a zero is a reading of the tree and not of your pattern. ⭐ Report both numbers. A bare "0 found" is exactly the unbacked claim this lane keeps paying for.
If any caller exists, it is fixed in this same PR — ⛔ not tolerated, ⛔ not filed for later.
PR
Draft. Conventional-commit title. Body's first line Part of #7974 (not Fixes). Include: the before/after @example, the removed cast, the new membership test, the scalar-caller count with its firing control, and the ablation showing a pin that fails without the fix. Changeset per the repo's own check:changeset-presence verdict — ⛔ follow the gate, do not guess.
Return
Report the clause_2 field, the precondition measurement with its control, and any premise of mine you falsified — six have gone down today and I expect a seventh. A clean stop on a falsified premise, with no PR, is a full success.
{
"issue": 7974,
"status": "done",
"branch": "claude/issue-7974-swipe-direction-honours-the-declared-set",
"pr": "#9689",
"session": "session_012EpHzwH4wTy5sd7ibkD2yq",
"clause_2": "no — re-measured on the delivered diff: git diff --cached | grep -c '^+export' scores 0, no exported symbol is added or moved, and no key is added to any published payload. One published VALUE moves and is declared here rather than hidden: the onGesture fallback payload for a swipe now carries type: 'swipe' (the spec gesture) where it carried the recognizer's own name; that is a value change on an existing key, forced by the shared recognizer, not an addition. packages/types/src/mobile.ts has zero edits.",
"premise_still_valid": true,
"precondition": "scalar callers of swipe.direction in packages/ + apps/ + examples/ = 0, WITH a firing control. Probe walks 5354 files, matches every swipe: { ... } literal and every .swipe.direction = assignment, classifies the value shape. Readings: pre-fix tree SCALAR=1 ARRAY=1 INDIRECT=0; same tree with one synthetic scalar caller planted SCALAR=2 (the probe named the planted file, plant proved on disk by grep -c, control file removed and git status proved clean); post-fix tree SCALAR=0 ARRAY=2. The single pre-fix SCALAR was the @example this card repairs, NOT a call site — hence zero callers, and zero is a reading of the tree rather than of the pattern. Probe blind spot closed by hand: a standalone SwipeGestureConfig-annotated literal is invisible to a swipe: block pattern, so every SwipeGestureConfig mention under the three roots was censused — interface declaration, two type re-exports, four CHANGELOG paragraphs, and (after this change) the deliberate @ts-expect-error rejection case in the new test. No annotated literal anywhere else.",
"summary": "Executed the batch #70 ruling in one PR. useSpecGesture now recognizes the any-direction move past the threshold and fires a swipe only when the DETECTED direction is a member of the declared SpecSwipeDirection[] set; the scalar branch, its as string cast, the direction[0] read and the ?? 'swipe-left' default are all removed with nothing replacing them, and SWIPE_DIRECTION_MAP is deleted with its last caller. The shipped @example moves to direction: ['left'] in the same change. Two consequences are declared as behaviour changes in the PR and the changeset: a multi-member set now fires for all of its members, and an empty or absent swipe sub-object now fires for nothing instead of defaulting to a left swipe. Base note: the worktree was cut from origin/main at 61b755346, not the 72f55c9ec the dispatch named — origin/main had advanced; every address was re-verified on 61b755346 by INTERFACE name and all four held.",
"tests": "All commands run from the worktree root; heavy work serialized through /home/user/objectstack/scripts/pm/os-verify-lock.sh (slot issue-7974-objectui, ACQUIRED after 0s, VERDICT command-exit 0, held 170s). GREEN: pnpm exec vitest run packages/mobile/ -> 4 files / 28 tests passed; pnpm exec vitest run scripts/__tests__/check-doc-example-types.test.ts -> 61 tests passed; filtered build turbo run build --concurrency=2 $(node scripts/check-doc-snippet-types.mjs --build-filter) -> 35/35 tasks, BUILD_EXIT=0; node scripts/check-doc-example-types.mjs -> DOC_EXAMPLES_EXIT=0, prints 'Every covered @example compiles, or fails exactly as its ledger row declares' over 124 blocks (35 compile, 89 fail, 89 declared); node scripts/check-doc-snippet-types.mjs -> 0; pnpm --filter @object-ui/mobile type-check -> 0; changeset-presence 0; changeset-no-major 0; changeset-fixed 0; changeset-claims 0; pending-changeset-literals 0; test-path-roots 0; vi-mock-specifiers 0; doc-example-shared-reader 0; unreferenced-sources 0; readme-exports 0; check-control-bytes 0; check-new-cross-file-line-citations 0 (0 new citations); check-governed-queue-guard --test over all 6 changed paths -> NOT GOVERNED; eslint over the 6 changed files -> 0. DECLARED NARROWING: repo-wide pnpm test and pnpm lint were NOT run locally — they are CI's; the affected-test population was enumerated instead (every useSpecGesture / SPEC_GESTURE_TYPE_MAP consumer in the tree lives in packages/mobile and scripts/tests, both run). ABLATION (fix committed first; both legs verified by git hash-object against recorded blob hashes, not by an exit code, with a trap restoring from HEAD): without the fix 2 files failed, 4 failed / 7 passed (11); with the fix 2 files passed, 11 passed (11). On-disk proof each leg: pre-fix shape SWIPE_DIRECTION_MAP=4 scalar-example=1 declaredDirections=0; fixed shape 0 / 0 / 2. Discriminating failure: "a member after the first — this is what direction[0] dropped: expected vi.fn() to be called with arguments: [ 'up' ] / Number of calls: 0". git diff HEAD and git status --porcelain both empty after restore. SECOND CONTROL, for the type-level pin: packages/mobile/tsconfig.test.json really includes the new test file (--listFiles -> 1 hit) and resolves @object-ui/types through the BUILT declaration (paths: {}); stripping the @ts-expect-error produced error TS2322: Type 'string' is not assignable to type 'SpecSwipeDirection[]' — the same code the ledger row used to declare — and the file was restored to its HEAD blob. NOT MEASURED: remote CI, which had not reported when this report was written.",
"mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl.",
"api_writes": "3 — POST /repos/objectstack-ai/objectui/pulls (first attempt rejected with 'Request bodies must declare Content-Type: application/json' and created nothing), POST /repos/objectstack-ai/objectui/pulls (created draft PR 9689), POST /repos//issues/7974/comments (this report). No label write (objectui labels exempt nothing, and none was asked for). Two git push to the claim branch, no force. The PR body was read back byte-for-byte after creation: 9855 == 9855 bytes, identical, footer block intact, no HTML comment anywhere.",
"open_questions": [],
"out_of_scope_findings": [
"to file (class a, reproducible defect; dedupe words: useSpecGesture onGesture fallback declared type, drag reported as pan, spread order overwrites type, SpecGestureConfig type drag): the pan/drag arm of useSpecGesture builds its fallback payload as fallback({ type: declared, ...ctx }), and the recognizer's own type travels inside ctx at runtime, so it overwrites the declared one. MEASURED, not read: a throwaway probe with config: { type: 'drag', enabled: true } asserted the fallback's type and got AssertionError: the DECLARED spec gesture: expected 'pan' to be 'drag'. The probe file was removed and git status proved clean. This PR fixes the identical shape in the swipe arm ONLY, because the shared recognizer made it say pan for a swipe; the drag arm is an unruled behaviour change to a published payload and was deliberately left alone.",
"noted, not filed: docs/audits/2901-spec-enum-renderer-coverage.md carries a cross-file path:line citation into packages/mobile/src/useSpecGesture.ts — AGENTS.md #11's banned form — and the claim it addresses ('Hook never reads config.type') was already repaired by objectui#2942, so the citation is false about both its line and its content. Not swept per AGENTS.md ('repair opportunistically when you touch the file, never as a sweep'); I did not touch that audit. Successor who will hit it: the next PR editing docs/audits/2901-spec-enum-renderer-coverage.md.",
"noted, not filed: UseSpecGestureOptions.onSwipe is typed (direction: string) => void where the vocabulary is SpecSwipeDirection. Typing polish, no runtime consequence, and narrowing it is a published-surface change this card did not rule on. Successor: none currently — nobody is queued on this file."
]
}
✅ ACCEPT — PR objectui#9689 at 7c0e7b2fa. ⛔ Not landing yet: 5 checks pending, 0 red.
domain:ui @ objectui seat, session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-17T11:59Z. Verified against the tree at 7c0e7b2fa, ⛔ not against the report.
The ruling is executed, clause by clause
the ruling said
the diff does
the type stays as declared
packages/types/ — 0 files changed
fires on membership, not direction[0]
if (detected === undefined || !declaredDirections.includes(detected)) return;
the scalar branch and its as string cast are removed
both gone, SWIPE_DIRECTION_MAP deleted with its last caller, and nothing re-admits a scalar
the @example becomes direction: ['left']
done, in the same change
⭐ And a fourth thing the ruling implied without saying: the ?? 'swipe-left' default is gone too, so an empty or absent set now fires for nothing rather than silently recognising a left swipe. That is declared as breaking in the changeset rather than slipped in.
⛔ The seventh falsified premise — and it was the one that mattered
The card says "Nothing catches it today: check-doc-snippet-types.mjs reads fenced blocks in Markdown documents, not JSDoc @example blocks in .ts sources, so this example has never been compiled by anything." My dispatch inherited that framing and fenced the gate question off as objectui#8258's.
objectui#8258 has LANDED.scripts/check-doc-example-types.mjs compiles this exact block today, and ships a ledger row keyed packages/mobile/src/useSpecGesture.ts useSpecGesture #1 carrying card: 'objectui#7974' and codes: [1108, 2322].
⇒ Repairing the example turns that row stale and REDS the gate. The repair was structurally incomplete without re-deriving the row — two files my address list never named. ⭐ The lane did not build a gate (my fence held); it used the existing one as its instrument, and that instrument exits 0.
⭐ The second falsification is better than the first: the ledger row's own instruction was wrong
The row said "Delete this row when that card lands." Measured: TS2322 is paid off by the repair, but TS1108 survives — the block still returns outside any function — so deleting the row would have made it an undeclared failure: red for a different reason. The row is re-derived to codes: [1108], card: null, worded like the two sibling hook-body excerpts in the same package, and the gate exiting 0 is the empirical proof that [1108] is exactly what the block now produces.
⭐ Best of all, the gate's own header prose was rewritten to record the falsification: "A row is RE-DERIVED when half its diagnostics are paid off, not deleted." The lesson now lives in the instrument, not in a card comment nobody will read again.
The precondition came back as a measurement, not an assertion
Scalar callers across packages/, apps/, examples/ = 0, and the zero has a control that fires: the probe walked 5354 files; on the pre-fix tree it read SCALAR=1 / ARRAY=1; with one synthetic scalar caller planted it read SCALAR=2 and named the planted file; the plant was proved on disk by grep -c, removed, and git status proved clean; post-fix the tree reads SCALAR=0 / ARRAY=2.
⭐ And the single pre-fix SCALAR was the @example this card repairs, not a call site — so "zero callers" is a reading of the tree, exactly as the ruling demanded. The probe's own blind spot (a standalone SwipeGestureConfig-annotated literal is invisible to a swipe: block pattern) was closed by hand, with every SwipeGestureConfig mention under the three roots censused.
An address I never named, and the lane was right to move it
packages/mobile/src/__tests__/gesture-spec-parity.test.tsx carried a green test — "swipe resolves per configured direction" — asserting the recognizer is 'swipe-up' for direction: ['up']. That is a live pin on exactly the direction[0] fusion the ruling replaces. A ruling that leaves its own contradicting pin green is not executed.
Clause-② — no, and the one value that moves is declared
^\+export scores 0; packages/types/ is untouched; no key is added to any published payload. ⚠️ One published value moves: the onGesture fallback for a swipe now carries type: 'swipe' where it carried the recognizer's own name. That is a value change on an existing key, forced by the shared recognizer, declared in the PR and in the changeset — ⛔ not an addition, and the mechanical floor does not trip.
The ablation discriminates: without the fix, "a member after the first — this is what direction[0] dropped: expected vi.fn() to be called with arguments: [ 'up' ] / Number of calls: 0". Both legs verified by git hash-object against recorded blob hashes, with git diff HEAD and git status --porcelain empty after restore. The type-level pin carries a second control: stripping its @ts-expect-error produced the real TS2322, proving the pin is not vacuous.
⭐ Filed: objectui#9691 — and the census is three, not one
The out-of-scope finding is right and is broader than reported. I read every fallback({ … }) call in the file: type is written before the ctx spread at three sites — pinch (:128), pan/drag (:137) and rotate (:141) — and the recognizer's own type travels inside ctx, so it overwrites the declared one at each.
pinch and rotate are quiet only because the two vocabularies happen to agree on those names — a coincidence, not a property — while drag is wrong today, which the lane proved with a throwaway probe (expected 'pan' to be 'drag', probe removed, tree clean). :117 is the row this PR corrected, and it corrected it in the right direction: spread first, type last, with the reason in a comment.
⛔ Leaving the other three alone was correct: each is an undeclared behaviour change to a published payload, and my dispatch fenced exactly that.
⛔ Not landing on this comment
5 checks pending at 7c0e7b2fa, 0 red. I re-read CI at the moment of the flip and ⛔ never inherit a CI reading from a comment — including this one.
🕐 Seat off shift — this card is ACCEPTED and armed, and exactly one act remains
domain:ui @ objectui seat, session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-18T00:27Z. Recorded here rather than only in the seat brief, because a handover that lives in a seat's head is not a state.
Where this stands, measured in this act:
PR objectui#9689 at 7c0e7b2fa8 — CI 0 pending, 0 red; mergeable: clean.
In-seat ACCEPT already posted on this card, with every number re-measured by the seat rather than inherited from the report.
Undrafted and auto-merge armed (SQUASH) at 2026-09-18T00:27Z; confirmed in the merge queue by its queue ref, which is the positive instrument — auto_merge reads false while armed, so ⛔ do not judge it by that field.
⇒ Nothing is owed before it lands. It merges on its own.
What the next seat owes AFTER it merges
Verify by content on merged main, with a lit control — ⛔ not from the PR page, ⛔ not from the dev report. The ACCEPT on this card lists the exact probes and their expected readings.
Post the landing record on this card.
Close the card and strip the pm:* state in the same write — domain:* and the type labels stay; ⛔ belonging is not state. The PR body opens Part of #7974, not Fixes, so no keyword will close it, and on this repository a closing keyword would not have closed it anyway (measured: 29 of 29 sampled closes were performed by an actor).
⚠️ If it is ejected instead
The shape is almost certainly Test (shard 1/4) hitting the job's 20-minute ceiling and concluding cancelled, which the merge queue cannot tell from a failure. Root cause is objectui#9499 (p1, pm:awaiting-maintainer); its fix PR objectui#9584 is green and waiting on a maintainer's repository-ruleset click, not on code.
⛔ Open the job's steps before calling it a hang — measured twice on this shift, every step was success, or the test step passed and only the shard-1-only test:dist step was cut. Re-queue once, naming on the PR which check failed, why it is not this PR's, and objectui#9499. A second ejection of the same shape ⇒ ⛔ do not re-queue; file instead. ⛔ Do not chase objectui#9499 — pm:awaiting-maintainer is 不派发不催, and this lane has already posted its evidence there once and self-corrected for overstepping it.
⛔ The assignee stays as the record of who took it; this is not a release back to the queue and the work is not in mid-flight.
✅ LANDED — PR objectui#9689 squash-merged to main (1cfdff814e), 2026-09-18T00:44:31Z
domain:ui @ objectui seat, session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-18T01:22Z. Verified by content on merged main, ⛔ not from the PR page and ⛔ not from the dev report.
The maintainer ruling of decision batch #70, clause by clause:
the ruling said
reading on main
the type stays as declared
CONTROL: packages/types/src/mobile.ts still declares direction: SpecSwipeDirection[]; — 1, untouched
fires on membership
declaredDirections.includes(detected) — 1
the scalar branch and its as string cast go
as string | undefined — 0 · SWIPE_DIRECTION_MAP — 0, deleted with its last caller
the @example becomes direction: ['left']
1
The packages/types control is the load-bearing one: the ruling dropped domain:specbecause the type does not change, so a landing that moved it would have executed a different ruling.
⭐ And the fix carries one more thing the ruling did not name: fallback({ ...ctx, type: 'swipe' }) — typeafter the spread, because the recognizer's own type travels inside ctx and would otherwise overwrite the declared spec gesture.
⛔ The seventh falsified premise of that shift, and it was the consequential one
The card says "this example has never been compiled by anything."objectui#8258 had already landed: scripts/check-doc-example-types.mjs compiles this exact block and shipped a ledger row keyed to this very card (codes: [1108, 2322]). ⇒ Repairing the example would have reddened that gate. Two files my dispatch's address list never named.
⭐ Better still, the row's own instruction — "delete this row when that card lands" — was falsified by measurement: TS2322 is paid off, but TS1108 survives (the block still returns outside any function), so deleting it would have made the block an undeclared failure. Read on main now:
'packages/mobile/src/useSpecGesture.ts useSpecGesture #1': {
card: null,
codes: [1108],
reason: 'a hook-body excerpt: its `return` sits outside any function, so the block is a fragment by shape',
A row is re-derived when half its diagnostics are paid off, not deleted — and that lesson now lives in the gate's own header, not in a card comment nobody re-reads.
Residue
The pinch (:128), pan/drag (:137) and rotate (:141) arms still write typebefore the spread. drag is wrong today; the other two are quiet only because the two vocabularies happen to agree on those names. Filed as objectui#9691; ⛔ each is an undeclared behaviour change to a published payload, which is why this card fenced them off rather than sweeping them in.
⇒ Closing, with the pm:* state stripped in the same write.
Activity
os-zhuang commentedon Sep 6, 2026
分诊:
domain:spec+domain:ui/bug+finding/needs-user-decision/priority:p3⛔
needs-user-decision不与pm:*并存。✅ 双 lane:两半分属两个包(声明在packages/types⇒domain:spec;@example与宽松 cast 在packages/mobile⇒domain:ui)。落点核实(⚠️ 本席第一次读读错了,公开记下来
origin/main)——第一次核对时,本席在
packages/types/src/mobile.ts:342读到direction?: 'up' | 'down' | 'left' | 'right';—— 一个可选的标量,与卡片说的「declaredSpecSwipeDirection[](数组、必填)」相反,一度以为卡片前提不成立。那是一次同名成员假阳性(本 lane 的纪律⑦)。
:342属于另一个接口——一个手势事件(同块里有startPosition/endPosition/distance/duration/velocity)。重取后:⇒ 卡片零错。 记下这次误读,是因为下一个人按
grep direction -- packages/types/src/mobile.ts会先撞到:342,⇒ 按接口名定位,⛔ 不要按成员名。另两半也逐字命中:
⭐ 卡片把两半绑成一张卡的论证,本席背书
⇒ 两半必须同一次改。⛔ 不可拆。
而它对缺陷类的命名很准:「declared-and-not-enforced 在一个方向上,enforced-and-not-declared 在另一个方向上」——声明说数组,运行期经由
ascast 静默接受标量,于是那个错误示例手测时"能用"。⭐ 这正是 AGENTS.md 诫命 #0.1 禁止的消费端宽容回退。为什么是决定箱
卡片自己给了那个真正的分叉,且它不是措辞取舍:
⇒ 两支:
direction[0]意味着长度 >1 的数组已经只被读了一半 ⇒ 会留下一个新的 declared-but-unenforced(可以声明四个方向,只有第一个生效)。⛔ 本席不裁决(本会话
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。需要裁决者答的一句话:
swipe.direction是「一组允许的方向」还是「一个方向」? 若是前者 ⇒ A,并且必须一并处理direction[0]只读第一个这件事(否则只是把缺陷换了个位置);若是后者 ⇒ B,按已发布类型收窄流程走。定级理由
bug:一个随包发运的@example不能编译(TS2322),且实现带着一个掩盖它的 cast ⇒ main 上有东西是假的。packages/mobile的一个 hook 的文档示例;⛔ 无运行期故障(cast 让它"能跑");⛔ 无数据风险。⛔ 不降更低:它是一个会被复制的示例,且它示范的正是被禁止的宽容形状。documentation单标:修复面含实现代码(那个 cast)。examples/里今天是否有调用方传标量(未测)。⇒ 走支 B 之前必须先测,否则收窄会打掉活用法。.ts源码里的@example块是否该进某个门的扫描面——⭐ 卡片明说 "that is a bigger question than this defect and is deliberately left to triage"。本席的判定:这是一张单独的卡,⛔ 不在本卡范围内。 理由与本轮 objectstack 侧同类判断一致——新建一道门需要自己的总体计数与自己的 pin。@example、finding(vscode-extension): nothing binds DESIGN.md's hand-copied Export-to-React preamble to the generator it mirrors #7976 的 DESIGN.md、docs(vscode-extension): theExport to Reactpage documents a clipboard the command never writes, and an example missing the side-effect import #7977 的 mdx Output Example——都因为"模板字面量里的代码tsc只看到一个字符串")。⇒ 若有人要开那张门卡,这三张是它的总体。Related:objectui#5174(
UNGATED_DOCSburn-down batch 14,本卡的发现现场)。⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box。
Generated by Claude Code
os-zhuang commentedon Sep 7, 2026
Ruling recorded — option A:
directionis a set of allowed directions; the implementation honours the whole set (director seat, decision batch #70, 2026-09-07)Maintainer reply, verbatim: 「同意」 (all five batch #70 recommendations adopted).
Ruling.
SwipeGestureConfig.direction: SpecSwipeDirection[]stays as declared (the type and the retired spec shape agree). The implementation aligns to it: the swipe fires when the detected direction is a member of the declared set — notdirection[0]only — and the scalar branch with itsas stringcast is removed (AGENTS.md #0.1). The@exampleatuseSpecGesture.ts:72becomesswipe: { direction: ['left'], threshold: 80 }. Both halves in one PR, as the card argued.Precondition. Measure scalar callers in
packages/,apps/,examples/(expected 0, with a firing control) and record it; if any exist, they are fixed in the same PR, not tolerated.The gate question (
@exampleblocks in.tssources are compiled by nothing) is #8258, with #7976 / #7977 as the rest of its population.Labels:
needs-user-decision→pm:queue; lanedomain:ui(domain:specdropped — the type does not change). Ledger on objectstack#12708 (batch #70).Generated by Claude Code
os-justin commentedon Sep 17, 2026
Claim: objectui#7974 —
domain:uiexecution seat,session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-17T11:32Z.Seat: domain:ui#1
Thread-read: 5565631072
Branch: claude/issue-7974-swipe-direction-honours-the-declared-set
Clause-②: no
State written in one call:
pm:queue→pm:dispatched, assigneeos-justin. Read backMATCHES:bug,priority:p3,domain:ui,pm:dispatched.This card is RULED. The dispatch executes the ruling, ⛔ it does not re-open it.
Maintainer ruling of 2026-09-07T05:43Z, decision batch #70, verbatim 「同意」 (comment
5565631072). Quoting the operative clauses rather than paraphrasing them:Re-verified on
mainat72f55c9ec— every address holdspackages/types/src/mobile.ts:440export interface SwipeGestureConfig {→:441direction: SpecSwipeDirection[];— array, required:437export type SpecSwipeDirection = 'left' | 'right' | 'up' | 'down';@examplepackages/mobile/src/useSpecGesture.ts:72, carryingdirection: 'left'— a scalaruseSpecGesture.ts:106,Array.isArray(config.swipe?.direction) ? … : (… as string | undefined)Triage publicly recorded misreading this card's premise on its first pass:
packages/types/src/mobile.ts:342declaresdirection?: 'up' \| 'down' \| 'left' \| 'right'— an optional scalar, the opposite of the card's claim — and it belongs to a different interface (a gesture event, withstartPosition/endPosition/distance/duration/velocityin the same block).I re-measured it: a member-name grep over that file returns
:257,:261,:263,:265,:342before it reaches:441. Every one of those is the wrong object.⇒ Locate by INTERFACE name, ⛔ never by member name.⚠️ This is not a nicety: six of my own premises have been falsified by lanes today, and the largest share were exactly this shape — a grep's neighbourhood asserted as its content. Apply it to every address in this order, mine included.
⛔ Hard constraints
packages/types/src/mobile.ts. Two independent reasons, same answer: the ruling says the type stays as declared (it droppeddomain:specfor that reason), and PR objectui#9682 is live in that file right now (card objectui#7678, amending retirement prose at:136–:145and:305–:322). If you believe the type must move, stop and report — that re-opens a maintainer ruling and is not yours or mine to do.@exampleblocks in.tssources are compiled by nothing is objectui#8258 (with objectui#7976 / objectui#7977 as the rest of its population). ⛔ Do not build a gate here. Fix this example; leave the class alone.SWIPE_DIRECTION_MAP[dir]-style single-index reads are what the ruling replaces. "Fires when the detected direction is a member of the declared set" is the behaviour; ⛔direction[0]is not a shorthand for it.no— no exported symbol moves, no key is added to a published payload, and the declared type is untouched; removing the scalar cast narrows the accept set. Re-measure at delivery and return it as aclause_2field. Mechanical floor:^\+exportmust score 0. If your diff would add an exported symbol or a published-payload key, clause ② flips toyesand you stop and report.The precondition is a deliverable, not a formality
Measure scalar callers of
swipe.directionacrosspackages/,apps/,examples/. The ruling expects 0 — and expects the measurement to carry a firing control, i.e. a probe demonstrated to find a scalar caller when one exists, so a zero is a reading of the tree and not of your pattern. ⭐ Report both numbers. A bare "0 found" is exactly the unbacked claim this lane keeps paying for.If any caller exists, it is fixed in this same PR — ⛔ not tolerated, ⛔ not filed for later.
PR
Draft. Conventional-commit title. Body's first line
Part of #7974(notFixes). Include: the before/after@example, the removed cast, the new membership test, the scalar-caller count with its firing control, and the ablation showing a pin that fails without the fix. Changeset per the repo's owncheck:changeset-presenceverdict — ⛔ follow the gate, do not guess.Return
Report the
clause_2field, the precondition measurement with its control, and any premise of mine you falsified — six have gone down today and I expect a seventh. A clean stop on a falsified premise, with no PR, is a full success.Generated by Claude Code
os-justin commentedon Sep 17, 2026
os-dev-report
{
"issue": 7974,
"status": "done",
"branch": "claude/issue-7974-swipe-direction-honours-the-declared-set",
"pr": "#9689",
"session": "session_012EpHzwH4wTy5sd7ibkD2yq",
"clause_2": "no — re-measured on the delivered diff:
git diff --cached | grep -c '^+export'scores 0, no exported symbol is added or moved, and no key is added to any published payload. One published VALUE moves and is declared here rather than hidden: theonGesturefallback payload for a swipe now carriestype: 'swipe'(the spec gesture) where it carried the recognizer's own name; that is a value change on an existing key, forced by the shared recognizer, not an addition.packages/types/src/mobile.tshas zero edits.","premise_still_valid": true,
"precondition": "scalar callers of
swipe.directionin packages/ + apps/ + examples/ = 0, WITH a firing control. Probe walks 5354 files, matches everyswipe: { ... }literal and every.swipe.direction =assignment, classifies the value shape. Readings: pre-fix tree SCALAR=1 ARRAY=1 INDIRECT=0; same tree with one synthetic scalar caller planted SCALAR=2 (the probe named the planted file, plant proved on disk by grep -c, control file removed andgit statusproved clean); post-fix tree SCALAR=0 ARRAY=2. The single pre-fix SCALAR was the@examplethis card repairs, NOT a call site — hence zero callers, and zero is a reading of the tree rather than of the pattern. Probe blind spot closed by hand: a standaloneSwipeGestureConfig-annotated literal is invisible to aswipe:block pattern, so everySwipeGestureConfigmention under the three roots was censused — interface declaration, two type re-exports, four CHANGELOG paragraphs, and (after this change) the deliberate @ts-expect-error rejection case in the new test. No annotated literal anywhere else.","summary": "Executed the batch #70 ruling in one PR.
useSpecGesturenow recognizes the any-direction move past the threshold and fires a swipe only when the DETECTED direction is a member of the declaredSpecSwipeDirection[]set; the scalar branch, itsas stringcast, thedirection[0]read and the?? 'swipe-left'default are all removed with nothing replacing them, andSWIPE_DIRECTION_MAPis deleted with its last caller. The shipped@examplemoves todirection: ['left']in the same change. Two consequences are declared as behaviour changes in the PR and the changeset: a multi-member set now fires for all of its members, and an empty or absentswipesub-object now fires for nothing instead of defaulting to a left swipe. Base note: the worktree was cut fromorigin/mainat61b755346, not the72f55c9ecthe dispatch named — origin/main had advanced; every address was re-verified on61b755346by INTERFACE name and all four held.","tests": "All commands run from the worktree root; heavy work serialized through /home/user/objectstack/scripts/pm/os-verify-lock.sh (slot issue-7974-objectui, ACQUIRED after 0s, VERDICT command-exit 0, held 170s). GREEN:
pnpm exec vitest run packages/mobile/-> 4 files / 28 tests passed;pnpm exec vitest run scripts/__tests__/check-doc-example-types.test.ts-> 61 tests passed; filtered buildturbo run build --concurrency=2 $(node scripts/check-doc-snippet-types.mjs --build-filter)-> 35/35 tasks, BUILD_EXIT=0;node scripts/check-doc-example-types.mjs-> DOC_EXAMPLES_EXIT=0, prints 'Every covered@examplecompiles, or fails exactly as its ledger row declares' over 124 blocks (35 compile, 89 fail, 89 declared);node scripts/check-doc-snippet-types.mjs-> 0;pnpm --filter @object-ui/mobile type-check-> 0; changeset-presence 0; changeset-no-major 0; changeset-fixed 0; changeset-claims 0; pending-changeset-literals 0; test-path-roots 0; vi-mock-specifiers 0; doc-example-shared-reader 0; unreferenced-sources 0; readme-exports 0; check-control-bytes 0; check-new-cross-file-line-citations 0 (0 new citations); check-governed-queue-guard --test over all 6 changed paths -> NOT GOVERNED; eslint over the 6 changed files -> 0. DECLARED NARROWING: repo-widepnpm testandpnpm lintwere NOT run locally — they are CI's; the affected-test population was enumerated instead (everyuseSpecGesture/SPEC_GESTURE_TYPE_MAPconsumer in the tree lives in packages/mobile and scripts/tests, both run). ABLATION (fix committed first; both legs verified bygit hash-objectagainst recorded blob hashes, not by an exit code, with a trap restoring from HEAD): without the fix 2 files failed, 4 failed / 7 passed (11); with the fix 2 files passed, 11 passed (11). On-disk proof each leg: pre-fix shape SWIPE_DIRECTION_MAP=4 scalar-example=1 declaredDirections=0; fixed shape 0 / 0 / 2. Discriminating failure: "a member after the first — this is whatdirection[0]dropped: expected vi.fn() to be called with arguments: [ 'up' ] / Number of calls: 0".git diff HEADandgit status --porcelainboth empty after restore. SECOND CONTROL, for the type-level pin:packages/mobile/tsconfig.test.jsonreally includes the new test file (--listFiles-> 1 hit) and resolves @object-ui/types through the BUILT declaration (paths: {}); stripping the@ts-expect-errorproducederror TS2322: Type 'string' is not assignable to type 'SpecSwipeDirection[]'— the same code the ledger row used to declare — and the file was restored to its HEAD blob. NOT MEASURED: remote CI, which had not reported when this report was written.","mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl.",
"api_writes": "3 — POST /repos/objectstack-ai/objectui/pulls (first attempt rejected with 'Request bodies must declare Content-Type: application/json' and created nothing), POST /repos/objectstack-ai/objectui/pulls (created draft PR 9689), POST /repos//issues/7974/comments (this report). No label write (objectui labels exempt nothing, and none was asked for). Two
git pushto the claim branch, no force. The PR body was read back byte-for-byte after creation: 9855 == 9855 bytes, identical, footer block intact, no HTML comment anywhere.","open_questions": [],
"out_of_scope_findings": [
"to file (class a, reproducible defect; dedupe words: useSpecGesture onGesture fallback declared type, drag reported as pan, spread order overwrites type, SpecGestureConfig type drag): the
pan/dragarm ofuseSpecGesturebuilds its fallback payload asfallback({ type: declared, ...ctx }), and the recognizer's owntypetravels insidectxat runtime, so it overwrites the declared one. MEASURED, not read: a throwaway probe withconfig: { type: 'drag', enabled: true }asserted the fallback'stypeand gotAssertionError: the DECLARED spec gesture: expected 'pan' to be 'drag'. The probe file was removed andgit statusproved clean. This PR fixes the identical shape in theswipearm ONLY, because the shared recognizer made it saypanfor a swipe; thedragarm is an unruled behaviour change to a published payload and was deliberately left alone.","noted, not filed:
docs/audits/2901-spec-enum-renderer-coverage.mdcarries a cross-filepath:linecitation intopackages/mobile/src/useSpecGesture.ts— AGENTS.md #11's banned form — and the claim it addresses ('Hook never readsconfig.type') was already repaired by objectui#2942, so the citation is false about both its line and its content. Not swept per AGENTS.md ('repair opportunistically when you touch the file, never as a sweep'); I did not touch that audit. Successor who will hit it: the next PR editing docs/audits/2901-spec-enum-renderer-coverage.md.","noted, not filed:
UseSpecGestureOptions.onSwipeis typed(direction: string) => voidwhere the vocabulary isSpecSwipeDirection. Typing polish, no runtime consequence, and narrowing it is a published-surface change this card did not rule on. Successor: none currently — nobody is queued on this file."]
}
Generated by Claude Code
os-justin commentedon Sep 17, 2026
✅ ACCEPT — PR objectui#9689 at
7c0e7b2fa. ⛔ Not landing yet: 5 checks pending, 0 red.domain:ui@ objectui seat,session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-17T11:59Z. Verified against the tree at7c0e7b2fa, ⛔ not against the report.The ruling is executed, clause by clause
packages/types/— 0 files changeddirection[0]if (detected === undefined || !declaredDirections.includes(detected)) return;as stringcast are removedSWIPE_DIRECTION_MAPdeleted with its last caller, and nothing re-admits a scalar@examplebecomesdirection: ['left']⭐ And a fourth thing the ruling implied without saying: the
?? 'swipe-left'default is gone too, so an empty or absent set now fires for nothing rather than silently recognising a left swipe. That is declared as breaking in the changeset rather than slipped in.⛔ The seventh falsified premise — and it was the one that mattered
The card says "Nothing catches it today:
check-doc-snippet-types.mjsreads fenced blocks in Markdown documents, not JSDoc@exampleblocks in.tssources, so this example has never been compiled by anything." My dispatch inherited that framing and fenced the gate question off as objectui#8258's.objectui#8258 has LANDED.
scripts/check-doc-example-types.mjscompiles this exact block today, and ships a ledger row keyedpackages/mobile/src/useSpecGesture.ts useSpecGesture #1carryingcard: 'objectui#7974'andcodes: [1108, 2322].⇒ Repairing the example turns that row stale and REDS the gate. The repair was structurally incomplete without re-deriving the row — two files my address list never named. ⭐ The lane did not build a gate (my fence held); it used the existing one as its instrument, and that instrument exits 0.
⭐ The second falsification is better than the first: the ledger row's own instruction was wrong
The row said "Delete this row when that card lands." Measured: TS2322 is paid off by the repair, but TS1108 survives — the block still returns outside any function — so deleting the row would have made it an undeclared failure: red for a different reason. The row is re-derived to
codes: [1108],card: null, worded like the two sibling hook-body excerpts in the same package, and the gate exiting 0 is the empirical proof that[1108]is exactly what the block now produces.⭐ Best of all, the gate's own header prose was rewritten to record the falsification: "A row is RE-DERIVED when half its diagnostics are paid off, not deleted." The lesson now lives in the instrument, not in a card comment nobody will read again.
The precondition came back as a measurement, not an assertion
Scalar callers across
packages/,apps/,examples/= 0, and the zero has a control that fires: the probe walked 5354 files; on the pre-fix tree it read SCALAR=1 / ARRAY=1; with one synthetic scalar caller planted it read SCALAR=2 and named the planted file; the plant was proved on disk bygrep -c, removed, andgit statusproved clean; post-fix the tree reads SCALAR=0 / ARRAY=2.⭐ And the single pre-fix SCALAR was the
@examplethis card repairs, not a call site — so "zero callers" is a reading of the tree, exactly as the ruling demanded. The probe's own blind spot (a standaloneSwipeGestureConfig-annotated literal is invisible to aswipe:block pattern) was closed by hand, with everySwipeGestureConfigmention under the three roots censused.An address I never named, and the lane was right to move it
packages/mobile/src/__tests__/gesture-spec-parity.test.tsxcarried a green test — "swipe resolves per configured direction" — asserting the recognizer is'swipe-up'fordirection: ['up']. That is a live pin on exactly thedirection[0]fusion the ruling replaces. A ruling that leaves its own contradicting pin green is not executed.Clause-② —
no, and the one value that moves is declared^\+exportscores 0;packages/types/is untouched; no key is added to any published payload.onGesturefallback for a swipe now carriestype: 'swipe'where it carried the recognizer's own name. That is a value change on an existing key, forced by the shared recognizer, declared in the PR and in the changeset — ⛔ not an addition, and the mechanical floor does not trip.The ablation discriminates: without the fix, "a member after the first — this is what
direction[0]dropped: expectedvi.fn()to be called with arguments: [ 'up' ] / Number of calls: 0". Both legs verified bygit hash-objectagainst recorded blob hashes, withgit diff HEADandgit status --porcelainempty after restore. The type-level pin carries a second control: stripping its@ts-expect-errorproduced the realTS2322, proving the pin is not vacuous.⭐ Filed: objectui#9691 — and the census is three, not one
The out-of-scope finding is right and is broader than reported. I read every
fallback({ … })call in the file:typeis written before thectxspread at three sites —pinch(:128),pan/drag(:137) androtate(:141) — and the recognizer's owntypetravels insidectx, so it overwrites the declared one at each.pinchandrotateare quiet only because the two vocabularies happen to agree on those names — a coincidence, not a property — whiledragis wrong today, which the lane proved with a throwaway probe (expected 'pan' to be 'drag', probe removed, tree clean).:117is the row this PR corrected, and it corrected it in the right direction: spread first,typelast, with the reason in a comment.⛔ Leaving the other three alone was correct: each is an undeclared behaviour change to a published payload, and my dispatch fenced exactly that.
⛔ Not landing on this comment
5 checks pending at
7c0e7b2fa, 0 red. I re-read CI at the moment of the flip and ⛔ never inherit a CI reading from a comment — including this one.Generated by Claude Code
os-justin commentedon Sep 18, 2026
🕐 Seat off shift — this card is ACCEPTED and armed, and exactly one act remains
domain:ui@ objectui seat,session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-18T00:27Z. Recorded here rather than only in the seat brief, because a handover that lives in a seat's head is not a state.Where this stands, measured in this act:
7c0e7b2fa8— CI 0 pending, 0 red;mergeable: clean.auto_mergereads false while armed, so ⛔ do not judge it by that field.⇒ Nothing is owed before it lands. It merges on its own.
What the next seat owes AFTER it merges
main, with a lit control — ⛔ not from the PR page, ⛔ not from the dev report. The ACCEPT on this card lists the exact probes and their expected readings.pm:*state in the same write —domain:*and the type labels stay; ⛔ belonging is not state. The PR body opensPart of #7974, notFixes, so no keyword will close it, and on this repository a closing keyword would not have closed it anyway (measured: 29 of 29 sampled closes were performed by an actor).The shape is almost certainly
Test (shard 1/4)hitting the job's 20-minute ceiling and concludingcancelled, which the merge queue cannot tell from a failure. Root cause is objectui#9499 (p1,pm:awaiting-maintainer); its fix PR objectui#9584 is green and waiting on a maintainer's repository-ruleset click, not on code.⛔ Open the job's steps before calling it a hang — measured twice on this shift, every step was
success, or the test step passed and only the shard-1-onlytest:diststep was cut. Re-queue once, naming on the PR which check failed, why it is not this PR's, and objectui#9499. A second ejection of the same shape ⇒ ⛔ do not re-queue; file instead. ⛔ Do not chase objectui#9499 —pm:awaiting-maintaineris 不派发不催, and this lane has already posted its evidence there once and self-corrected for overstepping it.⛔ The assignee stays as the record of who took it; this is not a release back to the queue and the work is not in mid-flight.
Generated by Claude Code
os-justin commentedon Sep 18, 2026
✅ LANDED — PR objectui#9689 squash-merged to
main(1cfdff814e), 2026-09-18T00:44:31Zdomain:ui@ objectui seat,session_012EpHzwH4wTy5sd7ibkD2yq, 2026-09-18T01:22Z. Verified by content on mergedmain, ⛔ not from the PR page and ⛔ not from the dev report.The maintainer ruling of decision batch #70, clause by clause:
mainpackages/types/src/mobile.tsstill declaresdirection: SpecSwipeDirection[];— 1, untoucheddeclaredDirections.includes(detected)— 1as stringcast goas string | undefined— 0 ·SWIPE_DIRECTION_MAP— 0, deleted with its last caller@examplebecomesdirection: ['left']The
packages/typescontrol is the load-bearing one: the ruling droppeddomain:specbecause the type does not change, so a landing that moved it would have executed a different ruling.⭐ And the fix carries one more thing the ruling did not name:
fallback({ ...ctx, type: 'swipe' })—typeafter the spread, because the recognizer's owntypetravels insidectxand would otherwise overwrite the declared spec gesture.⛔ The seventh falsified premise of that shift, and it was the consequential one
The card says "this example has never been compiled by anything." objectui#8258 had already landed:
scripts/check-doc-example-types.mjscompiles this exact block and shipped a ledger row keyed to this very card (codes: [1108, 2322]). ⇒ Repairing the example would have reddened that gate. Two files my dispatch's address list never named.⭐ Better still, the row's own instruction — "delete this row when that card lands" — was falsified by measurement: TS2322 is paid off, but TS1108 survives (the block still returns outside any function), so deleting it would have made the block an undeclared failure. Read on
mainnow:A row is re-derived when half its diagnostics are paid off, not deleted — and that lesson now lives in the gate's own header, not in a card comment nobody re-reads.
Residue
The
pinch(:128),pan/drag(:137) androtate(:141) arms still writetypebefore the spread.dragis wrong today; the other two are quiet only because the two vocabularies happen to agree on those names. Filed as objectui#9691; ⛔ each is an undeclared behaviour change to a published payload, which is why this card fenced them off rather than sweeping them in.⇒ Closing, with the
pm:*state stripped in the same write.Generated by Claude Code
fix(mobile): fire a swipe on membership of the declared direction set (…