⛔ 未分级、未指派 —— domain:*、type、priority 是分诊的写。
这不是假设,是刚发生过的一次事故的结构性成因
2026-09-13 事故链 (每一环都实测,判据在下面):
1e20f816e(feat(spec)!: retire the bare string sort clause on the list-view doors — the PRODUCER half of the sort seam #17914 ,feat(spec)!)把 ListViewSchema.sort 从两臂 union 收成单臂数组 —— 改动本身正确 ,是有意为之的破坏性收紧。
packages/cli/test/format-zod-union.test.ts 拿那扇门当 union 规格件,断言恰好 1 个 issue 且 code === 'invalid_union'。union 一塌 ⇒ 变成 2 个,且都不是 invalid_union。
feat(spec)!: retire the bare string sort clause on the list-view doors — the PRODUCER half of the sort seam #17914 自己的 CI 没跑这条测试 ⇒ PR 全绿,main 变红。
合并队列每次把候选在当前 main 上重建 ⇒ 每个入队 PR 都撞这条红被踢出。至少三次弹出 (Queue-flake anchor: test/format-zod-union.test.ts #18032 记录两次 + 第三次是 fix(plugin-security, spec): name the ADR-0025 reading of manifest.permissions when the audience-binding reconciler cannot use it, and make both halves of the key point at each other #18035 )。
队列分诊 workflow 自动立了 Queue-flake anchor: test/format-zod-union.test.ts #18032 ,命名为 queue-flake anchor 。它按日志里的失败文件路径计数,自己写明「This issue is a NAME, not a diagnosis」。⇒ 「flake」这个框架邀请人等,而不是查。 卡上三条自动化评论还在做 shard 组成 / vitest worker 数的相关性推测,修复席位实测后逐条推翻:红在裸 main、无 PR、无队列、无分片的本地一次就复现。
根因:闸门有一个它自己写下来的 盲区,而这条测试正落在里面
scripts/check-cross-package-test-inputs.mjs 的导入侧边界,逐字:
⚠️ The boundary that makes this safe: only specifiers that START RELATIVE (./, ../) are read. A BARE specifier (@objectstack/verify, node:fs) is an installed dependency resolved through node_modules …
⛔ NOT @objectstack/<pkg> // a BARE specifier is an installed dependency, never a repo source input
而 format-zod-union.test.ts 正是 import { ObjectStackDefinitionSchema } from '@objectstack/spec' —— 裸包名 。⇒ 它对 spec schema 的真实依赖,对受影响子集过滤器与 turbo 缓存双双不可见 。
实测(真 glob 语义,非 substring):
packages/cli 声明的跨包输入共 33 条
能匹配 packages/spec/src/ui/view.zod.ts 的: ⛔ 零条
其中所有 packages/spec 相关声明只有两条:
- packages/spec/src/system/translation.zod.ts
- packages/spec/src/data/field.zod.ts
⚠️ 这恰恰是闸门文件头自己描述的 #7802 形态:"a test whose real inputs are wider than its package is invisible to BOTH the affected-subset filter and the turbo cache, so it can go red on main while every PR reports green." —— 闸门预言了这件事,并且明确把这一类排除在自己视野之外。
⭐ 而修复把同一个陷阱重新装填在同一个文件 上
#18043 的修法是对的(把规格件重指到一扇仍是 union 的门,保住 #5341 的保证,⛔ 没有把断言改成迎合现状)。但实测新规格件 views[].list.gantt.tooltipFields[] 的声明位置:
tooltipFields 所在文件: packages/spec/src/ui/view.zod.ts ← 与 #17914 改的是同一个文件
cli 声明能匹配它的: ⛔ 零条
⇒ 下一个触及 view.zod.ts 的 feat!(很可能就是同一条 sort seam 的 CONSUMER 半边)会原样复现整条事故链:绿 PR → 红 main → 队列逐个踢 → 再立一张「flake anchor」。
可选修法(⛔ 建议,非裁决)
给 packages/cli 的 roster 补声明 :把它测试真正消费的 spec 面(至少 packages/spec/src/ui/**)加进跨包输入。最小、最直接,但治标 :下一个走裸包名消费 spec 的测试仍然隐形,而且这是一份「要记得更新的清单」—— 闸门文件头自己说过,「a list you must remember to update is exactly the failure mode that produced main is red for every PR that touches packages/spec: #7769 gave sys_api_key update without bulk, and the conformance scan that catches it lives in a package #7769 never touched #7802 」。
让闸门认识这一类 :裸 @objectstack/* 导入在本仓 workspace 内 并非普通 installed dependency,它是 workspace: 链接、指向仓内源码。可以把「裸包名 → 该 workspace 包的 src」作为一条可识别的读,要求声明或至少报告 。⚠️ 这会把总体扩大很多,需要先量规模再决定是拒绝还是只报告。
只改分诊叙事 :让 queue-flake anchor 在同一文件连续 N 次 弹出时改称「确定性回归候选」并强制定因。最便宜,不防复发但能把「躺着」的时间从数天压到一轮。
⚠️ 我倾向 2 > 1 > 3,但没有量过路 2 的总体规模 ,所以这只是排序不是建议。承接席位应先测:全仓有多少测试通过裸 @objectstack/* 消费另一个 workspace 包的源码?
判据复跑
# 盲区:cli 的 roster 匹配不到 view.zod.ts
node -e " import('./scripts/cross-package-test-inputs.mjs').then(m=>{ /* 见卡内 glob 匹配片段 */ })"
# 测试确实走裸包名
grep -n " from '@objectstack/spec'" packages/cli/test/format-zod-union.test.ts
# 新规格件与 #17914 改的是同一文件
git grep -ln tooltipFields origin/main -- ' packages/spec/src/**/*.zod.ts'
git show --stat 1e20f816e -- ' packages/spec/src/ui/view.zod.ts'
Refs: #18032 (被误判为 flake 的那张)· #18043 (修复)· #17914 (触发)· #7802 (闸门存在的理由)· #18035 / #18039 (被队列踢的受害者)
⛔ 未分级、未指派 ——
domain:*、type、priority 是分诊的写。这不是假设,是刚发生过的一次事故的结构性成因
2026-09-13 事故链(每一环都实测,判据在下面):
1e20f816e(feat(spec)!: retire the bare stringsortclause on the list-view doors — the PRODUCER half of the sort seam #17914,feat(spec)!)把ListViewSchema.sort从两臂 union 收成单臂数组 —— 改动本身正确,是有意为之的破坏性收紧。packages/cli/test/format-zod-union.test.ts拿那扇门当 union 规格件,断言恰好 1 个 issue 且code === 'invalid_union'。union 一塌 ⇒ 变成 2 个,且都不是invalid_union。sortclause on the list-view doors — the PRODUCER half of the sort seam #17914 自己的 CI 没跑这条测试 ⇒ PR 全绿,main变红。manifest.permissionswhen the audience-binding reconciler cannot use it, and make both halves of the key point at each other #18035)。queue-flake anchor。它按日志里的失败文件路径计数,自己写明「This issue is a NAME, not a diagnosis」。⇒ 「flake」这个框架邀请人等,而不是查。 卡上三条自动化评论还在做 shard 组成 / vitest worker 数的相关性推测,修复席位实测后逐条推翻:红在裸 main、无 PR、无队列、无分片的本地一次就复现。根因:闸门有一个它自己写下来的盲区,而这条测试正落在里面
scripts/check-cross-package-test-inputs.mjs的导入侧边界,逐字:而
format-zod-union.test.ts正是import { ObjectStackDefinitionSchema } from '@objectstack/spec'—— 裸包名。⇒ 它对 spec schema 的真实依赖,对受影响子集过滤器与 turbo 缓存双双不可见。实测(真 glob 语义,非 substring):
mainwhile every PR reports green." —— 闸门预言了这件事,并且明确把这一类排除在自己视野之外。⭐ 而修复把同一个陷阱重新装填在同一个文件上
#18043 的修法是对的(把规格件重指到一扇仍是 union 的门,保住 #5341 的保证,⛔ 没有把断言改成迎合现状)。但实测新规格件
views[].list.gantt.tooltipFields[]的声明位置:⇒ 下一个触及
view.zod.ts的feat!(很可能就是同一条 sort seam 的 CONSUMER 半边)会原样复现整条事故链:绿 PR → 红 main → 队列逐个踢 → 再立一张「flake anchor」。可选修法(⛔ 建议,非裁决)
packages/cli的 roster 补声明:把它测试真正消费的 spec 面(至少packages/spec/src/ui/**)加进跨包输入。最小、最直接,但治标:下一个走裸包名消费 spec 的测试仍然隐形,而且这是一份「要记得更新的清单」—— 闸门文件头自己说过,「a list you must remember to update is exactly the failure mode that producedmainis red for every PR that touchespackages/spec: #7769 gavesys_api_keyupdatewithoutbulk, and the conformance scan that catches it lives in a package #7769 never touched #7802」。@objectstack/*导入在本仓 workspace 内并非普通 installed dependency,它是workspace:链接、指向仓内源码。可以把「裸包名 → 该 workspace 包的 src」作为一条可识别的读,要求声明或至少报告。@objectstack/*消费另一个 workspace 包的源码?判据复跑
Refs: #18032(被误判为 flake 的那张)· #18043(修复)· #17914(触发)· #7802(闸门存在的理由)· #18035 / #18039(被队列踢的受害者)