Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
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

Closed

Description

@baozhoutao

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:

config: { type: 'swipe', enabled: true, swipe: { direction: 'left', threshold: 80 } },

SwipeGestureConfig.direction is declared in @object-ui/types as SpecSwipeDirection[] — an ARRAY:

export interface SwipeGestureConfig {
    direction: SpecSwipeDirection[];
    threshold?: number;
    velocity?: number;
}

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.

Activity

  1. added
    bugSomething isn't working
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 6, 2026
  2. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊: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'; —— 一个可选的标量,与卡片说的「declared SpecSwipeDirection[](数组、必填)」相反,一度以为卡片前提不成立。

    那是一次同名成员假阳性(本 lane 的纪律⑦)。:342 属于另一个接口——一个手势事件(同块里有 startPosition / endPosition / distance / duration / velocity)。重取后:

    packages/types/src/mobile.ts:437   export type SpecSwipeDirection = 'left' | 'right' | 'up' | 'down';
    packages/types/src/mobile.ts:440   export interface SwipeGestureConfig {
    packages/types/src/mobile.ts:441     direction: SpecSwipeDirection[];      ← 数组,且必填
    packages/types/src/mobile.ts:442     threshold?: number;
    packages/types/src/mobile.ts:443     velocity?: number;
    packages/types/src/mobile.ts:444   }
    

    ⇒ 卡片零错。 记下这次误读,是因为下一个人按 grep direction -- packages/types/src/mobile.ts 会先撞到 :342,⇒ 按接口名定位,⛔ 不要按成员名。

    另两半也逐字命中:

    packages/mobile/src/useSpecGesture.ts:72    *   config: { type: 'swipe', enabled: true, swipe: { direction: 'left', threshold: 80 } },
    packages/mobile/src/useSpecGesture.ts:106-108
          const dir = Array.isArray(config.swipe?.direction)
            ? config.swipe?.direction[0]
            : (config.swipe?.direction as string | undefined);
    

    ⭐ 卡片把两半绑成一张卡的论证,本席背书

    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.

    ⇒ 两半必须同一次改。⛔ 不可拆。

    而它对缺陷类的命名很准:「declared-and-not-enforced 在一个方向上,enforced-and-not-declared 在另一个方向上」——声明说数组,运行期经由 as cast 静默接受标量,于是那个错误示例手测时"能用"。⭐ 这正是 AGENTS.md 诫命 #0.1 禁止的消费端宽容回退。

    为什么是决定箱

    卡片自己给了那个真正的分叉,且它不是措辞取舍:

    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.

    ⇒ 两支:

    • A. 保留数组(改示例、删 cast)⇒ 但 direction[0] 意味着长度 >1 的数组已经只被读了一半 ⇒ 会留下一个新的 declared-but-unenforced(可以声明四个方向,只有第一个生效)。
    • B. 收窄成标量(与运行期真正消费的东西一致)⇒ 收窄一个已发布类型的接受集 ⇒ 破坏性,人工地板。

    ⛔ 本席不裁决(本会话 claude-opus-5,CONTRACT_REVIEW_TIER 硬门要求 fable)。

    需要裁决者答的一句话:swipe.direction 是「一组允许的方向」还是「一个方向」? 若是前者 ⇒ A,并且必须一并处理 direction[0] 只读第一个这件事(否则只是把缺陷换了个位置);若是后者 ⇒ B,按已发布类型收窄流程走。

    定级理由

    • bug:一个随包发运的 @example 不能编译(TS2322),且实现带着一个掩盖它的 cast ⇒ main 上有东西是假的。
    • p3:packages/mobile 的一个 hook 的文档示例;⛔ 无运行期故障(cast 让它"能跑");⛔ 无数据风险。⛔ 不降更低:它是一个会被复制的示例,且它示范的正是被禁止的宽容形状。
    • ⛔ 不挂 documentation 单标:修复面含实现代码(那个 cast)。

    ⚠️ 卡片明说未测的两项,本席原样转达为取卡前置

    1. 本仓或 examples/ 里今天是否有调用方传标量(未测)。⇒ 走支 B 之前必须先测,否则收窄会打掉活用法。
    2. .ts 源码里的 @example 块是否该进某个门的扫描面——⭐ 卡片明说 "that is a bigger question than this defect and is deliberately left to triage"。本席的判定:这是一张单独的卡,⛔ 不在本卡范围内。 理由与本轮 objectstack 侧同类判断一致——新建一道门需要自己的总体计数与自己的 pin。⚠️ 但值得记下:本轮 objectui 侧已见到三张卡指向同一个盲区(本卡的 JSDoc @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): the Export to React page documents a clipboard the command never writes, and an example missing the side-effect import #7977 的 mdx Output Example——都因为"模板字面量里的代码 tsc 只看到一个字符串")。⇒ 若有人要开那张门卡,这三张是它的总体。

    Related:objectui#5174(UNGATED_DOCS burn-down batch 14,本卡的发现现场)。


    ⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box。


    Generated by Claude Code

  3. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    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).


    Generated by Claude Code

  4. added and removed
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    on Sep 7, 2026
  5. self-assigned this
    on Sep 17, 2026
  6. os-justin commented on Sep 17, 2026

    @os-justin
    Collaborator

    Claim: objectui#7974 — domain:ui execution 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, 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

    probe reading
    the declared type packages/types/src/mobile.ts:440 export interface SwipeGestureConfig { → :441 direction: SpecSwipeDirection[]; — array, required
    the vocabulary :437 export type SpecSwipeDirection = 'left' | 'right' | 'up' | 'down';
    the @example 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, :342 before 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.


    Generated by Claude Code

  7. os-justin commented on Sep 17, 2026

    @os-justin
    Collaborator

    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: 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."
    ]
    }


    Generated by Claude Code

  8. os-justin commented on Sep 17, 2026

    @os-justin
    Collaborator

    ✅ 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.


    Generated by Claude Code

  9. os-justin commented on Sep 18, 2026

    @os-justin
    Collaborator

    🕐 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

    1. 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.
    2. Post the landing record on this card.
    3. 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.


    Generated by Claude Code

  10. os-justin commented on Sep 18, 2026

    @os-justin
    Collaborator

    ✅ 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:spec because 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' }) — type after 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 type before 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.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions