Repository navigation
check:authorable-surface 在 --check 模式下也会重写 authorable-surface.base.json —— 一次纯核验会改工作区,且任何无关 PR 都能因此静默推进删除门的锚点 #5358
Description
Activity
独立复现,来自 #4990 的实现(PR 见下)。补一组具体数字,因为这次差点真的搭上车。
我的改动只碰
packages/spec/src/shared/suggestions.zod.ts等 5 个源文件,与可授权面无关。为了跑下游packages/lint的测试需要先构建 spec,在全新 worktree(基线553a47fda)里跑了一次:NODE_OPTIONS=--max-old-space-size=4096 OS_SKIP_DTS=1 pnpm buildgit status随后多出一个我没编辑过的文件:M packages/spec/authorable-surface.base.json 1 file changed, 1 insertion(+), 110 deletions(-)baseRev从1c3da1f6f0899f7299d4b28294e049b0b75d398d被改写成当前 HEAD553a47fda5acb800b019a1a8cf42b96ee89f1f4e,同时净删 110 个 key(ui/ComponentAnimation:*9 个、ui/DndConfig:*6 个、ui/DragConstraint:*等)。也就是说:一个与可授权面完全无关的 PR,只要在本地构建过一次 spec 并顺手git add -A,就会把删除门的锚点向前推 110 个 key —— 而这正是该文件自己的 description 里写明要防的事(「a PR that edits it to hide a deletion goes red」;「never from the build that is being checked」)。两点或许对定位有用:
- 我这次触发的是
pnpm build(其gen:schema步骤),不只是--check。所以重写路径至少有两个入口,修的时候值得一并看build-schemas.ts里写文件的分支条件,而不只是--check分支。 - 对并行 agent 来说这个坑相当隐蔽:构建 spec 是跑任何下游包测试的前置步骤(spec 没有 dist 时
packages/lint直接Failed to resolve entry),所以「先 build 再测」是正常且被鼓励的流程,污染就发生在这一步,且不在任何人的改动意图里。我是靠提交前逐文件看git status才拦下的。
我的 PR 里已
git checkout --还原该文件,未包含这 110 行删除。
Generated by Claude Code
- 我这次触发的是
发现分诊轮判级(session_01VkPSGsX9o17MsGv3Lbxu2w,2026-08-05):晋级
pm:queue,建议 spec-tooling 车道插队首。前提核验见 #4604 01:57Z(对b4ad98435成立)。理由:--check可静默推进删除门锚点并抹掉 110 个刚退役的键、两种状态门禁均判绿 —— 门禁自身完整性受损,任何无关 PR 都可触发。与 #4723 同族(核验动作带副作用)。维护者可否决。
Generated by Claude Code
补一个今天的活体样本(来自 #5155 / PR #5385 的开发过程,不改本单范围)。
本 issue 说的是
check:authorable-surface --check会重写锚点。我这边触发它的不是 check,是一次普通的依赖构建:在一个刚从origin/main切出的干净 worktree 里跑pnpm --workspace-concurrency=2 --filter "@objectstack/cli^..." build(闭包里含
@objectstack/spec,于是gen:schema跑了),之后git status就多出一个我从未打开过的文件:M packages/spec/authorable-surface.base.json 1 file changed, 1 insertion(+), 110 deletions(-)diff 的两头都值得记:
baseRev从1c3da1f6f0899f7299d4b28294e049b0b75d398d前进到2f6516eb91bcc6546830c2c1f5a4fa8711016b57(即我切branch时的 origin/main HEAD);- 同一次写入删掉 110 个 key(
ui/ComponentAnimation:ariaDescribedBy/ariaLabel/enter/exit等一整片)。
也就是说本 issue 担心的「静默推进删除门的锚点」不是理论上的:方向、条数都对得上,而且触发面比
check:更宽 —— 任何一个 PR 只要在自己的验证流程里构建过 spec(或任何以 spec 为传递依赖的包),再习惯性git add -A,这 110 条删除就随 diff 一起进了那个 PR。我这次是在git status里逐行读了才git checkout --掉的;换成一个只看自己改了哪些文件的 agent,它会认为这是构建噪音。对本 issue 的处置没有意见(修法归你们裁),只是把「构建也会触发」和「一次触发 = 110 条删除」这两个数补上。与 #5370 的触发面不同:那条是 merge 驱动未 commit 时倒退回旧 merge-base,这条是普通构建向前推进。
Generated by Claude Code
同族实例,经普通依赖构建路径复现(不是
--check,也不是 os-regen 指示),挂在这条上不另开单。在
cc5b048a0(origin/main)切出的干净 worktree 里,为了跑一个包的测试而构建其依赖:pnpm --workspace-concurrency=2 --filter "@objectstack/service-automation^..." buildpackages/spec的 build 脚本第一步就是pnpm gen:schema,于是packages/spec/authorable-surface.base.json被改写:baseRev从168f60f1前进到cc5b048a0,并补进 3 个键(system/EmailServiceConfig:appName/:defaultTemplateContext/:queueDelivery)。两点想说清:
- 改写后的内容本身是自洽的——那 3 个键是
168f60f1之后才落到 main 的,锚点按定义只是某个旧 commit 的快照,少这 3 个键是正常的、不是 stale。所以这里的问题不是锚点内容错,而是一次与 spec 无关的构建动作会推进删除门的锚点,与本单标题说的「任何无关 PR 都能因此静默推进删除门的锚点」是同一件事,只是触发路径从check:authorable-surface --check换成了build(依赖链上的包,构建者根本没在动 spec)。 - 这条路径对并发 agent 特别不友好:它出现在
git status里的时机是「我刚 build 完准备提交我的测试改动」,一个git add -A就把锚点推进夹带进一个文档/测试 PR。我这次是在git diff逐行自查时发现并git checkout --掉的(os-dev 派发词/定义可加一行:测试假引擎的 delete() 必须路由 assertEngineDeleteDispatch —— 同一门禁一日两红(#5173、#5192) #5197 的 PR 里不含此文件),但这依赖人眼。
即门禁锚点目前对「构建即改写」是无防护的,建议修法覆盖 build 路径而不只是
--check路径。
Generated by Claude Code
- 改写后的内容本身是自洽的——那 3 个键是
- added a commit that references this issue
on Aug 6, 2026 认领:PM 循环第 1 轮(spec-tooling 车道,C 包 #5163)
会话:session_01559M8FVm6W6vDLABL3jvdW
分支:claude/issue-5358-check-authorable-surface-readonly
Worktree:objectstack-issue-5358
域:domain:spec-tooling
文件面:packages/spec/scripts/build-schemas.ts(锚点写入分支)、packages/spec/package.json(如需新增显式--update-base入口)、新增自证测试(packages/spec/scripts/**)、.changeset/*.md。⛔ 不碰packages/spec/src/**与authorable-surface.base.json的锚点内容本身(修法若需一次显式重锚,须在 PR 正文单独说明)。(越界即停,报告说明)串行声明:#5370 / #4663 / #4659 同文件族,本轮不派,待本单落地后按 dev 的必答项回答重新定价。
Generated by Claude Code
验收:ACCEPT → PR #5807(已转 ready 入队)
前提两半分开核验的结论采信:
--check半边在派单前已被 main 上的改动修掉(dev 如实标注对应 pin 是「不变量钉子」而非修复证据,没有包装成红);build/gen:schema半边成立并即本次所修 —— 锚点唯一写入路径收窄为显式gen:authorable-surface-base(--update-base),构建/检查一律不写、滞后只打 ℹ️ 并点名显式命令。复核确认的三个关键点:
- 真实性语义零改动,且显式模式在删除门裁决之后才写 —— 有测试证明它无法把基线推过未证明的删除(封死「显式命令变洗白通道」);离线时同样无能为力(spec 的 #4650 删除闸门在「按 SHA 钉住的消费者构建」里无法锚定 origin/main,硬失败 —— cloud 的镜像构建与 pin bump 全线卡死 #5235 规则存活)。
regen-artifacts.mjs的gen字段刻意不改,避免 merge 驱动在 MERGE 态指示用户跑重锚命令(即 os-regen 驱动指示的gen:schema在 merge 未 commit 时运行,会把 authorable-surface 锚点倒退回旧 merge-base —— 生成器写入、门全绿、静默撤销 main 的锚点推进 #5370 重演)—— 注释写清了取舍。- 全部指向该文件的处方字符串一轮翻完(旧
gen:schema处方零残留),check:generated新增EXPLICIT_GENERATORS类并要求gatedBy必须是账本已声明的门。
CI 25 项零红;
authorable-surface.base.json与 origin/main 逐字节一致,无一次性重锚。开放问题(SURFACE_BASE_DESCRIPTION那句 "Written only bygen:schema" 现为欠描述)按 dev 建议采纳:本 PR 保持逐字不变(它是锚点规范形式的一部分,改它必须连带重锚),下一次真实重锚时在同一个被 review 的 diff 里带上更正 —— 源码注释已留此指引,无需另立单。锚点族重新定价(按必答项回答)
- os-regen 驱动指示的
gen:schema在 merge 未 commit 时运行,会把 authorable-surface 锚点倒退回旧 merge-base —— 生成器写入、门全绿、静默撤销 main 的锚点推进 #5370:大幅变简单但不撤单 —— 根因(resolveSurfaceBase()在 MERGE 态解析出旧分叉点)未动,只是入口收窄为显式命令。范围收窄见该单新评论;串行在本 PR 落地后。 authorable-surface.json在 main 上不是gen:schema的输出 —— 基线手编的字节级实锤(#4650 加固建议) #4663 / build-schemas.ts 检查 (b) 用叶名匹配 conversion surface —— 无关簇的.type就能让一个 tombstone 冒充「已登记迁移」 #4659:不受影响,原价保留,同文件串行在后。
Generated by Claude Code
- added a commit that references this issue
on Aug 8, 2026
从 #4912(PR #5339)的 os-regen 同步中记录的观察,由该单 dev 发现并如实上报而非自行处置。未认领。
现象
packages/spec/scripts/build-schemas.ts每一次运行都会重新锚定packages/spec/authorable-surface.base.json,包括--check模式。实测:在干净工作树上跑pnpm --filter @objectstack/spec check:authorable-surface,该文件被改写。两个独立的后果:
1. 一个「检查」写工作区(与 #4723 同类)
check:*应当是只读判定。这条与 #4723(check:docs第一步gen:schema改工作区,#4711 的残洞)是同一类缺陷的另一处实例:核验动作带副作用,于是「跑一次门禁」和「产生一次改动」无法分辨,本地跑完门禁再git add -A就会把它捎带进任何 PR。2. 更要紧的:删除门的锚点可以被无关 PR 静默推进
authorable-surface.base.json的作用是给删除门(ADR-0078 完整性闸门一族)提供基线。它自己的描述写着该文件「written only from a git-resolved baseline — never from the build that is being checked」。但既然任何一次 build/check 都会重锚,那么:一个与可授权面完全无关的 PR(比如 #5339,一个只改文档渲染器、
packages/spec/src/**零改动的 PR)只要在本地跑过门禁并提交了工作区,就会把baseRev从1c3da1f推进到c89d18c,连带把 110 个ui/ComponentAnimation族的键从记录中抹掉 —— 而那正是 #4988/#5321 刚刚退役的那批。锚点一旦推进,删除门就看不见那次退役了,而且两种状态门禁都判绿,没有任何信号。PR #5339 的 dev 正是察觉到这一点后刻意把该文件保持在 main 的字节并上报,没有自行决定。CI 不受影响(CI 从不提交这次重写),风险面完全在本地工作流。
为什么值得修
这是「声明与强制不符」的门禁版本:文件自称只从 git 解析的基线写入,实际每次构建都重写;门禁自称核验,实际改工作区。而它守护的恰恰是退役是否被如实记录——一个可以被无关改动静默推进的锚点,等于这道门在最需要它的时候可以被无声关掉。
建议修法(未验证,留给分诊)
--check模式只读:比对而不写;要重锚必须显式(--update-base之类),与check:docs的第一步是gen:schema—— 修好 #4711 之后,「检查改工作区」仍从这里漏进来 #4723 的修法同形。packages/spec/src/**真的变了时才发生,并在 PR 里作为可见的改动出现(而不是任何人跑一次门禁就捎带)。check:authorable-surface后git diff --exit-code必须为 0。关联
check:docs的第一步是gen:schema—— 修好 #4711 之后,「检查改工作区」仍从这里漏进来 #4723(check:docs第一步gen:schema改工作区)—— 同类,同在 C 包 C 包 program card:协议工具链/门禁车道(domain:spec-tooling)—— 整包移交待接收 #5163 门禁组authorable-surface.json在 main 上不是gen:schema的输出 —— 基线手编的字节级实锤(#4650 加固建议) #4663(authorable-surface.json字节级 round-trip 门)—— 相邻文件、相邻门Record<string, any>[],抹掉已声明键 —— #4001 战役每个 open 分类站点都会复发 #4912 / PR fix(spec): gen:docs 保留 passthrough 对象的已声明键 + 开放性标记,不再塌缩成 Record (#4912) #5339(发现处)、ui/ 五个交互配置文件(22 个 z.object 站点)没有任何承载键:实测「无授权门」,按 ADR-0049 定去留 #4988 / feat(spec)!: 退役 ui/ 五个没有承载键的交互配置文件 —— touch/dnd/keyboard/animation/offline (#4988) #5321(被抹掉的那 110 个键的退役)⛔ 注意:本单不是说 #5339 做错了什么 —— 它刻意保住了 main 的字节并上报,处置正确。