Restart-when: closed #19024
⬆️ 这一行由分诊席于本轮补为行首、未加引用的形式。原句写在下方 STOP 块的引用里(> ⛔ **Serialised: …**),行首正则与正文-only 两道检测都读不到它 —— 那正是前任交接台账记录过的盲区。⛔ 内容未改,只改了可被机器读到这一点。
⛔ STOP — the mechanism this body and TITLE name is REFUTED by measurement
The defect is real, but it is not the turbo shared cache. Measured on this container at 1047fe1016: a cache hit serves a byte-identical dist (451 files, path+sha256 diff exit 0, raw readdir order identical); a restore normalises mtime to epoch 0, so the freshness guard accuses rather than being short-circuited; and turbo's 1808-input set is a strict superset of what the stamp hashes, so on a cache hit the stamp necessarily matches.
⚠️ The reproduction recipe below is also not runnable as written — pnpm --filter @objectstack/spec build never invokes turbo, and pnpm appends the --force in step 3 to check-dev-prereqs.mjs, not to turbo. Steps 1 and 3 are the same command. ⇒ running it returns a null result that is not a failure to reproduce.
⭐ What survives, and is the whole card: in check-generated.ts the readsDist refusal lives only inside the --fix loop, while the reporting path prints its advisory only when a gate FAILS — so a gate that passes over a stale dist prints 「All 16 generated artifacts are up to date.」 saying nothing about the dist. And distIsStale is (mtime-accuses AND stamp-not-match), so it answers fresh whenever mtime says fresh: the acquitting branch never consults the only content-based evidence there is. A write path gated on the absence of an accusation rather than on positive proof.
⛔ Serialised: Restart-when: closed objectstack-ai/objectstack#19024 — that PR holds check-generated.ts, lib/sharded-artifacts.ts and build-api-surface.ts, and under the maintainer's own ruling it is maintainer-landed. It also deletes the surface of defect (β).
Full measurements, radii and lit controls: comment 5737695403. The seat's retraction of a dispatch made without reading this thread: comment 5737665608. ⛔ The prose below is kept rather than rewritten, and is now HISTORY.
Filed by the domain:spec#4 seat (session_01AmH9bKvGoLjiY86Q4Z3og2) on a dev's out-of-scope finding while working #18163 / PR #19067. ⛔ Filed unassigned, ⛔ no priority or type applied — routing and grading are triage's. ⛔ Not a claim.
The defect
pnpm --filter @objectstack/spec check:generated --fix will WRITE generated artifacts from a dist that turbo served from the shared cache — which every linked worktree on a container replays — and then report:
All 16 generated artifacts are up to date.
⇒ a verifier reporting success over a measurement it did not take. Route and surface ownership rule 3 calls that worse than no verifier: a green from an instrument that never looked is indistinguishable, downstream, from a green from one that did.
Repro, as the reporting dev recorded it
In a linked worktree:
pnpm --filter @objectstack/spec build (plain — turbo may serve from cache)
pnpm --filter @objectstack/spec check:generated --fix
pnpm --filter @objectstack/spec build --force
pnpm --filter @objectstack/spec check:generated
⇒ the shards differ between (2) and (4). Two independent --force builds agree with each other.
⭐ It already cost a real reading, and the cost is the reason to file
The dev disclosed it rather than quietly fixing it: it had reverted an edit partly on the grounds of 「330 lines of order-only churn in the unrelated ui shard」. From a --force build that tree emits ui.txt and automation.txt byte-identical to main and no such churn exists. ⇒ the measurement was VOID — not disproven, and the decision had to be re-grounded on what survives measurement.
⚠️ A void reading is the dangerous shape here, not a wrong one: it looked like evidence, it was quoted as evidence, and nothing in the instrument's output said it had not looked.
Why the existing guardrail does not cover it
AGENTS.md documents the shared-cache hazard in the READ direction (「a typecheck failing on a package your diff never touched」) and prescribes a force-rebuild. The gate's own prescription says only 「Build first」.
⇒ the WRITE direction — --fix committing artifacts derived from a cached dist — is unguarded.
Who feels it
Whoever regenerates spec artifacts inside a worktree, which is every packages/spec card. The generated artifacts land in commits and are gated by check:generated, so a wrong one is discovered downstream, on someone else's PR.
Dedupe words
check:generated --fix · turbo cache · stale dist · api-surface-declarations · worktree shared cache
⛔ The filer does not dedupe (only these words); triage searches its own list, and a zero there needs a control that hits.
Related: #18163 (the card whose round-2 work surfaced it) · PR #19067
维护者速读(由 domain:spec 席 3 于 2026-09-20T16:35Z 补写;⛔ 不含判定)
一句话问题:check:generated --fix 会照着一个已经不描述 src 的 dist去改写已提交的产物(实测删掉 3 个还活着的导出),而下一次 check:generated 会对着那棵脏树打印「All 15 generated artifacts are up to date」。守门的只有一个三行谓词,而它从不读 dist 的任何一个字节——它比的是 mtime 和一份对输入取的摘要。
为什么 mtime 挡不住:leg 1 只能给「比 src 旧」的 dist 定罪,而任何一次写入都会把 dist 的 mtime 往前推。⇒ 这一整类损坏按构造就在这个谓词能报出的范围之外。实测:一份删了两个导出的 dist,distIsStale=false 且 stamp=match;--fix 照写不误。
谁会自然造出这个状态:被打断或被杀掉的声明生成过程(本包 build 是 tsup(clean:!isDts)+ 独立 DTS 过程,而 stamp 由 build 脚本最后一步写,所以中断会留下「新的 dist mtime + 上一次的 stamp」)· 只发了一部分 chunk · 任何手改或部分还原的 dist。实测 E1:46 个 chunk 声明缺 2 个时,check:api-surface 报 70 处 breaking 移除——那一刻 --fix 会从基线里删掉 64 个活导出,只打印一行 ✓ gen:api-surface。
四个修法,代价各不同(⛔ 本席不选):A 让 build 额外记一份对 dist 产物的摘要,放弃时序推断改用正面证据;B 只是别再丢弃已有的输入侧证据(实测对本复现无效——那次 stamp 说 match);C 让写入路径干脆先重建再写(全封死,代价是每次 fix 一次完整 build,而本卡线程自己警告过「慢到没人跑的守卫会被绕过」);D 只改报告面,让那句「All N ... up to date」说出它凭的是哪一份 dist,并让 --fix 事后重验。
⚠️ 为什么必须您来裁,而不是本席派一轮下去:A 会把 stamp 从「只能放行」变成「能定罪」,B/C 会动放行分支——任何一个都在改门禁的接受集,那是本卡分诊已经记下的人工地板(Clause-② + 契约复核),⛔ 不是一次安静的工具修补。
末句一字问:A / B / C / D?
os-decision-facets
- ① 项目长远合理性 — 今天的契约是「stamp 只能放行,缺失或不可读都让 mtime 拒绝继续站着」。A 把它改成能定罪,是收紧特例;D 只让报告说出自己量了什么,不新增任何特例。⇒ 两者都缩小而非扩大契约增生;B 留着一个已实测挡不住本复现的守卫,C 新增一条「每次都重建」的永久成本。
- ② 实际业务拉动 — ⏱️ 今天就撞上了:一次
--fix 实测删掉 3 个活导出并被下一次运行判为绿。拉动不是假设。⛔ 但也要说清:本轮未量到 CI 上能否达到这个被放行的状态(CI 里 dist 在 job 内构建)⇒ 已知受害面是本地与 agent 容器。
- ③ 防 AI 犯错 — 这正是「响亮拒绝优于静默容忍」的反面教材:错误的那次静默写入,而绿色出现在下一次运行、且结构上不可能反驳上一次写了什么(
--fix 写完不复验,下一次检查比的又是同一份 dist——任何 dist 都会自我同意)。D 直接治这一条:让那句绿说出它凭什么。
- ④ 创业阶段不扩散 — A 要新增一份产物(dist 摘要)并因此新增一份永久义务;D 不新增任何键、任何产物,只让现有那句话诚实。⇒ 按「remove 优于 declare-and-maintain」,D 是最小面;若只能落一半,D 是可独立落地的那一半。
Prior rulings read: check,generated,tooling,writes,artifacts,turbo,cache-served,dist,reports,date,verifier,reporting (+2 more) → 53 hits; ADR-0058 D4, ADR-0087 D5, ADR-0087 D6, ADR-0119 D4, ADR-0124 D1, ADR-0029 D3, ADR-0029 D8, ADR-0029 D9.9, ADR-0044 D1; thread: none
⚠️ 以上 ADR 由 check-prior-rulings --card 19086(⏱️ 16:33Z,origin/main fade3da2,真退出 0)按词命中,本席逐条读过:没有一条回答本卡的问题,所以该工具「已有 ACCEPTED 裁决 ⇒ 本卡可能是执行而非决策」的常设警告在此不成立。
推荐 A 复合 D;自检「只看①选 A(它是唯一实测能封住本轮复现的机制的选项);②③④ 是否翻转:不翻——② 拉动实测存在、③ D 直接治静默、④ D 是最小面且可独立先落」。
置信缺口:⛔ 未量到 CI 能否达到被放行的状态;⛔ 未量到任何修法的运行代价(没取过「对 dist 取摘要」的耗时);⛔ 那份「被打断的 build 会留下此状态」是从 tsup.config.ts 与 build 脚本次序推出的,⛔ 不是杀过一次 build 量出来的;⛔ 另四个读 dist 的门与 bundle 轴未在损坏 dist 下测过。
Generated by Claude Code
Restart-when: closed #19024
⬆️ 这一行由分诊席于本轮补为行首、未加引用的形式。原句写在下方 STOP 块的引用里(
> ⛔ **Serialised: …**),行首正则与正文-only 两道检测都读不到它 —— 那正是前任交接台账记录过的盲区。⛔ 内容未改,只改了可被机器读到这一点。Filed by the
domain:spec#4seat (session_01AmH9bKvGoLjiY86Q4Z3og2) on a dev's out-of-scope finding while working #18163 / PR #19067. ⛔ Filed unassigned, ⛔ no priority or type applied — routing and grading are triage's. ⛔ Not a claim.The defect
pnpm --filter @objectstack/spec check:generated --fixwill WRITE generated artifacts from adistthat turbo served from the shared cache — which every linked worktree on a container replays — and then report:⇒ a verifier reporting success over a measurement it did not take. Route and surface ownership rule 3 calls that worse than no verifier: a green from an instrument that never looked is indistinguishable, downstream, from a green from one that did.
Repro, as the reporting dev recorded it
In a linked worktree:
pnpm --filter @objectstack/spec build(plain — turbo may serve from cache)pnpm --filter @objectstack/spec check:generated --fixpnpm --filter @objectstack/spec build --forcepnpm --filter @objectstack/spec check:generated⇒ the shards differ between (2) and (4). Two independent
--forcebuilds agree with each other.⭐ It already cost a real reading, and the cost is the reason to file
The dev disclosed it rather than quietly fixing it: it had reverted an edit partly on the grounds of 「330 lines of order-only churn in the unrelated
uishard」. From a--forcebuild that tree emitsui.txtandautomation.txtbyte-identical to main and no such churn exists. ⇒ the measurement was VOID — not disproven, and the decision had to be re-grounded on what survives measurement.Why the existing guardrail does not cover it
AGENTS.mddocuments the shared-cache hazard in the READ direction (「a typecheck failing on a package your diff never touched」) and prescribes a force-rebuild. The gate's own prescription says only 「Build first」.⇒ the WRITE direction —
--fixcommitting artifacts derived from a cached dist — is unguarded.Who feels it
Whoever regenerates spec artifacts inside a worktree, which is every
packages/speccard. The generated artifacts land in commits and are gated bycheck:generated, so a wrong one is discovered downstream, on someone else's PR.Dedupe words
check:generated --fix·turbo cache·stale dist·api-surface-declarations·worktree shared cache⛔ The filer does not dedupe (only these words); triage searches its own list, and a zero there needs a control that hits.
Related: #18163 (the card whose round-2 work surfaced it) · PR #19067
Generated by Claude Code
维护者速读(由
domain:spec席 3 于 2026-09-20T16:35Z 补写;⛔ 不含判定)一句话问题:
check:generated --fix会照着一个已经不描述 src 的 dist去改写已提交的产物(实测删掉 3 个还活着的导出),而下一次check:generated会对着那棵脏树打印「All 15 generated artifacts are up to date」。守门的只有一个三行谓词,而它从不读 dist 的任何一个字节——它比的是 mtime 和一份对输入取的摘要。为什么 mtime 挡不住:leg 1 只能给「比 src 旧」的 dist 定罪,而任何一次写入都会把 dist 的 mtime 往前推。⇒ 这一整类损坏按构造就在这个谓词能报出的范围之外。实测:一份删了两个导出的 dist,
distIsStale=false且 stamp=match;--fix照写不误。谁会自然造出这个状态:被打断或被杀掉的声明生成过程(本包 build 是
tsup(clean:!isDts)+ 独立 DTS 过程,而 stamp 由 build 脚本最后一步写,所以中断会留下「新的 dist mtime + 上一次的 stamp」)· 只发了一部分 chunk · 任何手改或部分还原的 dist。实测 E1:46 个 chunk 声明缺 2 个时,check:api-surface报 70 处 breaking 移除——那一刻--fix会从基线里删掉 64 个活导出,只打印一行✓ gen:api-surface。四个修法,代价各不同(⛔ 本席不选):A 让 build 额外记一份对 dist 产物的摘要,放弃时序推断改用正面证据;B 只是别再丢弃已有的输入侧证据(实测对本复现无效——那次 stamp 说
match);C 让写入路径干脆先重建再写(全封死,代价是每次 fix 一次完整 build,而本卡线程自己警告过「慢到没人跑的守卫会被绕过」);D 只改报告面,让那句「All N ... up to date」说出它凭的是哪一份 dist,并让--fix事后重验。末句一字问:A / B / C / D?
os-decision-facets
--fix实测删掉 3 个活导出并被下一次运行判为绿。拉动不是假设。⛔ 但也要说清:本轮未量到 CI 上能否达到这个被放行的状态(CI 里 dist 在 job 内构建)⇒ 已知受害面是本地与 agent 容器。--fix写完不复验,下一次检查比的又是同一份 dist——任何 dist 都会自我同意)。D 直接治这一条:让那句绿说出它凭什么。Prior rulings read: check,generated,tooling,writes,artifacts,turbo,cache-served,dist,reports,date,verifier,reporting (+2 more) → 53 hits; ADR-0058 D4, ADR-0087 D5, ADR-0087 D6, ADR-0119 D4, ADR-0124 D1, ADR-0029 D3, ADR-0029 D8, ADR-0029 D9.9, ADR-0044 D1; thread: none
check-prior-rulings --card 19086(⏱️ 16:33Z,origin/mainfade3da2,真退出 0)按词命中,本席逐条读过:没有一条回答本卡的问题,所以该工具「已有 ACCEPTED 裁决 ⇒ 本卡可能是执行而非决策」的常设警告在此不成立。推荐 A 复合 D;自检「只看①选 A(它是唯一实测能封住本轮复现的机制的选项);②③④ 是否翻转:不翻——② 拉动实测存在、③ D 直接治静默、④ D 是最小面且可独立先落」。
置信缺口:⛔ 未量到 CI 能否达到被放行的状态;⛔ 未量到任何修法的运行代价(没取过「对 dist 取摘要」的耗时);⛔ 那份「被打断的 build 会留下此状态」是从
tsup.config.ts与 build 脚本次序推出的,⛔ 不是杀过一次 build 量出来的;⛔ 另四个读 dist 的门与 bundle 轴未在损坏 dist 下测过。Generated by Claude Code