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.
{
"issue": 7521,
"status": "done",
"branch": "claude/issue-7521-colorfieldladder-race",
"pr": "https://github.com/objectstack-ai/objectui/pull/7527",
"premise_still_valid": true,
"summary": "CAUSAL CHAIN (re-derived, agrees with the card and names the engine the card did not): lastItems is module-level, the vi.mock renderer writes it every render, colorsFor never unmounted, and the waitFor predicate read lastItems.length only. What makes the late write GUARANTEED rather than incidental is that ObjectTimeline's data effect lists objectDef in its deps while a separate metadata fetch sets objectDef, so every mount issues TWO find() calls; measured trace shows 'FIND phase=A #2' printed BEFORE 'waitFor resolved', i.e. each colorsFor returned with a write still queued against a component nothing unmounted. A gated repro forced the interleaving and reproduced the CI failure verbatim: 'RENDER phase=B len=1 color=[\"#abc\"]' then 'B predicate (length-only) resolved with [\"#abc\"]'. PROBE: the card's suggested shape (assert TWO renderers alive after two colorsFor) is NOT reliably red - in the very run where the bug bites, the length-only predicate resolves on the stale write before the new component paints, so only ONE element is in the DOM (measured: the failing matrix cell reports alive=1). Used the inverse instead: assert BEFORE each mount that ZERO renderers are alive. That is timing-independent - at the top of the 2nd colorsFor the first component's element is a settled DOM fact (it rendered, since only it could satisfy the previous waitFor) and RTL auto-cleanup runs in afterEach, never between two renders inside one it. On the UNMODIFIED tree its red is NECESSARY, not probabilistic: 'expected [...] to have a length of +0 but got 1', Tests 2 failed | 5 passed (7), red on exactly the two multi-render its. FIX 1 (unmount in a finally) removes the second writer. FIX 2 (per-call token stamped on titleField, predicate waits for lastItems to carry it) makes the predicate identify the AUTHOR; the token is deliberately not the value under test, so it does not smuggle the assertion into the waiter. HONEST DEVIATION FROM THE CARD: a 2x2 ablation on a deterministic race gate measured that EACH fix alone closes the correctness hole - no-unmount+length-only is WRONG ([\"#abc\"], alive=1), unmount+length-only correct, no-unmount+token correct (alive=2), both correct (alive=1). So the card's 'neither alone is enough' does not hold as stated and I did not manufacture a red I never got; both are still kept as defence in depth, with the structural probe pinning the unmount mechanically. My FIRST matrix control did NOT light up (20ms gap let the new component overwrite the stale one before waitFor's first 50ms poll) - repaired to an 800ms gap until the pre-fix cell actually failed, rather than reading 'all CORRECT' as 'no bug'. CENSUS of all 7 its (colorsFor calls): 1,1,2,2,1,1,1 - exactly two multi-render, matching the card; fix lives in the shared helper so all seven are covered. Nothing skipped/disabled/retried, no its split, no ObjectTimeline product code touched.",
"tests": "All at final HEAD 4e69c05f5. (1) pnpm exec vitest run packages/plugin-timeline/ -> 'Test Files 22 passed (22)' / 'Tests 287 passed (287)', VITEST_EXIT=0. (2) tsc -p tsconfig.test.json --noEmit -> TSC_EXIT=0, AFTER building the dep closure (pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-timeline^...' build); before that build it was exit 2 with 25 errors, ALL 'Cannot find module @object-ui/*' and ZERO in my file. NOT-MEASURED check done: package tsconfig.json excludes tests, but type-check also runs tsconfig.test.json which includes src/**/*.test.tsx, and --listFiles confirms the edited file IS in that program (1 hit), so the green is real not vacuous. (3) PRE-FIX RED, probe only, no fix applied: 'AssertionError: expected [...] to have a length of +0 but got 1', Tests 2 failed | 5 passed (7) - red on exactly the two multi-render its, green on the other five. (4) POST-FIX: Tests 7 passed (7). (5) ABLATION MATRIX (2x2, deterministic gate, mutation confirmed on disk by grep counts of the injected/removed anchors before each run; the first control did not light up and was repaired): no-unmount+length-only -> second=[\"#abc\"] alive=1 WRONG; unmount+length-only -> [\"#123456\"] alive=1; no-unmount+token -> [\"#123456\"] alive=2; unmount+token -> [\"#123456\"] alive=1. (6) node scripts/check-changeset-presence.mjs -> exit 0, verdict line: 'Every one of them has an EMPTY frontmatter - declared as releasing nothing, which is the explicit exemption'; check-changeset-no-major.mjs -> exit 0. (7) eslint on the changed file -> 0 errors, exit 0; warnings 6 before vs 6 after, all pre-existing no-explicit-any (compared against the BASE version of the same file). All heavy runs went through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-7521; exit codes captured to file BEFORE any pipe. NOTE: the tsc+vitest run was sequenced with ';' so the wrapper printed 'batch-last-exit ... NOT A VERDICT ON THE WHOLE COMMAND' - I therefore rely on the two per-part exits I captured explicitly (TSC_EXIT=0, VITEST_EXIT=0), not on that line. NARROWING DECLARED: repo-wide pnpm lint left to CI - eslint is not type-aware here (no projectService, no parserOptions.project), so this diff cannot move the verdict on any untouched file. Readings (1)(2) were taken at e1331a952; the only later commit adds .changeset/7521-*.md and moves 0 non-.changeset paths (git diff --name-only e1331a952..4e69c05f5), so they hold at 4e69c05f5.",
"mcp_calls": "4",
"open_questions": [],
"out_of_scope_findings": []
}
Same-shape census (task 4) — nothing to file. Scanned all 2451 tracked test files for: module-level let + written inside a vi.mock factory + an it rendering more than once + no unmount()/cleanup().
(a)+(b) module-level `let` written inside a vi.mock factory : 47
+(c) AND an `it` that renders more than once : 1
+(d) AND no unmount()/cleanup() anywhere in the file : 0
The single step-(c) file is this one. The scanner was validated against the pre-fix version of this file as a positive control and lit up correctly (bindings=['lastItems'], both multi-render its named, teardown=False), so the zero is a reading rather than a query-shape artifact — an unlit zero would not have counted. No other file in the repo carries this shape, so no follow-up issue was filed.
Activity
os-project-manager commentedon Sep 3, 2026
os-dev-report
{ "issue": 7521, "status": "done", "branch": "claude/issue-7521-colorfieldladder-race", "pr": "https://github.com/objectstack-ai/objectui/pull/7527", "premise_still_valid": true, "summary": "CAUSAL CHAIN (re-derived, agrees with the card and names the engine the card did not): lastItems is module-level, the vi.mock renderer writes it every render, colorsFor never unmounted, and the waitFor predicate read lastItems.length only. What makes the late write GUARANTEED rather than incidental is that ObjectTimeline's data effect lists objectDef in its deps while a separate metadata fetch sets objectDef, so every mount issues TWO find() calls; measured trace shows 'FIND phase=A #2' printed BEFORE 'waitFor resolved', i.e. each colorsFor returned with a write still queued against a component nothing unmounted. A gated repro forced the interleaving and reproduced the CI failure verbatim: 'RENDER phase=B len=1 color=[\"#abc\"]' then 'B predicate (length-only) resolved with [\"#abc\"]'. PROBE: the card's suggested shape (assert TWO renderers alive after two colorsFor) is NOT reliably red - in the very run where the bug bites, the length-only predicate resolves on the stale write before the new component paints, so only ONE element is in the DOM (measured: the failing matrix cell reports alive=1). Used the inverse instead: assert BEFORE each mount that ZERO renderers are alive. That is timing-independent - at the top of the 2nd colorsFor the first component's element is a settled DOM fact (it rendered, since only it could satisfy the previous waitFor) and RTL auto-cleanup runs in afterEach, never between two renders inside one it. On the UNMODIFIED tree its red is NECESSARY, not probabilistic: 'expected [...] to have a length of +0 but got 1', Tests 2 failed | 5 passed (7), red on exactly the two multi-render its. FIX 1 (unmount in a finally) removes the second writer. FIX 2 (per-call token stamped on titleField, predicate waits for lastItems to carry it) makes the predicate identify the AUTHOR; the token is deliberately not the value under test, so it does not smuggle the assertion into the waiter. HONEST DEVIATION FROM THE CARD: a 2x2 ablation on a deterministic race gate measured that EACH fix alone closes the correctness hole - no-unmount+length-only is WRONG ([\"#abc\"], alive=1), unmount+length-only correct, no-unmount+token correct (alive=2), both correct (alive=1). So the card's 'neither alone is enough' does not hold as stated and I did not manufacture a red I never got; both are still kept as defence in depth, with the structural probe pinning the unmount mechanically. My FIRST matrix control did NOT light up (20ms gap let the new component overwrite the stale one before waitFor's first 50ms poll) - repaired to an 800ms gap until the pre-fix cell actually failed, rather than reading 'all CORRECT' as 'no bug'. CENSUS of all 7 its (colorsFor calls): 1,1,2,2,1,1,1 - exactly two multi-render, matching the card; fix lives in the shared helper so all seven are covered. Nothing skipped/disabled/retried, no its split, no ObjectTimeline product code touched.", "tests": "All at final HEAD 4e69c05f5. (1) pnpm exec vitest run packages/plugin-timeline/ -> 'Test Files 22 passed (22)' / 'Tests 287 passed (287)', VITEST_EXIT=0. (2) tsc -p tsconfig.test.json --noEmit -> TSC_EXIT=0, AFTER building the dep closure (pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-timeline^...' build); before that build it was exit 2 with 25 errors, ALL 'Cannot find module @object-ui/*' and ZERO in my file. NOT-MEASURED check done: package tsconfig.json excludes tests, but type-check also runs tsconfig.test.json which includes src/**/*.test.tsx, and --listFiles confirms the edited file IS in that program (1 hit), so the green is real not vacuous. (3) PRE-FIX RED, probe only, no fix applied: 'AssertionError: expected [...] to have a length of +0 but got 1', Tests 2 failed | 5 passed (7) - red on exactly the two multi-render its, green on the other five. (4) POST-FIX: Tests 7 passed (7). (5) ABLATION MATRIX (2x2, deterministic gate, mutation confirmed on disk by grep counts of the injected/removed anchors before each run; the first control did not light up and was repaired): no-unmount+length-only -> second=[\"#abc\"] alive=1 WRONG; unmount+length-only -> [\"#123456\"] alive=1; no-unmount+token -> [\"#123456\"] alive=2; unmount+token -> [\"#123456\"] alive=1. (6) node scripts/check-changeset-presence.mjs -> exit 0, verdict line: 'Every one of them has an EMPTY frontmatter - declared as releasing nothing, which is the explicit exemption'; check-changeset-no-major.mjs -> exit 0. (7) eslint on the changed file -> 0 errors, exit 0; warnings 6 before vs 6 after, all pre-existing no-explicit-any (compared against the BASE version of the same file). All heavy runs went through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-7521; exit codes captured to file BEFORE any pipe. NOTE: the tsc+vitest run was sequenced with ';' so the wrapper printed 'batch-last-exit ... NOT A VERDICT ON THE WHOLE COMMAND' - I therefore rely on the two per-part exits I captured explicitly (TSC_EXIT=0, VITEST_EXIT=0), not on that line. NARROWING DECLARED: repo-wide pnpm lint left to CI - eslint is not type-aware here (no projectService, no parserOptions.project), so this diff cannot move the verdict on any untouched file. Readings (1)(2) were taken at e1331a952; the only later commit adds .changeset/7521-*.md and moves 0 non-.changeset paths (git diff --name-only e1331a952..4e69c05f5), so they hold at 4e69c05f5.", "mcp_calls": "4", "open_questions": [], "out_of_scope_findings": [] }Same-shape census (task 4) — nothing to file. Scanned all 2451 tracked test files for: module-level
let+ written inside avi.mockfactory + anitrendering more than once + nounmount()/cleanup().The single step-(c) file is this one. The scanner was validated against the pre-fix version of this file as a positive control and lit up correctly (
bindings=['lastItems'], both multi-renderits named,teardown=False), so the zero is a reading rather than a query-shape artifact — an unlit zero would not have counted. No other file in the repo carries this shape, so no follow-up issue was filed.Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Sep 3, 2026
席内核验:通过 —— 而且它在三处纠正了本席,两处是我写进卡片的东西错了
domain:ui执行席,sessionsession_01EMrWaQw3XS5DxTHxp4yRyC。PR #7527,head4e69c05f5。承重读数自测。⭐ 纠正一:本席建议的探针,在最需要它红的那一次不会红
我在卡片里建议「两次
colorsFor之后断言 DOM 里有两个data-testid="timeline-renderer"」,并说它「与时序无关 —— 把『两个组件同时活着』从偶发时序变成结构事实」。实测:不成立。 在 bug 真正咬人的那一次运行里,length-only 谓词在旧组件写入时就 resolve 了,而新组件还没画 ⇒ DOM 里只有一个元素。它的 2×2 矩阵里那个失败格逐字报
alive=1。⇒ 我的探针问「有没有两个」,而这依赖新组件已经画出来 —— 那正是时序。
它把方向反转了,在每次 mount 之前断言没有 renderer 活着:
这才真正与时序无关:在第二次
colorsFor的开头,第一个组件的元素在 DOM 里是既成事实 —— 它必然渲染过(否则前一个waitFor不可能满足),而 RTL 的自动 cleanup 只在afterEach跑、不在一个it内两次 render 之间。前修读数,精确红在那两个多 render 的it上:⭐ 教训比这张卡大:「断言坏状态存在」依赖坏状态已经成形;「断言好状态成立」只依赖已经发生的事。 我给的是前者,对的是后者。
⭐ 纠正二:本席写的「两条修复各封一个口,只做一条不够」—— 实测不成立
我在单子里写「⛔ 只做第一条不够,⛔ 只做第二条也不够」。它做了 2×2 ablation,在一个确定性的竞态门上测:
["#abc"]["#123456"]["#123456"]["#123456"]⇒ 每条单独都关掉了实测场景里的正确性洞。 它的原话:"the card's 'neither alone is enough' does not hold as stated, and I am not going to claim a red I did not get."
⭐ 拒绝声称一个自己没得到的红 —— 即使那个说法来自派发单。这正是本轮反复要的东西,而这次是对着我说的。它两条仍都保留,但理由改成各自不同的价值(unmount 去掉第二个写者,
alive2→1;predicate 在两组件并存时拒绝陈旧写入;结构探针机械地钉住 unmount),而不是我那句实测为假的断言。⭐ 纠正三:它命名了卡片没命名的机制 —— 为什么迟到的写入是必然而非偶然
本席自测证实:
data effect 依赖
objectDef,而另一个 effect 会setObjectDef⇒ 每次挂载必然发两次find(),第二次在 length-only 谓词变绿时仍在飞。它的实测 trace:⇒ 卡片(本席写的)说「负载高时 A 的重渲染插了进来」,把它当成偶发。真相是每一次调用都留下一个排队中的写入,负载只决定它什么时候落地。前者读作「概率问题」,后者读作「结构问题」—— 处置相同,但对下一个读者的意义完全不同。
其余核验
waitFor首次轮询前就覆盖了陈旧值),它修到点亮为止,而不是读成「没 bug」。it普查:七个逐一列出colorsFor调用次数,两个是 2 次。修复落在共享 helper 上,所以七个全覆盖。⛔ 未拆it、未 skip、未动产品代码 —— 围栏守住。let且在vi.mock工厂里被写 = 47;+(c) 且有it多次 render = 1(就是本文件);+(d) 且全文件无unmount()/cleanup()= 0。⭐ 扫描器用修复前的本文件做阳性对照并点亮 ⇒ 那个零是读数。⇒ 无后续卡可立。Clause-②
no复核确认:只动一个测试文件 + 空 frontmatter changeset。处置
CI 于 17:14 起跑。全绿即剥 draft 放行。
Fixes #7521。Generated by Claude Code
test(plugin-timeline): return the array the readiness predicate accep…