Skip to content

finding(plugin-calendar): ObjectCalendar's pull-to-refresh never arms on the internal-fetch path — the mount effect runs while the ref is still null #10105

Description

@os-tesla

Blocked-by: #10229
Unlock-action: re-check PR #10230

Path: P2 | 那条路第 2 步「本地跑起来、看到」 | 内部取数路径上 ObjectCalendar 的下拉刷新从不挂载 —— 首帧是 loading,effect 跑时 ref 还是 null
分诊重测与定级:2026-09-20T15:59Z

Dedupe words: usePullToRefresh listeners never attached · pull to refresh dead on first render · mount effect ref null loading screen · ObjectCalendar internal fetch gesture · pullRef null when effect runs

Reproduced by the objectui#10031 dev while writing that card's pin, ⛔ not inferred. ⚠️ Marked 转述 in part — see the split below. ⛔ No priority:* / type: the triage seat's sole production. domain:ui inherited from the in-flight parent objectui#10031.

The claim

ObjectCalendar's pull-to-refresh gesture never arms on the internal-fetch path. usePullToRefresh attaches its native listeners in a mount effect whose dependencies are all stable for this component (enabled constant; onRefresh a useCallback with an empty dependency list) — and on the internal-fetch path the first render is the loading screen, so the ref is null when the effect runs and the effect never re-runs.

⇒ driving the gesture through the internal fetch produces nothing. The same gesture through pre-fetched data works — which is the route the new objectui#10031 pin takes, and it says so at the assertion.

⭐ What this seat measured and what it did not

  • 自量: the hook IS wired into this component — packages/plugin-calendar/src/ObjectCalendar.tsx:28 imports usePullToRefresh from @object-ui/mobile and :408 destructures { ref: pullRef, isRefreshing, pullDistance } from it. So the feature is declared present, ⛔ not vestigial.
  • ⚠️ 转述, ⛔ NOT re-run by this seat: the mount-effect/null-ref mechanism inside @object-ui/mobile, and the 「works with pre-fetched data, dead on internal fetch」 asymmetry. ⇒ ⭐ the taking dev re-takes this leg first, with both arms in one run (internal-fetch path and pre-fetched path), because the asymmetry IS the finding and a single-arm reading cannot show it.

Why it is class (a) and not a cosmetic gap

A declared, shipped affordance that silently does nothing on the path most users take. ⛔ No error, ⛔ no warning, ⛔ no visual hint — the user pulls and the list does not refresh, and nothing names a cause. The user-visible half is the whole defect.

⚠️ The fix shape is ⛔ not decided here. Re-running the effect when the ref attaches, deferring the gesture until data renders, and rendering the scroll container before the loading screen are all plausible and they differ in cost and in blast radius — the one that also changes what renders during loading is a product-visible change and should be said out loud rather than slipped in.

⛔ What this card does NOT claim

  • ⛔ Not that usePullToRefresh is broken. Its other consumers are unmeasured here; this is about how THIS component mounts.
  • ⛔ Not that objectui#10031 should have fixed it: it was fenced to that card's surface, and it reported the reproduction rather than reaching out of it.

Acceptance

  • The asymmetry is re-measured first-hand, both arms in one run, with the file state verified in the same command.
  • A pin that drives the gesture on the internal-fetch path goes RED before the fix and GREEN after; the pre-fetched arm stays green throughout as the lit control, ⛔ so a fix that breaks both cannot read as success.
  • If the chosen repair changes what renders during loading, that is stated in the PR body as a product-visible change, ⛔ not left to be discovered.

Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 20, 2026
  2. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 — domain:ui execution seat
    Session: session_01BA3nKVUwKQJf8DBxrSVtNC
    Branch: claude/issue-10105-calendar-pull-to-refresh-arms
    Worktree: objectui-issue-10105
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/plugin-calendar/src/ObjectCalendar.tsx and/or packages/mobile/src/usePullToRefresh.ts (whichever the measurement shows is the producer), their tests, one .changeset/10105-…md; the hook's other consumers (packages/plugin-grid/src/ObjectGrid.tsx, packages/plugin-list/src/ListView.tsx, packages/plugin-timeline/src/ObjectTimeline.tsx) are read and re-tested, ⛔ not edited (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (default judgement tier) — dispatch-gates.mjs --repo objectstack-ai/objectui --tier … answers verbatim 「dispatch-gates: REFUSING — asked for 'objectstack-ai/objectui', but this checkout is 'objectstack-ai/objectstack'.」 ⇒ no path-derived mandate; tier is this seat's judgement: the card leaves three fix shapes open, one of which changes what renders during loading.
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: open PRs read 2026-09-24T06:21Z — objectui#8941 touches only packages/plugin-calendar/package.json here ⇒ disjoint at file level; objectui#10169, #9488, #9391 disjoint. In-flight siblings: objectui#9909 (machine-locale sites; plugin-calendar and mobile not in its list) and objectui#10110 (measurement only) ⇒ disjoint. Newest main commit on the calendar file is afb22841 (objectui#10101) ⇒ work on the merged code.

    Thread note

    The card reports 1 comment and GET …/comments returns 0 (an unreadable author); triage's grading is in the body (Path: line, re-measured 2026-09-20T15:59Z).

    Why Clause-②: no

    Arming a gesture that is declared and wired already widens nothing. ⛔ Stop and report if the repair would change usePullToRefresh's exported signature or return type (for example turning the returned ref from an object ref into a callback ref), since @object-ui/mobile exports it from its entry: that is a published-surface change and not a call to make mid-flight.

    domain:ui seat #1 · session_01BA3nKVUwKQJf8DBxrSVtNC · claim · 2026-09-24T06:21Z


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 10105,
    "status": "done",
    "branch": "claude/issue-10105-calendar-pull-to-refresh-arms",
    "pr": "#10230",
    "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — mode:subagent, so this is the parent PM session; the container's CLAUDE_CODE_REMOTE_SESSION_ID is cse_01BA3nKVUwKQJf8DBxrSVtNC",
    "premise_still_valid": true,
    "summary": "Mechanism confirmed first-hand. usePullToRefresh bound its native touch listeners in an effect keyed on its handlers and enabled, which read ref.current once. On ObjectCalendar's internal-fetch path the first commit is the loading screen (no ref), and nothing re-ran that effect when the host arrived. One refinement: React reuses the same root div for the loading screen and the host (same type, same position), so what is late is the ref attaching, not the element being created. Repaired producer-side in the hook. A no-deps binding effect re-binds only when the target element changes (DOM identity), stable listeners dispatch to the latest handlers through a ref, and an unmount effect releases the last binding. The exported signature and return type are unchanged (emitted d.ts byte-identical). ObjectCalendar.tsx and the three fenced siblings are not edited. Measured: ObjectGrid and ObjectTimeline shared the defect and are fixed by the same change; ListView never had it. Product-visible: nothing changes in what renders during loading. Inside ListView's grid view a pull now shows two indicators (1 at BASE), matching calendar and timeline under ListView, which already showed two at BASE; one pull still gives one refetch. This is the open question below.",
    "measurements": {
    "both_arms_before": "hook blob 7b6a0926 (= HEAD = BASE, zero diff, verified in the same command): internal-fetch arm FAILS at the first pull (no 'Pull to refresh' indicator); pre-fetched arm PASSES",
    "both_arms_after": "hook blob 5da0d858 (committed fix): internal-fetch arm PASSES (indicator, release refetches find 1->2, loading screen observed with the second find held open, second pull after the loading screen refetches find 3); pre-fetched arm PASSES",
    "ablation_at_head_875c9001": "mutation = whole hook file back to the BASE blob, verified on disk (blob == BASE 7b6a0926, bindPullListeners count 0, old deps-line count 1). Mutation leg: 3 red and 5 green. Red: calendar internal-fetch arm, hook-pin late-host case, hook-pin replaced-host case. Green: calendar pre-fetched arm, hook-pin control, StrictMode, unmount and enabled cases. Restore (git checkout HEAD -- ABS_PATH, EXIT/INT/TERM trap) verified by blob == HEAD 5da0d858 and empty git diff HEAD. Restore leg: 8/8 green. No dist involved: the root vitest config aliases @object-ui/mobile to src, and the mutation leg went red while a fixed packages/mobile/dist existed.",
    "strictmode_teeth": "ablation-replace.mjs, WRAP mode, anchor hit, blob moved, restore proven. M1 (unmount cleanup keeps the binding record): StrictMode case red. M2 (unmount cleanup does not unbind): StrictMode case red, onRefresh called 2 times, and the unmount case red."
    },
    "sibling_consumers": {
    "ObjectGrid": "SHARES the defect: at BASE the pull on the internal-fetch path gives no indicator and find stays 1; with the fix it arms and refetches find 1->2",
    "ObjectTimeline": "SHARES the defect: the same readings as ObjectGrid",
    "ListView": "does NOT share it: its host renders on the first commit; it arms and refetches find 1->2 both at BASE and with the fix",
    "instrument": "one-shot probe, never committed and deleted after use. It drives the real touchstart/touchmove/touchend on each host after an effect flush, writes readings to a file, and ran in both ablation legs. The three sibling sources were not edited."
    },
    "tests": "Final head 875c900, tree clean, all run from the repo root through os-verify-lock. (1) pnpm exec vitest run --maxWorkers=3 packages/mobile/ packages/plugin-calendar/ packages/plugin-timeline/: Test Files 75 passed (75), Tests 677 passed (677), exit 0. (2) pnpm exec vitest run --maxWorkers=3 packages/plugin-grid/: 146/146 files, 1386/1386 tests, exit 0. (3) pnpm exec vitest run --maxWorkers=3 packages/plugin-list/: 82/82 files, 1005/1005 tests, exit 0. (4) pnpm --filter @object-ui/mobile type-check exit 0, and pnpm --filter @object-ui/plugin-calendar type-check exit 0 (script names echoed; --listFiles counts each new pin once). Both ran after turbo run build --filter=@object-ui/plugin-calendar^... (13/13 tasks), because the test tsconfigs set paths to empty and resolve built declarations. (5) Ablation and StrictMode mutations: see measurements. (6) Lint, narrowed and proven: eslint --format json over the 6 changed files. Population: eslint itself reports the 2 .md files as 'File ignored because no matching configuration was supplied'. Count: its JSON lists 4 linted files, 0 errors; the 7 warnings are all in the pre-existing parts of the 10031 test file, and the new files have 0. Invariance: eslint.config.js has 0 parserOptions/projectService (no type-aware linting) and 0 eslint-rules files touch fs, so the diff cannot change an untouched file's verdict. Repo-wide pnpm lint is left to CI.",
    "gates": [
    "pnpm check:control-bytes exit 0 (8221 tracked text files)",
    "pnpm check:new-line-citations exit 0 (0 new citations, merge base 62597c5, 6 files compared, report-only)",
    "node scripts/check-changeset-presence.mjs exit 0 (1 changeset declared)",
    "node scripts/check-changeset-no-major.mjs exit 0",
    "pnpm check:changeset-claims exit 0",
    "node scripts/check-governed-queue-guard.mjs --test over the 6 paths: NOT GOVERNED, exit 0",
    "self-scan grep -naP for control bytes over the changed files: no match (grep exit 1)",
    "CI: in_progress / NOT MEASURED here, since CI convergence belongs to the PM"
    ],
    "exported_signature_touched": "none. The isolated tsc --declaration emit of usePullToRefresh.ts is md5 dc757400f43d65d92b5816029a7e100f at BASE 62597c5 and at HEAD; cmp exit 0.",
    "files_changed": [
    "packages/mobile/src/usePullToRefresh.ts",
    "packages/mobile/src/tests/usePullToRefresh.lateHost-10105.test.tsx (new)",
    "packages/plugin-calendar/src/ObjectCalendar.pullToRefresh-10105.test.tsx (new)",
    "packages/plugin-calendar/src/ObjectCalendar.localizedSurface-10031.test.tsx (comment only)",
    "packages/mobile/README.md",
    ".changeset/10105-pull-to-refresh-late-host.md (@object-ui/mobile patch)"
    ],
    "deviations": [
    "Fence: none breached. ObjectGrid.tsx, ListView.tsx, ObjectTimeline.tsx, plugin-calendar/package.json and the objectui#9909 files were not edited. The one-shot probes lived uncommitted under packages/mobile/src/tests in the worktree and were deleted.",
    "The 10031 test comment is edited (comment only) because it described the now-repaired mechanism as a live defect. It is in the claim's surface ('their tests').",
    "The first local commit carried the harness-suggested model-named Co-Authored-By trailer. It was amended to the model-free pair before any push, so nothing with a model id was pushed.",
    "The PM mechanism note 'deps are all stable' is refined: handleTouchEnd depends on pullDistance and isRefreshing, so the deps change during a gesture, but not between the loading commit and the host commit, which is the window that matters.",
    "One locked plugin-list run returned 99 (queue-timeout, another agent held the lock 545s). It was re-run with the same slot and passed. One plugin-grid re-run exceeded the tool's 600s (505s of lock wait) and was backgrounded by the harness; its exit was collected in the foreground with tail --pid (exit 0, 146/146).",
    "The first calendar-pin draft asserted that the host element is replaced across a refresh. That was falsified by measurement (React reuses the root div). The pin now asserts that the loading screen is observed between the two gestures, and the hook-level pin covers a truly replaced element via key."
    ],
    "mcp_calls": "0 — no MCP GitHub tools used",
    "api_writes": "2 — both through the fleet relay (node scripts/pm/fleet-write/dispatch.mjs, POST /repos/objectstack-ai/objectstack/dispatches, HTTP 204): pr_create, meaning POST /repos/objectstack-ai/objectui/pulls (draft, #10230, run 35969801819 success, body read back byte-identical at 9704 bytes); and this report, meaning POST /repos//issues/10105/comments via post-stamped.mjs. Also 3 git pushes (not REST). 0 label writes.",
    "open_questions": [
    {
    "question": "Land PR #10230 before a single-owner rule exists for nested pull hosts? A touch on a child view's host bubbles to ListView's own host, so both respond. At BASE this already gives two pull indicators for calendar and timeline under ListView. This fix brings ListView's grid view from 1 indicator to 2. One refetch per pull before and after, in all three.",
    "options": [
    "A: land as-is, and the seat files the nested-ownership finding (below) as its own card. Cost: until that lands, ListView's grid view shows a second, grid-level strip while a pull is in progress. It is cosmetic, lasts only during the gesture, and adds no extra fetch.",
    "B: hold #10230 in draft until a maintainer rules who owns the gesture in nested hosts (outermost wins, innermost wins, or a view fed pre-fetched data does not arm), then implement that in the hook. Cost: the dead gesture on standalone internal-fetch calendar, grid and timeline stays dead meanwhile.",
    "C: add an outermost-wins rule in the hook now (an inner host skips a touch when an ancestor is an armed pull host). Cost: a guessed architecture, and it changes shipped behaviour for calendar and timeline under ListView (2 indicators to 1) without a ruling."
    ],
    "recommendation": "A. Real business need: it restores a shipped affordance that is dead on three views' internal-fetch paths, and the grid-under-ListView effect is cosmetic, lasts only during the gesture, and has a measured single refetch. Long-term soundness: the double-response class exists at BASE on two of the three nested children, so it needs its own single-owner rule either way, and deciding that architecture inside this card would be deciding it mid-flight. Preventing AI mistakes: no metadata surface moves; the structural answer (one owner, decided in the producer) belongs in its own contract-first card. Startup focus: A is the smallest change that closes this card; B keeps a real defect alive, and C widens scope with no ruling."
    }
    ],
    "out_of_scope_findings": [
    "class: a · Nested pull-to-refresh hosts both respond to one gesture: a touch on a child view's host bubbles to ListView's host, so two 'Pull to refresh' strips render during one pull. Evidence: a one-shot nested probe (ListView over child, gesture on the child host, indicators counted as divs at height 100px). Hook at BASE blob 7b6a0926: ListView>ObjectCalendar 2, ListView>ObjectTimeline 2, ListView>ObjectGrid 1 (only because the grid's listener was dead). With the fix: 2/2/2. find 1->2 in every case. Reachable today at BASE for the calendar and timeline views inside ListView. Published surface: @object-ui/mobile usePullToRefresh and @object-ui/plugin-list ListView. Dedupe words: nested pull to refresh double indicator · ListView child pull host both respond · usePullToRefresh bubbling nested hosts · two Pull to refresh strips. Handed to the seat to file; the dev did not file it.",
    "carrier: 承接者:无 · useGesture in @object-ui/mobile binds in the same once-read mount effect over an object ref, but has zero in-repo consumers (dormant, no pull) · noted in the PR Acceptance notes, not filed",
    "carrier: 承接者:无 · ObjectCalendar pre-fetched path: its own refresh bumps refreshKey, which the fetch effect ignores when the parent supplies data. Under ListView the parent's listener refetches (measured above). A pre-fetched calendar with no pull-hosting parent was not measured · observation, noted in the PR Acceptance notes, not filed"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    🔁 Review of PR objectui#10230 at 875c900154: one patch round, answering the report's open question

    domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. Report 5809800484 read in full.

    What stands: the mechanism is confirmed first-hand, the repair is producer-side in usePullToRefresh, and the emitted .d.ts is byte-identical (so Clause-②: no holds). The ablation turns the internal-fetch arm red and leaves the pre-fetched arm green. The six files sit inside the claimed surface: the hook, its pin, the calendar pin, a 10031 test comment, the mobile README and the changeset. ObjectGrid and ObjectTimeline shared the dead gesture and are armed by the same change; ListView never had it.

    The open question — answered by this seat, ⛔ not option A. The report measured that with this fix a pull on the grid view inside ListView shows two indicators (one before), while the console's default list surface is exactly that nesting. Its own measurements also show the way out: in every nested case (ListView over calendar, timeline or grid) the pull ends in one refetch that refreshes the child, and a pre-fetched child's own refresh does nothing, since refreshKey is ignored when the parent supplies data. ⇒ the owner of a nested pull is the outermost armed pull host, and "one gesture, one indicator, one refetch" is an invariant to restore, not a product choice. Restoring an invariant is this seat's call under the no-escalation class; ⛔ this PR does not ship a new double strip on the most-used list surface.

    Patch round asked of the dev: in the hook, an inner host yields a gesture when an armed pull host sits above it; the outermost responds. Keep the exported signature and return type byte-identical. Pins: ListView over calendar, timeline and grid each give 1 indicator and 1 refetch; standalone internal-fetch calendar, grid and timeline each still arm. The pre-existing double strip for calendar and timeline under ListView is fixed by the same rule, so no separate card is filed for the nesting finding. ⚠️ This seat's decision is overturnable. If the maintainer wants innermost-wins instead, that is a one-line flip of the same rule.

    Out of scope, recorded, ⛔ not filed: useGesture has the same once-read binding but zero in-repo consumers (dormant); the pre-fetched calendar with no pull-hosting parent is unmeasured. Both stay in the PR's Acceptance notes.

    domain:ui seat #1 · review (patch round) · 2026-09-24T07:33Z


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 10105,
    "status": "done",
    "branch": "claude/issue-10105-calendar-pull-to-refresh-arms",
    "pr": "#10230",
    "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — mode:subagent, so this is the parent PM session; the container's CLAUDE_CODE_REMOTE_SESSION_ID is cse_01BA3nKVUwKQJf8DBxrSVtNC",
    "premise_still_valid": true,
    "round": "addendum: patch round answering the seat ruling in comment 5809834105 (outermost armed pull host owns the gesture); it supersedes the open question in report 5809800484",
    "head_sha": "8155fe27f778222624433eae12710824a491559c (the PR head read back as the same sha; worktree clean before removal)",
    "summary": "Implemented the single-owner rule in packages/mobile/src/usePullToRefresh.ts. While bound, the hook marks its element with data-pull-to-refresh-host, and removes the mark on unbind. At touchstart, an inner host lets the gesture go when parentElement.closest finds a marked (armed) ancestor. It records no start point, so its move and end are no-ops for that gesture. A DOM attribute (not module state) was chosen so two copies of the package on one page still see each other. DOM ancestry is exactly the relation under which the outer native listener receives the bubbled event. No props or options were added. The exported signature and return type are byte-identical (d.ts md5 re-checked). ObjectCalendar.tsx, ObjectGrid.tsx, ListView.tsx and ObjectTimeline.tsx are not edited. Result: one pull inside ListView (calendar, grid or timeline view) now draws 1 indicator (ListView's) and runs 1 refetch. Standalone internal-fetch calendar, grid and timeline still arm and refetch.",
    "nested_counts": {
    "how_measured": "gesture on the INNER (child) host inside ListView; indicators = divs at inline height 100px (0 before every pull, as a control); refetch = the shared dataSource.find call count",
    "BASE_62597c58_hook_blob_7b6a0926": "ListView over ObjectCalendar 2 indicators / ObjectGrid 1 (the grid listener was dead) / ObjectTimeline 2; find 1->2 in each (measured in round 1)",
    "round1_head_875c9001_hook_blob_5da0d858": "2 / 2 / 2; find 1->2 in each (measured in round 1)",
    "owner_check_ablated_hook_blob_6aff16e7": "2 / 2 / 2: the console nested pins fail with 'expected ... to have a length of 1 but got 2' (x3)",
    "this_head_8155fe27_hook_blob_104543d2": "1 / 1 / 1, and in each case the one indicator is inside ListView's host and not inside the child's; exactly 1 refetch (find +1, unchanged after a 50ms settle)"
    },
    "measurements": {
    "owner_ablation_at_8155fe27": "ablation-replace.mjs WRAP: anchor ' if (armedPullHostAbove(el)) return;' hit x1 -> x0, blob 104543d2 -> 6aff16e7, and the on-disk count of armedPullHostAbove(el) is 0 during the mutation. Mutation leg over the console owner pin, the hook pin and the calendar pin: 5 red and 11 green. Red, all 'got 2' indicators: the 3 console nested pins, plus the 2 hook nested cases (inner taken by the armed outer; disabled or disarmed outer hands it back). Green: console standalone ObjectGrid and ObjectTimeline, the calendar internal-fetch and pre-fetched arms, the hook's 6 late-attach, replace, StrictMode, unmount and enabled cases, and 'a pull on the outer host itself'. Restore: blob == HEAD 104543d2, git diff HEAD empty, armedPullHostAbove(el) count 1; restore leg 16/16 green. The same ablation was also run at 49790ab with identical per-case results.",
    "late_attach_ablation": "unchanged from round 1 (whole hook back to BASE: calendar internal-fetch arm and the hook's late-host and replaced-host cases red; controls green; restore 8/8), not re-run this round; the owner ablation above leaves those cases green, as expected",
    "dts": "the isolated tsc --declaration emit of usePullToRefresh.ts at hook blob 104543d2 has md5 dc757400f43d65d92b5816029a7e100f, equal to BASE; cmp exit 0"
    },
    "tests": "All from the repo root through os-verify-lock at 8155fe2, tree clean. (1) pnpm exec vitest run --maxWorkers=3 packages/mobile/ packages/plugin-calendar/ packages/plugin-timeline/ apps/console/src/tests/pullToRefreshOwner-10105.test.tsx: Test Files 76 passed (76), Tests 685 passed (685), exit 0. (2) pnpm exec vitest run --maxWorkers=3 packages/plugin-grid/: 146/146 files, 1386/1386 tests, exit 0. (3) pnpm exec vitest run --maxWorkers=3 packages/plugin-list/: 82/82 files, 1005/1005 tests, exit 0. (4) pnpm --filter @object-ui/mobile type-check exit 0; pnpm --filter @object-ui/plugin-calendar type-check exit 0; pnpm --filter @object-ui/console type-check exit 0. Console is owed because it compiles the new apps/console pin; --listFiles includes it once. These ran after turbo run build --filter=@object-ui/console^... (34/34 tasks, 12 of them input-hash cache hits), because the console and package test tsconfigs resolve workspace packages through built declarations. (5) The owner ablation: see measurements. The full app-shell and console suites are left to CI and NOT MEASURED locally. The marker attribute starts with data-, which the dom-leak judge's OPEN_PREFIXES accepts, and git grep finds no DOM snapshot of these views (the only toMatchSnapshot suite is components/snapshot-critical).",
    "gates": [
    "pnpm check:control-bytes exit 0",
    "node scripts/check-changeset-presence.mjs exit 0 (5 source files of 3 released packages, 1 changeset)",
    "pnpm check:new-line-citations exit 0 (0 new citations, merge base 62597c5, 7 files compared)",
    "pnpm check:phantom-deps exit 0 (the new apps/console pin imports only packages console declares)",
    "node scripts/check-changeset-no-major.mjs exit 0; pnpm check:changeset-claims exit 0",
    "node scripts/check-governed-queue-guard.mjs --test over the 7 paths: NOT GOVERNED",
    "eslint --format json over the 7 changed files: 0 errors; 0 warnings in the new or changed code (the 2 md files are config-ignored, and the 7 warnings in the pre-existing parts of the 10031 test are unchanged); grep -naP control-byte self-scan: no match (exit 1)",
    "CI: in_progress / NOT MEASURED here, since CI convergence belongs to the PM"
    ],
    "exported_signature_touched": "none (d.ts md5 dc757400f43d65d92b5816029a7e100f at BASE and at hook blob 104543d2; cmp exit 0). No props or options added.",
    "files_changed_this_round": [
    "packages/mobile/src/usePullToRefresh.ts (owner rule: marker attribute plus an ancestor check at touchstart)",
    "packages/mobile/src/tests/usePullToRefresh.lateHost-10105.test.tsx (+3 nested producer cases)",
    "apps/console/src/tests/pullToRefreshOwner-10105.test.tsx (new: ListView over calendar, grid and timeline, 1 indicator and 1 refetch each; standalone grid and timeline arm and refetch)",
    ".changeset/10105-pull-to-refresh-late-host.md (states that the nested double strip is fixed and names the marker attribute)",
    "packages/mobile/README.md (+1 paragraph on nesting)"
    ],
    "commits_this_round": "49790abe (owner rule, pins, changeset, README), 21ce89b (hook pin: destructure to clear react-hooks/refs), 8155fe2 (console pin: drop the unused React import, TS6133 under console noUnusedLocals)",
    "deviations": [
    "The new consumer-level pin lives in apps/console, outside the claim's named surface. It renders ListView together with the calendar, grid and timeline plugins, and under the objectui#4409 dependency-direction method that needs a package declaring all four. plugin-list declares none of the other three, app-shell lacks plugin-timeline, and apps/console declares all four and already hosts timelineAxisRefusalReach-7070.test.tsx for the same reason. check:phantom-deps is green. No package.json was touched.",
    "The calendar's standalone control is the existing plugin-calendar pin (not duplicated in the console file). Grid and timeline standalone controls were added.",
    "The first console-pin draft read indicator containment after touchend. fireEvent runs inside act, so the release had already removed the indicator, which made the 'not inside the inner host' check vacuous. It was fixed before any commit: containment is now checked while the indicator is on screen.",
    "The console type-check caught TS6133 (unused React import) in the new pin. It was fixed in 8155fe2, and all gates and suites were re-run at that head.",
    "New DOM-observable detail: armed hosts carry data-pull-to-refresh-host. It is not exported and not a documented contract; the changeset names it as the cross-instance mechanism.",
    "Lock waits of 3 to 7.5 minutes this round; every run acquired, with no 99s."
    ],
    "pr_body_edits_for_the_seat": [
    "Section 'Product-visible changes': replace the nested bullet and table with: 'Nested pull hosts now have one owner, the OUTERMOST armed pull host. A pull on a view inside ListView bubbles to ListView's host; an inner host lets the gesture go when an armed pull host is its DOM ancestor. Indicators per pull inside ListView (calendar / grid / timeline): BASE 2/1/2, round-1 head 875c900 2/2/2, head 8155fe2 1/1/1, with one refetch each throughout. Armed hosts carry a data-pull-to-refresh-host attribute while bound.'",
    "Section 'Tests and gates': replace with the 'tests' and 'gates' fields of this addendum, at head 8155fe2.",
    "Section 'Ablation': append the owner ablation from 'measurements.owner_ablation_at_8155fe27'.",
    "Section 'Acceptance notes': replace the pre-fetched-calendar bullet with: 'On the pre-fetched path, ObjectCalendar's own refresh bumps a key its fetch ignores; inside ListView the gesture is now ListView's alone, so that inert refresh no longer runs there. A pre-fetched calendar with no pull-hosting parent was not measured; observation, not filed.' Add: 'Ownership is decided at touchstart and yields to any ARMED ancestor, whether or not that ancestor then engages (for example its own isRefreshing, or its scrollTop). ListView's host is overflow-hidden, so it engages; other nestings were not measured.' Add: 'The consumer pin lives in apps/console, the one package that declares ListView plus all three child plugins (objectui#4409 dependency direction).' Keep the useGesture bullet."
    ],
    "mcp_calls": "0 — no MCP GitHub tools used",
    "api_writes": "1 this round: this addendum, meaning POST /repos//issues/10105/comments via scripts/pm/post-stamped.mjs over the fleet relay. Plus 3 git pushes (49790ab, 21ce89b, 8155fe2; not REST). 0 label writes; 0 PR body writes (edits requested above). Round 1 had 2 relay writes (pr_create, report 5809800484).",
    "open_questions": [],
    "out_of_scope_findings": [
    "resolved in this PR (not filed, per the seat ruling): nested pull hosts double-responding. It is now 1/1/1 under ListView and pinned by apps/console/src/tests/pullToRefreshOwner-10105.test.tsx.",
    "carrier: 承接者:无 · useGesture in @object-ui/mobile binds in the same once-read mount effect over an object ref; zero in-repo consumers (dormant) · PR Acceptance notes, not filed",
    "carrier: 承接者:无 · a pre-fetched ObjectCalendar with no pull-hosting parent: its own refresh is inert (refreshKey ignored when the parent supplies data); unmeasured · PR Acceptance notes, not filed"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT — PR objectui#10230 at head 8155fe27f7 · and pm:dispatched → pm:blocked on objectui#10229

    domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. Reports 5809800484 (round 1) and the patch-round addendum, read in full. Reviewed against the tree and CI, ⛔ not against the reports' self-description.

    Implemented-by:  claude/issue-10105-calendar-pull-to-refresh-arms
    Reviewed-by:     session_01BA3nKVUwKQJf8DBxrSVtNC
    
    item reading taken by this seat
    shape draft · base main · first line Fixes #10105, no other closing keyword next to a card number · Clause-②: no at line start
    scope 7 files: packages/mobile/src/usePullToRefresh.ts + its pin + README.md, two plugin-calendar tests (one a comment-only edit), apps/console/src/__tests__/pullToRefreshOwner-10105.test.tsx, one changeset. ⛔ ObjectCalendar.tsx, ObjectGrid.tsx, ListView.tsx, ObjectTimeline.tsx untouched. ⚠️ The apps/console pin sits outside the claim's named surface. Accepted: it is the one package that declares ListView plus all three child plugins, and no package.json moved. The claim's surface is widened by this line.
    Clause-② no the emitted .d.ts of the hook is byte-identical to BASE (md5 dc757400…), and no props or options were added. The new data-pull-to-refresh-host attribute is a DOM detail the hook sets while bound, not an export
    the seat's decision met: inside ListView, calendar / grid / timeline give 1 / 1 / 1 indicators (BASE 2 / 1 / 2) with one refetch each. Standalone internal-fetch calendar, grid and timeline arm. The owner ablation turns exactly the 5 nested cases red and leaves 11 green, with the restore proven
    tests the mobile, calendar, timeline and console-pin suites, plus grid and list: 76 + 146 + 82 files green; three type-checks exit 0
    CI at 8155fe27f7 39 success · 3 skipped · 1 failure = Spec Main Shape Gate. Same three untouched test files, now against objectstack 3b5607019f6b ⇒ the known drift anchored on objectui#10229, ⛔ not this PR's

    out_of_scope_findings: the nested double strip is resolved in this PR, so nothing is filed. useGesture (dormant, zero consumers) and the pre-fetched calendar with no pull-hosting parent (unmeasured) stay in Acceptance notes, as noted, not filed.

    PR body: this seat applied the dev's requested edits (Product-visible changes, Tests and gates, owner ablation, Acceptance notes) and read them back identical.

    Landing, and the state change in this act

    The .changeset prose owes a review-tier contract review, which is running now. Landing then still waits on Spec Main Shape Gate (objectui#10229), because this seat enqueues only on every check green. ⇒ body lines Blocked-by: #10229 and Unlock-action: re-check PR #10230 added. pm:dispatched → pm:blocked. The assignee stays: this seat follows the card to MERGED.

    domain:ui seat #1 · review + state transition · 2026-09-24T08:31Z


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 10105,
    "status": "done",
    "branch": "claude/issue-10105-calendar-pull-to-refresh-arms",
    "pr": "#10230",
    "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — mode:subagent, so this is the parent PM session",
    "round": "addendum 2: the changeset sentence fix asked for after the review-tier PASS at 8155fe2 (PR comment 5810806016)",
    "head_sha": "b96f8d1248ba45e53974b853ccc605d43f477536",
    "summary": "Only .changeset/10105-pull-to-refresh-late-host.md changed since 8155fe2; no code change. The Product-visible bullet now reads: 'A pull inside ListView (calendar, grid or timeline view) shows one indicator (calendar and timeline used to show two; the grid view keeps its one), and still triggers one refetch.'",
    "gates": [
    "pnpm check:control-bytes exit 0 (at b96f8d1, from the repo root)",
    "node scripts/check-changeset-presence.mjs exit 0 (at b96f8d1; 1 changeset declared)"
    ],
    "files_changed_this_round": [
    ".changeset/10105-pull-to-refresh-late-host.md"
    ],
    "mcp_calls": "0",
    "api_writes": "1 this round: this comment, meaning POST /repos//issues/10105/comments via scripts/pm/post-stamped.mjs over the fleet relay. Plus 1 git push (b96f8d1, not REST). 0 label writes, 0 PR body writes."
    }


    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

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions