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.
plugin-list/kanban: switching to the kanban view issues the identical records fetch twice on mount #7394
Measured by the PM in the console's network log on 2026-09-02 (showcase Tasks → All Views page → 看板): the view switch issued GET /api/v1/data/showcase_task?top=100&select=id,title,assignee,status,priority,due_date,cover,start_date,end_date,created_at,progress,location,estimate_hours,done twice back-to-back (request ids 47553.440 and 47553.442) before the first paint; a later PATCH (drag) triggered a single refetch, so the duplicate is a mount-time effect, not the DnD path.
Not re-verified by a dev yet (the objectui#7249 dispatch declined to file unverified sibling claims); filed on the PM's own reading as a finding for triage. A production Vite build was used, so this is not a StrictMode double-effect.
Claim: PM loop — 2026-09-19 shift, batch slot 2/3
Session: session_018HrVaotisyhgmot9o2MLRq
Branch: claude/issue-7394-kanban-double-mount-fetch
Worktree: objectui-issue-7394
Domain: domain:ui
Seat: domain:ui#2
File surface: packages/plugin-kanban/src/, packages/plugin-list/src/ (the view-switch side) — stop on breach and explain in the report
Container & model: M, mode:subagent, model: opus — ⛔ no tier output is quoted because this repo has no scripts/pm/dispatch-gates.mjs; stated rather than fabricated
Clause-②: no
Thread-read: 5548590883
Serial constraints cleared: objectui#7373 (PR objectui#10041, in CI) touches console/**, views/studio-design/**, apps/console/src/components/** — ⛔ no overlap with either package above; objectui#10039 is filed, not dispatched
⭐ NORTH-STAR clause 3, re-taken at 2026-09-19T21:01Z
objectui 0 p0 / 6 p1; objectstack (the product repo) 4 p0 / 43 p1 ⇒ gate CLOSED.
This card is ⛔ not a tool card and ⛔ not a contract-hygiene card — it is a per-view-switch duplicate round trip on a shipped surface. ⇒ outside the gate's prohibition, dispatchable.
⭐⭐ Pre-dispatch re-reading — the shape has DRIFTED, and it partly falsifies triage's leading hypothesis
Re-anchored by symbol on objectui origin/main0febe1a59 (triage read a472b07). ⛔ Do not use triage's line numbers.
triage's reading
today
:264 — $top: schema.limit ?? DEFAULT_KANBAN_LIMIT
⚠️:107 now states «Before objectui#9925 this read was a bare schema.limit ?? DEFAULT_KANBAN_LIMIT» ⇒ that read has since changed
:291 — the fetch effect's dep array, 10 deps
:714, now 12 — it gained schemaKey and perms
(not seen by triage)
:579 — if (!objectDefReady) return;
(not seen by triage)
:710 — «objectDefReady is what re-runs this effect once the definition lands»
⭐ Triage's leading hypothesis was the paired readiness flag (objectDefReady false→true beside objectDef undefined→object, landing in two commits). Two readings above cut against it:
:579 makes the not-ready pass return before fetching, so the pre-ready run should issue no request at all; and
:710 says the re-run on objectDefReady is deliberate, not accidental.
⇒ ⛔ This seat does not declare that hypothesis dead — it declares it no longer the obvious one, and the two lines above are what you must reconcile with the observed two requests. If the effect really can only fetch once per readiness transition, the second request comes from somewhere else, and perms / schemaKey are the two deps that are new since the card was filed.
⭐ Note also that perms is the same dependency this lane measured landing on ObjectGantt's reload in objectui#7237 — where a host minting a fresh array of identical content produced exactly one extra teardown and one extra query. ⛔ That is a hypothesis to test, not a conclusion, and if it is the cause then the fix belongs to that family (objectui#10035 / objectui#10036), not to the kanban.
What the card has already ruled out — ⛔ do not re-litigate these
Both are the card's own exclusionary evidence and they are what make the search space narrow:
A production Vite build was used, so this is not a StrictMode double-effect.
a later PATCH (drag) triggered a single refetch, so the duplicate is a mount-time effect, not the DnD path.
⛔ "It was probably StrictMode" is the one answer this card pre-empted. Closing on it would be closing on the thing the filer already measured away.
Deliverable — reproduce FIRST, then attribute
Reproduce: mount the kanban through a view switch and count requests. objectui#7393 (closed today) is the worked precedent for the instrument: the pre-installed Chromium at /opt/pw-browsers/chromium (⛔ never run playwright install), a console dev server, a probe page mounting the real components, requests counted on more than one independent channel, and an instrument self-test (a hand-issued request that must appear) so a zero is a reading rather than a dark instrument.
Attribute: the two candidate causes live in different packages — the kanban's own effect, or the view switch remounting it from plugin-list. Say which, with evidence.
⛔ Do not "fix" it with an in-flight dedupe. That squashes two requests into one and leaves the effect still running twice, which is the defect wearing a bandage. Fix the cause or report that the cause is outside this card's packages.
⛔ Boundaries
⭐ Fall-back clause: if the cause turns out to be a published surface's behaviour (a hook or an exported component's contract), stop, write the readings on the card, and it returns to the decision box. ⛔ Never re-adjudicate silently.
If you cannot reproduce two requests, that is a result, not a failure — report it with what your instrument could and could not see, and ⛔ do not manufacture a fix. But ⛔ do not report "not reproducible" on an instrument that never mounted the kanban through a view switch; that is objectui#7393's trap, and it cost that card a whole round.
Every zero needs a same-subject must-hit probe in the same command.
⛔ Never edit a shared checkout; your own worktree only. ⛔ Never git stash. ⛔ Never touch content/docs/releases/.
Run the repo's own fast checks on any package you touch, after your last edit — a typecheck taken before the final edit lies the way a stale dist does (measured on objectui#6965 today).
{
"issue": 7394,
"status": "done",
"branch": "claude/issue-7394-kanban-double-mount-fetch",
"pr": "https://github.com/objectstack-ai/objectui/pull/10045",
"session": "session_018HrVaotisyhgmot9o2MLRq",
"premise_still_valid": true,
"summary": "REPRODUCED in a browser and ATTRIBUTED to packages/plugin-list, not to the kanban. A view switch to the kanban issued the identical GET /api/v1/data/showcase_task?top=100&select=... twice; ObjectKanban issued ZERO data requests in every run (its hasExternalData guard returns at the first line under ListView, and the card's URL carries a select= projection ObjectKanban never sends). The cause is ListView's fetch effect naming currentView in its dependency list while the query it builds never reads the visualization - the only way a surface reaches the wire is the $skip of its window, which is 0 for a flat grid and 0 for every other surface at page 1, so the re-issued request was byte-identical to the one on screen. FIX: the effect is keyed on the window (fetchSkip) instead; the two surface-shaped readings the fetch used to latch (the grid pager's server total, and the row-cap banner's 'but the real total is known' half) are derived at render, so no value any consumer sees moved. Not an in-flight dedupe - the effect now runs once rather than twice into a suppressed request. Both hypotheses in the dispatch were tested and are NOT the cause: useSettledSchema holds one {key, def} state so ready and def flip in the same commit and the not-ready pass returns before querying; and a run under a real PermissionProvider measured identical to one with no provider (2 before, 1 after), so this is not the objectui#7237 / #10035 / #10036 perms family. The fall-back clause was considered and does not trigger: ListView's declared contract (rows, pagination props, banners) is preserved; what changed is one fewer HTTP request per switch, which is the card's requested outcome. assignee was already set to os-tesla by the PM; I did not write it.",
"tests": "BROWSER REPRODUCTION (pre-installed Chromium at /opt/pw-browsers/chromium, never `playwright install`; Vite dev server whose middleware serves /api/v1/data/* and /api/v1/metadata/* and writes its own access log; probe page mounts the REAL ListView + REAL ObjectKanban through the REAL ComponentRegistry and clicks the REAL ViewSwitcher tab; NO StrictMode). Four independent counting channels - A Playwright request events, B dev-server access log, C in-page fetch wrapper, D dataSource.find log - plus channel E, a registry wrapper around object-kanban that records the real board's mounts. INSTRUMENT SELF-TEST in the same command, two hand-issued same-subject requests: A +2, B +2, C +2, D +1 (one leg is a raw fetch), every run. Kanban mount evidence every run: mounts 1, unmounts 0, lanes Todo/Doing/Done and cards Alpha/Beta/Gamma on screen. DATA-REQUEST COUNTS PER SESSION, before -> after: mount-on-grid then click Kanban 2 -> 1; same with all seven visualizations whitelisted (dropdown switcher) 2 -> 1; same under a real PermissionProvider 2 -> 1; mount straight onto the kanban, no click 1 -> 1; host hands viewType down late, no click 3 -> 2 (the residual is the host-identity class, see out_of_scope_findings). PIN: packages/plugin-list/src/__tests__/ListView.viewSwitchRefetch-7394.test.tsx, 5 cases. REVERSE VERIFICATION through scripts/ablation-replace.mjs so the mutation is proven on disk, direction predicted before running and then observed - anchor 'perms, fetchSkip, groupingConfig, ganttOwnsData]);' x1 -> x0, replacement with currentView restored x0 -> x1, blob 12b1097f1431 -> a928b7a96d96; result 2 failed | 3 passed - RED: S1 (expected 1 times, but got 2 times) and S2; GREEN in both worlds: S3 (from page 3 the same switch STILL refetches because the window really moves), S4 (a real query change still refetches), S5 (the row-cap banner keeps the gate the fetch used to apply). Restore proven: blob after restore == blob at HEAD (12b1097f1431) and `git diff HEAD` empty. CHECKS, all taken AFTER the last edit and both heavy runs through scripts/pm/os-verify-lock.sh (slot os-dev-7394-plugin-list): `pnpm exec vitest run packages/plugin-list/` 82 files / 1005 tests passed, VERDICT command-exit 0; `pnpm --filter @object-ui/plugin-list type-check` exit 0 (tsc --noEmit and tsc -p tsconfig.test.json), VERDICT command-exit 0; `pnpm --filter @object-ui/plugin-list lint` exit 0, 0 errors / 541 warnings all pre-existing, and ListView.tsx alone reports 175 warnings on this branch and 175 on origin/main so this branch adds none; `node scripts/check-changeset-presence.mjs` exit 0, 1 changeset declared; control-character scan `grep -naP` over every edited file, no hits. PREREQUISITE NOT MET, then met: the first type-check run was exit 2 with 'Cannot find module @object-ui/components' - a fresh worktree has no dist/*.d.ts, so `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build` ran first (VERDICT command-exit 0) and the re-run was green; that first red is NOT recorded as a failed measurement. CONSUMER SPOT-CHECK: the three app-shell StudioDesignSurface tests that assert find-call counts, 3 files / 7 tests passed - the public export surface of @object-ui/plugin-list is byte-unchanged, so no import-side tests are otherwise owed and the repo-wide farm is CI's run. NOT MEASURED: repo-wide `pnpm lint` and `pnpm test` (CI owns them); CI convergence on PR 10045 (reported at in_progress by contract, not waited on).",
"mcp_calls": "0 - no MCP GitHub tool was called, read or write; every GitHub read and write went through the REST proxy with curl",
"api_writes": "2 - POST /repos/objectstack-ai/objectui/pulls (draft PR 10045); POST /repos/objectstack-ai/objectui/issues/7394/comments (this report). Labels: NOT written by me - the repo's own labeler had already stamped plugin and tests on PR 10045 by the time I read it back, which a GET confirmed; no label endpoint was called. Two `git push` calls are git, not API writes. PR body written once, on the create call, and read back byte-for-byte: the only difference from what was sent is the platform's additive session-URL footer, +90 bytes, nothing eaten.",
"open_questions": [],
"out_of_scope_findings": [
"to file (class (a), reproducible defect; dedupe words: ListView schema identity refetch, fresh schema object per render, by-identity dependency producer stabilisation, objectui#4567, host mints new columns array): a HOST that answers a view switch by minting a FRESH schema object hands ListView new identities for schema.columns, schema.kanban and schema.options, which re-issues the identical query on its own - measured with the same four-channel instrument at one extra request in both the host-rerender and the late-viewType shapes, AFTER this card's fix. This is the LAST remaining duplicate on the card's path and it lives outside both of this card's packages (the host, e.g. app-shell ObjectView). objectui#4567 already ruled that ListView's by-identity dependency is correct for a real column change and that the stabilisation belongs at the PRODUCER, so the dedupe seat may well fold this into that ruling rather than open a new card - I do not dedupe, I hand it over with the words.",
"noted, not filed: packages/plugin-kanban renders no data-testid and no data-slot anywhere in its board DOM, so a browser probe cannot anchor on the board and has to wrap the registry entry to prove it mounted (which is what channel E does here). Not one of the three classes - a testability observation, not a defect, a contract breach or a metadata trap. Successor: the next card that needs browser or e2e evidence about the kanban board, which will hit the same wall.",
"noted, not filed: before this change there was a transient in which, right after a grid-to-kanban switch and before the refetch landed, serverTotal still held the grid's paged total, so the kanban's record-count bar briefly showed the grid's total instead of its own row count. Deriving serverTotal closes that window as a side effect of this fix, so there is nothing left to file. Successor: none."
]
}
{
"issue": 7394,
"status": "done",
"round": "post-review merge round — supersedes the readings in the earlier os-dev-report comment on this card; the reproduction, attribution and reverse-verification readings there are unchanged and not repeated",
"branch": "claude/issue-7394-kanban-double-mount-fetch",
"pr": "https://github.com/objectstack-ai/objectui/pull/10045",
"session": "session_018HrVaotisyhgmot9o2MLRq",
"premise_still_valid": true,
"summary": "Merged origin/main in (a MERGE commit, 1337373b8, never a rebase) and pushed; PR head is now c9ff691d2. Ancestry, both legs on a shallow checkout: before the merge `git merge-base --is-ancestor 3fa3b3eb6 HEAD` exited 1 with the control leg `ce986aafc` (the branch point, at least as deep as the subject) exiting 0, so the negative reading holds; after the merge `origin/main` exits 0. The merge was clean, no conflicts, and the PR diff is still exactly 3 paths - objectui#10041's 16 files are in the history but not in the diff against the new base. GitHub now reports mergeable true, mergeable_state blocked (the draft state, no longer behind). Also added the sentence the seat asked for, at the site rather than by PATCHing the PR body: the dep list lost TWO names, `serverPage` with `currentView`, and that is the same removal because `fetchSkip` is defined as the exact expression the effect used to compute inline (`paginate ? (serverPage - 1) * effectivePageSize : 0`), so the page, the page size and whether the surface pages at all are folded into the one number the query carries. `serverPage` is untouched elsewhere and the pager still reads it. ⚠️ The tighter consequence the seat pointed at - a `serverPage` change under `paginate === false` moves nothing and no longer re-runs the effect - is written as a claim about the DEFINITION and labelled as one, because nothing in the suite re-derives it (AGENTS.md #9). I did not add a pin for it: the seat said not to restructure to make the point, and a new case would have changed the advertised RED/GREEN split of the pin. If the seat wants it live, the case is `§3` extended with a hard equality after the page-reset commit flushes, and it would be RED in the old world, moving §3 out of the controls.",
"tests": "ALL TAKEN AFTER THE MERGE AND AFTER THE LAST EDIT, both heavy runs through scripts/pm/os-verify-lock.sh (slot os-dev-7394-plugin-list). PREREQUISITE first: `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build` - VERDICT command-exit 0 (the recreated worktree had no dist/*.d.ts, and the merge moved app-shell, so the closure was rebuilt before anything read a .d.ts). READING 1 - `pnpm exec vitest run packages/plugin-list/`: 82 files / 1005 tests passed. READING 2 - `pnpm --filter @object-ui/plugin-list type-check`: exit 0 (tsc --noEmit and tsc -p tsconfig.test.json). Both in one locked invocation, VERDICT command-exit 0. ALSO: `pnpm --filter @object-ui/plugin-list lint` exit 0, 0 errors / 541 warnings, the same count as before the merge; `node scripts/check-changeset-presence.mjs` exit 0 against the new merge-base 3fa3b3eb6 - 3 files changed, 2 published source, 1 changeset declared; control-character scan `grep -naP` over ListView.tsx, no hits. NOT MEASURED: repo-wide lint and test (CI owns them); CI convergence on PR 10045. NOT RE-RUN: the browser reproduction and the ablation - this round's only source change is a comment, and the dependency array is byte-identical to the one both were taken against.",
"mcp_calls": "0 - still zero; every GitHub read and write this round went through the REST proxy with curl",
"api_writes": "1 this round - POST /repos/objectstack-ai/objectui/issues/7394/comments (this comment). No PATCH of the PR body, by contract: the seat's sentence went into the source at the site instead. Labels untouched; a GET confirms PR 10045 still carries plugin and tests from the repo's own labeler. One `git push` (41b364a4f..c9ff691d2) is git, not an API write, and a GET confirms the PR picked it up as its head.",
"open_questions": [],
"out_of_scope_findings": [
"unchanged from the earlier report on this card; the host-identity finding is being filed by the seat"
]
}
Ruling on the open question: A — the consequence stays prose at the site, ⛔ no new pin
domain:ui#2 execution seat (session session_018HrVaotisyhgmot9o2MLRq), read at 2026-09-19T21:45Z.
Base merge accepted: merge commit 1337373b8, head c9ff691d2, mergeable: true, final file list still 3 paths, check-governed-merges.mjs re-run on that list ⇒ NOT governed.
Why A, and the deciding reason is NOT the one the dev cited
The dev recommended A partly because this seat said "do not restructure to make the point". ⛔ That instruction is the weaker half of the argument, and it is not what decides it. The deciding reason is the dev's own second one:
B … turns S3 from a both-worlds control into a defect assertion and changes the pin's advertised RED/GREEN split.
⭐ S3 — from page 3 the same switch still refetches because the window really moves — is GREEN in both worlds on purpose. That is what makes it a control: it is the case that would go red if the fix ever over-applied and started suppressing a refetch that a real window change earned. Converting it into a defect assertion would spend the control to buy an assertion. ⛔ That is a bad trade in the direction this lane has been burned in repeatedly today: a pin that stops failing at the same time it stops checking.
⇒ Keep S1/S2 RED-in-the-old-world and S3/S4/S5 GREEN-in-both. ⛔ Do not extend S3.
And the claim is honestly labelled, which is the point
The serverPage-under-paginate === false consequence is a derived one: it follows from fetchSkip's definition, and ⛔ nothing in the suite re-derives it. The dev wrote it at the site and labelled it as not re-derived, which is exactly what AGENTS.md commandment #9 asks for:
when you declare something verified, point at the instrument that re-derives it … Where nothing re-derives the claim, say that in the text rather than let it read as live.
⇒ A labelled unverified claim beside the definition it follows from is the honest shape. An unlabelled one would be the defect; a pin bought by spending a control is a different defect.
The sentence this seat asked for went where it belongs
⭐ Into the source at the site, ⛔ not into the PR body. The dev holds itself to writing that body once, and this seat did not overrule that — the reader who diffs the dep array and sees two names vanish now finds the reason next to fetchSkip's definition, which is where they will be looking.
⭐ One operational change this seat made on the strength of today's reading
This PR was flipped ready early, while eleven jobs were still running — deliberately the opposite of what this seat did on PR objectui#10041.
On that PR the flip came at the tail of a nearly-complete run, and a ready_for_review re-trigger with a cancel-in-progress concurrency group cancelled Test (shard 1/4) after it had run for twenty minutes, costing a re-run cycle. ⚠️ Stated as the likely mechanism and ⛔ not a measured one — this seat has not read the workflow's concurrency configuration. But the cost is real either way, and it is paid by when the flip happens, not by whether it happens. ⇒ Flip early, let the re-triggered run be the one that is read.
Landing when every job reads completed: success — ⛔ never the aggregate.
✅ Landed, content-verified on the merged ref — closing
domain:ui#2 execution seat (session session_018HrVaotisyhgmot9o2MLRq), read at 2026-09-19T22:25Z.
PR objectui#10045 merged at 2026-09-19T22:24:04Z as 237e5b8d2.
Landing is healthy
merged and removed_from_merge_queue both at 2026-09-19T22:24:04Z — the same second, so ⛔ no ejection signature. 237e5b8d2 has exactly one parent (3fa3b3eb6) and the title carries the (#10045) suffix ⇒ squash, confirmed by the artefact.
Content verification, base 3fa3b3eb6 → merged 237e5b8d2
⚠️ The first row's base cell was blank on this seat's first pass: the pattern allowed one name between perms and groupingConfig and the base has two. ⛔ Not a reading — the head matched, so the instrument was live; the base was re-read verbatim and is quoted above.
⭐ The attribution is structural, not asserted
packages/plugin-kanban is untouched by the landing, and the dev measured ObjectKanban issuing zero data requests in every run — its hasExternalData guard returns at the first line under ListView, and the card's URL carries a select= projection ObjectKanban never sends. ⇒ The doubled request could not have come from the board, and the fix is in the host.
The cause, in one line: ListView's fetch effect named currentView while the query it builds never reads the visualization — the only way a surface reaches the wire is the $skip of its window, which is 0 for a flat grid and 0 for every surface at page 1, so the re-issued request was byte-identical to the one already on screen.
⭐ And the fix is tighter than the card asked for: serverPage left the dep list beside currentView, because fetchSkip is defined as exactly the expression the effect used to compute inline (paginate ? (serverPage - 1) * effectivePageSize : 0). So the page, the page size and whether the surface pages at all fold into the one number the query carries — and a page change under paginate === false no longer re-runs the effect either. serverPage is untouched elsewhere and the pager still reads it.
⭐ Both of this seat's dispatch hypotheses were tested and are NOT the cause
Recorded because a dispatch that hands over hypotheses owes the reader their fate:
the paired readiness flag — useSettledSchema holds one {key, def} state, so ready and def flip in the same commit and the not-ready pass returns before querying;
⇒ Two plausible causes eliminated by measurement rather than by argument.
The pin's shape, and the ruling on the open question
S1/S2 are RED in the old world; S3/S4/S5 are GREEN in both. ⭐ S3 — from page 3 the same switch still refetches because the window really moves — is the control that would fire if the fix ever over-applied. The open question asked whether to extend it into a defect assertion for the paginate === false consequence; this seat ruled A: keep it prose at the site, labelled as not re-derived per AGENTS.md commandment #9. ⛔ Spending a control to buy an assertion is the trade this lane has been burned by repeatedly — a pin that stops failing at the same time it stops checking.
⭐ One operational change this landing confirmed
This PR was flipped ready early, while eleven jobs were still running — the opposite of PR objectui#10041, where a late flip cost a twenty-minute shard to a ready_for_review re-trigger. The result: 37 checks, zero cancelled, and no re-run needed. ⚠️ The mechanism is still stated as likely and ⛔ not measured — this seat has not read the workflow's concurrency configuration — but the cost difference is real and is paid by when the flip happens.
Carried out of the thread
objectui#10046 — the dev measured, after this fix, that a host minting a fresh schema object on a view switch re-issues the identical query on its own. That is the last remaining duplicate on this card's path and it lives outside both of this card's packages. ⛔ Not "stop depending on identity in ListView": that file records objectui#4567's ruling that the by-identity dependency is correct for a real column change and puts the stabilisation at the producer. Filed with the dev's dedupe words; ⛔ the filing seat does not de-duplicate.
Closing completed; pm:dispatched and the assignee come off in the same stroke. domain:ui and priority:p3 stay — ownership is not state.
Activity
os-zhuang commentedon Sep 5, 2026
分诊 —
domain:ui/priority:p3/pm:queue/finding锚定 (anchoring):切到看板视图时的挂载取数,落在
packages/plugin-list(视图切换)与packages/plugin-kanban(看板自身取数)之间 ⇒ 两者都是domain:ui,一个 lane。复核边界 —— 本席能验的与不能验的
⛔ 这张卡是运行时网络观察,本席无法在静态代码上复现。能做的是确认那个请求形状在代码里成立:
$top=100与DEFAULT_KANBAN_LIMIT = 100(ObjectKanban.tsx:71)对得上,所以卡片记录的那条 URL 出自这个取数点。⭐⚠️ 这类"就绪标志与它守护的值分别列在依赖里"的形状,是挂载期双次触发最常见的成因之一:
:291的依赖数组有 10 个依赖,其中objectDefReady与objectDef是成对的(一个布尔就绪标志 + 它所描述的对象)。objectDefReady从false→true与objectDef从undefined→{...}若不在同一次提交里落地,effect 就会跑两次。⛔ 本席不宣称这就是成因 —— 这是给接手者的第一个可检查假设,不是结论。⭐ 卡片自己排除了最常见的假成因,这一条要保留
以及:
两条都是排除性证据,把搜索空间收得很窄:不是 StrictMode、不是拖拽路径、就是挂载。⛔ 接手者不要重新怀疑这两条(尤其不要说"大概是 StrictMode"就关掉)—— 那正是卡片预先堵住的。
定级理由
priority:p3:不上 p2:不影响任何显示结果;也不像 #7392 那样是稳态轮询(那个每 10s 一次,这个每次切视图一次)。
卡片诚实标注了这一点,并说明了原因(#7249 的调度拒绝提交未经验证的兄弟主张)。这是正确的做法 —— 与其把未验证的观察塞进另一张卡的 PR,不如单独立卡等复核。⛔ 但也意味着接手第一步是复现,不是修。
定型:Bug(效率),不踩 manual floor
⛔ 给执行席的边界
GET /api/v1/data/showcase_task?top=100&select=…。ObjectKanban.tsx:291的依赖数组(见上的成对就绪标志假设),⛔ 但也要查视图切换那一侧 —— 卡片说的是"切到看板视图时",所以发起方可能是plugin-list的视图切换重挂载,而不是看板自己的 effect 跑了两次。这两种成因的修法在不同的包里。find",⛔ 不要断言请求 URL 的完整字符串(select列表会随对象定义变化)。⛔ 分诊席不认领、不派单、不写码。
Generated by Claude Code
os-tesla commentedon Sep 19, 2026
Claim: PM loop — 2026-09-19 shift, batch slot 2/3
Session:
session_018HrVaotisyhgmot9o2MLRqBranch:
claude/issue-7394-kanban-double-mount-fetchWorktree:
objectui-issue-7394Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/plugin-kanban/src/,packages/plugin-list/src/(the view-switch side) — stop on breach and explain in the reportContainer & model: M,
mode:subagent,model: opus— ⛔ no tier output is quoted because this repo has noscripts/pm/dispatch-gates.mjs; stated rather than fabricatedClause-②: no
Thread-read: 5548590883
Serial constraints cleared: objectui#7373 (PR objectui#10041, in CI) touches
console/**,views/studio-design/**,apps/console/src/components/**— ⛔ no overlap with either package above; objectui#10039 is filed, not dispatched⭐ NORTH-STAR clause 3, re-taken at 2026-09-19T21:01Z
objectui 0 p0 / 6 p1; objectstack (the product repo) 4 p0 / 43 p1 ⇒ gate CLOSED.
This card is ⛔ not a tool card and ⛔ not a contract-hygiene card — it is a per-view-switch duplicate round trip on a shipped surface. ⇒ outside the gate's prohibition, dispatchable.
⭐⭐ Pre-dispatch re-reading — the shape has DRIFTED, and it partly falsifies triage's leading hypothesis
Re-anchored by symbol on objectui
origin/main0febe1a59(triage reada472b07). ⛔ Do not use triage's line numbers.:264—$top: schema.limit ?? DEFAULT_KANBAN_LIMIT:107now states «Before objectui#9925 this read was a bareschema.limit ?? DEFAULT_KANBAN_LIMIT» ⇒ that read has since changed:291— the fetch effect's dep array, 10 deps:714, now 12 — it gainedschemaKeyandperms:579—if (!objectDefReady) return;:710— «objectDefReadyis what re-runs this effect once the definition lands»⭐ Triage's leading hypothesis was the paired readiness flag (
objectDefReadyfalse→true besideobjectDefundefined→object, landing in two commits). Two readings above cut against it::579makes the not-ready pass return before fetching, so the pre-ready run should issue no request at all; and:710says the re-run onobjectDefReadyis deliberate, not accidental.⇒ ⛔ This seat does not declare that hypothesis dead — it declares it no longer the obvious one, and the two lines above are what you must reconcile with the observed two requests. If the effect really can only fetch once per readiness transition, the second request comes from somewhere else, and
perms/schemaKeyare the two deps that are new since the card was filed.⭐ Note also that
permsis the same dependency this lane measured landing onObjectGantt'sreloadin objectui#7237 — where a host minting a fresh array of identical content produced exactly one extra teardown and one extra query. ⛔ That is a hypothesis to test, not a conclusion, and if it is the cause then the fix belongs to that family (objectui#10035 / objectui#10036), not to the kanban.What the card has already ruled out — ⛔ do not re-litigate these
Both are the card's own exclusionary evidence and they are what make the search space narrow:
⛔ "It was probably StrictMode" is the one answer this card pre-empted. Closing on it would be closing on the thing the filer already measured away.
Deliverable — reproduce FIRST, then attribute
/opt/pw-browsers/chromium(⛔ never runplaywright install), a console dev server, a probe page mounting the real components, requests counted on more than one independent channel, and an instrument self-test (a hand-issued request that must appear) so a zero is a reading rather than a dark instrument.plugin-list. Say which, with evidence.⛔ Boundaries
git stash. ⛔ Never touchcontent/docs/releases/.Generated by Claude Code
test(plugin-list): make the #7394 controls independent of the defect …
fix(plugin-list): keep the exhaustive-deps directive adjacent to the …
os-tesla commentedon Sep 19, 2026
os-dev-report
{ "issue": 7394, "status": "done", "branch": "claude/issue-7394-kanban-double-mount-fetch", "pr": "https://github.com/objectstack-ai/objectui/pull/10045", "session": "session_018HrVaotisyhgmot9o2MLRq", "premise_still_valid": true, "summary": "REPRODUCED in a browser and ATTRIBUTED to packages/plugin-list, not to the kanban. A view switch to the kanban issued the identical GET /api/v1/data/showcase_task?top=100&select=... twice; ObjectKanban issued ZERO data requests in every run (its hasExternalData guard returns at the first line under ListView, and the card's URL carries a select= projection ObjectKanban never sends). The cause is ListView's fetch effect naming currentView in its dependency list while the query it builds never reads the visualization - the only way a surface reaches the wire is the $skip of its window, which is 0 for a flat grid and 0 for every other surface at page 1, so the re-issued request was byte-identical to the one on screen. FIX: the effect is keyed on the window (fetchSkip) instead; the two surface-shaped readings the fetch used to latch (the grid pager's server total, and the row-cap banner's 'but the real total is known' half) are derived at render, so no value any consumer sees moved. Not an in-flight dedupe - the effect now runs once rather than twice into a suppressed request. Both hypotheses in the dispatch were tested and are NOT the cause: useSettledSchema holds one {key, def} state so ready and def flip in the same commit and the not-ready pass returns before querying; and a run under a real PermissionProvider measured identical to one with no provider (2 before, 1 after), so this is not the objectui#7237 / #10035 / #10036 perms family. The fall-back clause was considered and does not trigger: ListView's declared contract (rows, pagination props, banners) is preserved; what changed is one fewer HTTP request per switch, which is the card's requested outcome. assignee was already set to os-tesla by the PM; I did not write it.", "tests": "BROWSER REPRODUCTION (pre-installed Chromium at /opt/pw-browsers/chromium, never `playwright install`; Vite dev server whose middleware serves /api/v1/data/* and /api/v1/metadata/* and writes its own access log; probe page mounts the REAL ListView + REAL ObjectKanban through the REAL ComponentRegistry and clicks the REAL ViewSwitcher tab; NO StrictMode). Four independent counting channels - A Playwright request events, B dev-server access log, C in-page fetch wrapper, D dataSource.find log - plus channel E, a registry wrapper around object-kanban that records the real board's mounts. INSTRUMENT SELF-TEST in the same command, two hand-issued same-subject requests: A +2, B +2, C +2, D +1 (one leg is a raw fetch), every run. Kanban mount evidence every run: mounts 1, unmounts 0, lanes Todo/Doing/Done and cards Alpha/Beta/Gamma on screen. DATA-REQUEST COUNTS PER SESSION, before -> after: mount-on-grid then click Kanban 2 -> 1; same with all seven visualizations whitelisted (dropdown switcher) 2 -> 1; same under a real PermissionProvider 2 -> 1; mount straight onto the kanban, no click 1 -> 1; host hands viewType down late, no click 3 -> 2 (the residual is the host-identity class, see out_of_scope_findings). PIN: packages/plugin-list/src/__tests__/ListView.viewSwitchRefetch-7394.test.tsx, 5 cases. REVERSE VERIFICATION through scripts/ablation-replace.mjs so the mutation is proven on disk, direction predicted before running and then observed - anchor 'perms, fetchSkip, groupingConfig, ganttOwnsData]);' x1 -> x0, replacement with currentView restored x0 -> x1, blob 12b1097f1431 -> a928b7a96d96; result 2 failed | 3 passed - RED: S1 (expected 1 times, but got 2 times) and S2; GREEN in both worlds: S3 (from page 3 the same switch STILL refetches because the window really moves), S4 (a real query change still refetches), S5 (the row-cap banner keeps the gate the fetch used to apply). Restore proven: blob after restore == blob at HEAD (12b1097f1431) and `git diff HEAD` empty. CHECKS, all taken AFTER the last edit and both heavy runs through scripts/pm/os-verify-lock.sh (slot os-dev-7394-plugin-list): `pnpm exec vitest run packages/plugin-list/` 82 files / 1005 tests passed, VERDICT command-exit 0; `pnpm --filter @object-ui/plugin-list type-check` exit 0 (tsc --noEmit and tsc -p tsconfig.test.json), VERDICT command-exit 0; `pnpm --filter @object-ui/plugin-list lint` exit 0, 0 errors / 541 warnings all pre-existing, and ListView.tsx alone reports 175 warnings on this branch and 175 on origin/main so this branch adds none; `node scripts/check-changeset-presence.mjs` exit 0, 1 changeset declared; control-character scan `grep -naP` over every edited file, no hits. PREREQUISITE NOT MET, then met: the first type-check run was exit 2 with 'Cannot find module @object-ui/components' - a fresh worktree has no dist/*.d.ts, so `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build` ran first (VERDICT command-exit 0) and the re-run was green; that first red is NOT recorded as a failed measurement. CONSUMER SPOT-CHECK: the three app-shell StudioDesignSurface tests that assert find-call counts, 3 files / 7 tests passed - the public export surface of @object-ui/plugin-list is byte-unchanged, so no import-side tests are otherwise owed and the repo-wide farm is CI's run. NOT MEASURED: repo-wide `pnpm lint` and `pnpm test` (CI owns them); CI convergence on PR 10045 (reported at in_progress by contract, not waited on).", "mcp_calls": "0 - no MCP GitHub tool was called, read or write; every GitHub read and write went through the REST proxy with curl", "api_writes": "2 - POST /repos/objectstack-ai/objectui/pulls (draft PR 10045); POST /repos/objectstack-ai/objectui/issues/7394/comments (this report). Labels: NOT written by me - the repo's own labeler had already stamped plugin and tests on PR 10045 by the time I read it back, which a GET confirmed; no label endpoint was called. Two `git push` calls are git, not API writes. PR body written once, on the create call, and read back byte-for-byte: the only difference from what was sent is the platform's additive session-URL footer, +90 bytes, nothing eaten.", "open_questions": [], "out_of_scope_findings": [ "to file (class (a), reproducible defect; dedupe words: ListView schema identity refetch, fresh schema object per render, by-identity dependency producer stabilisation, objectui#4567, host mints new columns array): a HOST that answers a view switch by minting a FRESH schema object hands ListView new identities for schema.columns, schema.kanban and schema.options, which re-issues the identical query on its own - measured with the same four-channel instrument at one extra request in both the host-rerender and the late-viewType shapes, AFTER this card's fix. This is the LAST remaining duplicate on the card's path and it lives outside both of this card's packages (the host, e.g. app-shell ObjectView). objectui#4567 already ruled that ListView's by-identity dependency is correct for a real column change and that the stabilisation belongs at the PRODUCER, so the dedupe seat may well fold this into that ruling rather than open a new card - I do not dedupe, I hand it over with the words.", "noted, not filed: packages/plugin-kanban renders no data-testid and no data-slot anywhere in its board DOM, so a browser probe cannot anchor on the board and has to wrap the registry entry to prove it mounted (which is what channel E does here). Not one of the three classes - a testability observation, not a defect, a contract breach or a metadata trap. Successor: the next card that needs browser or e2e evidence about the kanban board, which will hit the same wall.", "noted, not filed: before this change there was a transient in which, right after a grid-to-kanban switch and before the refetch landed, serverTotal still held the grid's paged total, so the kanban's record-count bar briefly showed the grid's total instead of its own row count. Deriving serverTotal closes that window as a side effect of this fix, so there is nothing left to file. Successor: none." ] }Generated by Claude Code
os-tesla commentedon Sep 19, 2026
os-dev-report
{ "issue": 7394, "status": "done", "round": "post-review merge round — supersedes the readings in the earlier os-dev-report comment on this card; the reproduction, attribution and reverse-verification readings there are unchanged and not repeated", "branch": "claude/issue-7394-kanban-double-mount-fetch", "pr": "https://github.com/objectstack-ai/objectui/pull/10045", "session": "session_018HrVaotisyhgmot9o2MLRq", "premise_still_valid": true, "summary": "Merged origin/main in (a MERGE commit, 1337373b8, never a rebase) and pushed; PR head is now c9ff691d2. Ancestry, both legs on a shallow checkout: before the merge `git merge-base --is-ancestor 3fa3b3eb6 HEAD` exited 1 with the control leg `ce986aafc` (the branch point, at least as deep as the subject) exiting 0, so the negative reading holds; after the merge `origin/main` exits 0. The merge was clean, no conflicts, and the PR diff is still exactly 3 paths - objectui#10041's 16 files are in the history but not in the diff against the new base. GitHub now reports mergeable true, mergeable_state blocked (the draft state, no longer behind). Also added the sentence the seat asked for, at the site rather than by PATCHing the PR body: the dep list lost TWO names, `serverPage` with `currentView`, and that is the same removal because `fetchSkip` is defined as the exact expression the effect used to compute inline (`paginate ? (serverPage - 1) * effectivePageSize : 0`), so the page, the page size and whether the surface pages at all are folded into the one number the query carries. `serverPage` is untouched elsewhere and the pager still reads it. ⚠️ The tighter consequence the seat pointed at - a `serverPage` change under `paginate === false` moves nothing and no longer re-runs the effect - is written as a claim about the DEFINITION and labelled as one, because nothing in the suite re-derives it (AGENTS.md #9). I did not add a pin for it: the seat said not to restructure to make the point, and a new case would have changed the advertised RED/GREEN split of the pin. If the seat wants it live, the case is `§3` extended with a hard equality after the page-reset commit flushes, and it would be RED in the old world, moving §3 out of the controls.", "tests": "ALL TAKEN AFTER THE MERGE AND AFTER THE LAST EDIT, both heavy runs through scripts/pm/os-verify-lock.sh (slot os-dev-7394-plugin-list). PREREQUISITE first: `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build` - VERDICT command-exit 0 (the recreated worktree had no dist/*.d.ts, and the merge moved app-shell, so the closure was rebuilt before anything read a .d.ts). READING 1 - `pnpm exec vitest run packages/plugin-list/`: 82 files / 1005 tests passed. READING 2 - `pnpm --filter @object-ui/plugin-list type-check`: exit 0 (tsc --noEmit and tsc -p tsconfig.test.json). Both in one locked invocation, VERDICT command-exit 0. ALSO: `pnpm --filter @object-ui/plugin-list lint` exit 0, 0 errors / 541 warnings, the same count as before the merge; `node scripts/check-changeset-presence.mjs` exit 0 against the new merge-base 3fa3b3eb6 - 3 files changed, 2 published source, 1 changeset declared; control-character scan `grep -naP` over ListView.tsx, no hits. NOT MEASURED: repo-wide lint and test (CI owns them); CI convergence on PR 10045. NOT RE-RUN: the browser reproduction and the ablation - this round's only source change is a comment, and the dependency array is byte-identical to the one both were taken against.", "mcp_calls": "0 - still zero; every GitHub read and write this round went through the REST proxy with curl", "api_writes": "1 this round - POST /repos/objectstack-ai/objectui/issues/7394/comments (this comment). No PATCH of the PR body, by contract: the seat's sentence went into the source at the site instead. Labels untouched; a GET confirms PR 10045 still carries plugin and tests from the repo's own labeler. One `git push` (41b364a4f..c9ff691d2) is git, not an API write, and a GET confirms the PR picked it up as its head.", "open_questions": [], "out_of_scope_findings": [ "unchanged from the earlier report on this card; the host-identity finding is being filed by the seat" ] }Generated by Claude Code
os-tesla commentedon Sep 19, 2026
Ruling on the open question: A — the consequence stays prose at the site, ⛔ no new pin
domain:ui#2execution seat (sessionsession_018HrVaotisyhgmot9o2MLRq), read at 2026-09-19T21:45Z.Base merge accepted: merge commit
1337373b8, headc9ff691d2,mergeable: true, final file list still 3 paths,check-governed-merges.mjsre-run on that list ⇒ NOT governed.Why A, and the deciding reason is NOT the one the dev cited
The dev recommended A partly because this seat said "do not restructure to make the point". ⛔ That instruction is the weaker half of the argument, and it is not what decides it. The deciding reason is the dev's own second one:
⭐ S3 — from page 3 the same switch still refetches because the window really moves — is GREEN in both worlds on purpose. That is what makes it a control: it is the case that would go red if the fix ever over-applied and started suppressing a refetch that a real window change earned. Converting it into a defect assertion would spend the control to buy an assertion. ⛔ That is a bad trade in the direction this lane has been burned in repeatedly today: a pin that stops failing at the same time it stops checking.
⇒ Keep S1/S2 RED-in-the-old-world and S3/S4/S5 GREEN-in-both. ⛔ Do not extend S3.
And the claim is honestly labelled, which is the point
The
serverPage-under-paginate === falseconsequence is a derived one: it follows fromfetchSkip's definition, and ⛔ nothing in the suite re-derives it. The dev wrote it at the site and labelled it as not re-derived, which is exactly what AGENTS.md commandment #9 asks for:⇒ A labelled unverified claim beside the definition it follows from is the honest shape. An unlabelled one would be the defect; a pin bought by spending a control is a different defect.
The sentence this seat asked for went where it belongs
⭐ Into the source at the site, ⛔ not into the PR body. The dev holds itself to writing that body once, and this seat did not overrule that — the reader who diffs the dep array and sees two names vanish now finds the reason next to
fetchSkip's definition, which is where they will be looking.⭐ One operational change this seat made on the strength of today's reading
This PR was flipped ready early, while eleven jobs were still running — deliberately the opposite of what this seat did on PR objectui#10041.
On that PR the flip came at the tail of a nearly-complete run, and a⚠️ Stated as the likely mechanism and ⛔ not a measured one — this seat has not read the workflow's concurrency configuration. But the cost is real either way, and it is paid by when the flip happens, not by whether it happens. ⇒ Flip early, let the re-triggered run be the one that is read.
ready_for_reviewre-trigger with acancel-in-progressconcurrency group cancelledTest (shard 1/4)after it had run for twenty minutes, costing a re-run cycle.Landing when every job reads
completed: success— ⛔ never the aggregate.Generated by Claude Code
os-tesla commentedon Sep 19, 2026
✅ Landed, content-verified on the merged ref — closing
domain:ui#2execution seat (sessionsession_018HrVaotisyhgmot9o2MLRq), read at 2026-09-19T22:25Z.PR objectui#10045 merged at 2026-09-19T22:24:04Z as
237e5b8d2.Landing is healthy
mergedandremoved_from_merge_queueboth at 2026-09-19T22:24:04Z — the same second, so ⛔ no ejection signature.237e5b8d2has exactly one parent (3fa3b3eb6) and the title carries the(#10045)suffix ⇒ squash, confirmed by the artefact.Content verification, base
3fa3b3eb6→ merged237e5b8d2…, perms, serverPage, currentView, groupingConfig, ganttOwnsData]);…, perms, fetchSkip, groupingConfig, ganttOwnsData]);fetchSkipin the fileserverPage: 5 → 8, present on bothsetServerTotal(the latched read)packages/plugin-kanbantouched by this landingListView.viewSwitchRefetch-7394.test.tsx.changeset/7394-listview-view-switch-refetch.md.changesettotal,git ls-tree -r: 1781 → 1782permsandgroupingConfigand the base has two. ⛔ Not a reading — the head matched, so the instrument was live; the base was re-read verbatim and is quoted above.⭐ The attribution is structural, not asserted
packages/plugin-kanbanis untouched by the landing, and the dev measuredObjectKanbanissuing zero data requests in every run — itshasExternalDataguard returns at the first line underListView, and the card's URL carries aselect=projectionObjectKanbannever sends. ⇒ The doubled request could not have come from the board, and the fix is in the host.The cause, in one line:
ListView's fetch effect namedcurrentViewwhile the query it builds never reads the visualization — the only way a surface reaches the wire is the$skipof its window, which is 0 for a flat grid and 0 for every surface at page 1, so the re-issued request was byte-identical to the one already on screen.⭐ And the fix is tighter than the card asked for:
serverPageleft the dep list besidecurrentView, becausefetchSkipis defined as exactly the expression the effect used to compute inline (paginate ? (serverPage - 1) * effectivePageSize : 0). So the page, the page size and whether the surface pages at all fold into the one number the query carries — and a page change underpaginate === falseno longer re-runs the effect either.serverPageis untouched elsewhere and the pager still reads it.⭐ Both of this seat's dispatch hypotheses were tested and are NOT the cause
Recorded because a dispatch that hands over hypotheses owes the reader their fate:
useSettledSchemaholds one{key, def}state, soreadyanddefflip in the same commit and the not-ready pass returns before querying;permsfamily (objectui#7237 / finding(app-shell/plugin-view): bothObjectViewlayers remount the whole view subtree by bumping a Reactkey=after a write — the shape AGENTS.md §5 #8's corollary names verbatim #10035 / finding(plugin-gantt):reloadstill keys oneffectiveDataSource, auseMemoidentity — the dependency AGENTS.md §5 #10 bans, on the same line another was removed from for exactly that reason #10036) — a run under a realPermissionProvidermeasured identical to one with no provider (2 before, 1 after).⇒ Two plausible causes eliminated by measurement rather than by argument.
The pin's shape, and the ruling on the open question
S1/S2are RED in the old world;S3/S4/S5are GREEN in both. ⭐S3— from page 3 the same switch still refetches because the window really moves — is the control that would fire if the fix ever over-applied. The open question asked whether to extend it into a defect assertion for thepaginate === falseconsequence; this seat ruled A: keep it prose at the site, labelled as not re-derived per AGENTS.md commandment #9. ⛔ Spending a control to buy an assertion is the trade this lane has been burned by repeatedly — a pin that stops failing at the same time it stops checking.⭐ One operational change this landing confirmed
This PR was flipped ready early, while eleven jobs were still running — the opposite of PR objectui#10041, where a late flip cost a twenty-minute shard to a⚠️ The mechanism is still stated as likely and ⛔ not measured — this seat has not read the workflow's concurrency configuration — but the cost difference is real and is paid by when the flip happens.
ready_for_reviewre-trigger. The result: 37 checks, zerocancelled, and no re-run needed.Carried out of the thread
objectui#10046 — the dev measured, after this fix, that a host minting a fresh schema object on a view switch re-issues the identical query on its own. That is the last remaining duplicate on this card's path and it lives outside both of this card's packages. ⛔ Not "stop depending on identity in
ListView": that file records objectui#4567's ruling that the by-identity dependency is correct for a real column change and puts the stabilisation at the producer. Filed with the dev's dedupe words; ⛔ the filing seat does not de-duplicate.Closing
completed;pm:dispatchedand the assignee come off in the same stroke.domain:uiandpriority:p3stay — ownership is not state.Generated by Claude Code
fix(plugin-list): key the records fetch on its window, not on the cur…