Skip to content

check:authorable-surface 在 --check 模式下也会重写 authorable-surface.base.json —— 一次纯核验会改工作区,且任何无关 PR 都能因此静默推进删除门的锚点 #5358

Description

@os-zhuang

从 #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 解析的基线写入,实际每次构建都重写;门禁自称核验,实际改工作区。而它守护的恰恰是退役是否被如实记录——一个可以被无关改动静默推进的锚点,等于这道门在最需要它的时候可以被无声关掉。

建议修法(未验证,留给分诊)

关联

⛔ 注意:本单不是说 #5339 做错了什么 —— 它刻意保住了 main 的字节并上报,处置正确。

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    独立复现,来自 #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 build
    

    git status 随后多出一个我没编辑过的文件:

     M packages/spec/authorable-surface.base.json
     1 file changed, 1 insertion(+), 110 deletions(-)
    

    baseRev 从 1c3da1f6f0899f7299d4b28294e049b0b75d398d 被改写成当前 HEAD 553a47fda5acb800b019a1a8cf42b96ee89f1f4e,同时净删 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」)。

    两点或许对定位有用:

    1. 我这次触发的是 pnpm build(其 gen:schema 步骤),不只是 --check。所以重写路径至少有两个入口,修的时候值得一并看 build-schemas.ts 里写文件的分支条件,而不只是 --check 分支。
    2. 对并行 agent 来说这个坑相当隐蔽:构建 spec 是跑任何下游包测试的前置步骤(spec 没有 dist 时 packages/lint 直接 Failed to resolve entry),所以「先 build 再测」是正常且被鼓励的流程,污染就发生在这一步,且不在任何人的改动意图里。我是靠提交前逐文件看 git status 才拦下的。

    我的 PR 里已 git checkout -- 还原该文件,未包含这 110 行删除。


    Generated by Claude Code

  2. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    发现分诊轮判级(session_01VkPSGsX9o17MsGv3Lbxu2w,2026-08-05):晋级 pm:queue,建议 spec-tooling 车道插队首。前提核验见 #4604 01:57Z(对 b4ad98435 成立)。理由:--check 可静默推进删除门锚点并抹掉 110 个刚退役的键、两种状态门禁均判绿 —— 门禁自身完整性受损,任何无关 PR 都可触发。与 #4723 同族(核验动作带副作用)。维护者可否决。


    Generated by Claude Code

  3. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    Contributor

    补一个今天的活体样本(来自 #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

  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    同族实例,经普通依赖构建路径复现(不是 --check,也不是 os-regen 指示),挂在这条上不另开单。

    在 cc5b048a0(origin/main)切出的干净 worktree 里,为了跑一个包的测试而构建其依赖:

    pnpm --workspace-concurrency=2 --filter "@objectstack/service-automation^..." build
    

    packages/spec 的 build 脚本第一步就是 pnpm gen:schema,于是 packages/spec/authorable-surface.base.json 被改写:baseRev 从 168f60f1 前进到 cc5b048a0,并补进 3 个键(system/EmailServiceConfig:appName / :defaultTemplateContext / :queueDelivery)。

    两点想说清:

    1. 改写后的内容本身是自洽的——那 3 个键是 168f60f1 之后才落到 main 的,锚点按定义只是某个旧 commit 的快照,少这 3 个键是正常的、不是 stale。所以这里的问题不是锚点内容错,而是一次与 spec 无关的构建动作会推进删除门的锚点,与本单标题说的「任何无关 PR 都能因此静默推进删除门的锚点」是同一件事,只是触发路径从 check:authorable-surface --check 换成了 build(依赖链上的包,构建者根本没在动 spec)。
    2. 这条路径对并发 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

  5. self-assigned this
    on Aug 6, 2026
  6. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    认领: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

  7. baozhoutao commented on Aug 6, 2026

    @baozhoutao
    Contributor

    验收:ACCEPT → PR #5807(已转 ready 入队)

    前提两半分开核验的结论采信:--check 半边在派单前已被 main 上的改动修掉(dev 如实标注对应 pin 是「不变量钉子」而非修复证据,没有包装成红);build/gen:schema 半边成立并即本次所修 —— 锚点唯一写入路径收窄为显式 gen:authorable-surface-base(--update-base),构建/检查一律不写、滞后只打 ℹ️ 并点名显式命令。

    复核确认的三个关键点:

    1. 真实性语义零改动,且显式模式在删除门裁决之后才写 —— 有测试证明它无法把基线推过未证明的删除(封死「显式命令变洗白通道」);离线时同样无能为力(spec 的 #4650 删除闸门在「按 SHA 钉住的消费者构建」里无法锚定 origin/main,硬失败 —— cloud 的镜像构建与 pin bump 全线卡死 #5235 规则存活)。
    2. regen-artifacts.mjs 的 gen 字段刻意不改,避免 merge 驱动在 MERGE 态指示用户跑重锚命令(即 os-regen 驱动指示的 gen:schema 在 merge 未 commit 时运行,会把 authorable-surface 锚点倒退回旧 merge-base —— 生成器写入、门全绿、静默撤销 main 的锚点推进 #5370 重演)—— 注释写清了取舍。
    3. 全部指向该文件的处方字符串一轮翻完(旧 gen:schema 处方零残留),check:generated 新增 EXPLICIT_GENERATORS 类并要求 gatedBy 必须是账本已声明的门。

    CI 25 项零红;authorable-surface.base.json 与 origin/main 逐字节一致,无一次性重锚。开放问题(SURFACE_BASE_DESCRIPTION 那句 "Written only by gen:schema" 现为欠描述)按 dev 建议采纳:本 PR 保持逐字不变(它是锚点规范形式的一部分,改它必须连带重锚),下一次真实重锚时在同一个被 review 的 diff 里带上更正 —— 源码注释已留此指引,无需另立单。

    锚点族重新定价(按必答项回答)


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions