feat(spec): 可作者化 key 的默认值进入门禁 —— authorable-defaults 指纹比对 (#4666) - #6360
Conversation
`authorable-surface/` 只比对 key 的集合,同一个 key 的 `.default()` 变了对它 完全不可见 —— 而默认值决定「作者没写这个 key 时会发生什么」,翻转它会**静默** 改变所有已部署元数据的行为。#4661 实测:把 `maxRetries` 默认值改一个字符, `check:authorable-surface` 全绿。 新增 `packages/spec/authorable-defaults/`(按 category 分片,与 authorable-surface 同构)记录每个有默认值的可作者化 key 的规范化指纹;比对基线优先取 origin/main merge base(本 commit 改不动的锚点),其次取树内已提交的记录。已存在 key 的默认值 发生 变更 / 新增 / 删除 一律红,除非在 `DEFAULT_CHANGES_BY_MAJOR` 显式声明。 ⛔ 约束(`.min()` / `.max()`)**不**纳入指纹 —— 维护者对 #4666 的裁决(方向 B): 约束收紧是**硬拒**,响亮可诊断;只有默认值翻转是静默的。该排除是结构性的:指纹 只读发出的 JSON Schema 的 `default` 一个字段。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
四个沙箱(顶层 + 三个 box)此前只种 authorable-surface/。默认值门禁每次调用 都会跑,缺少记录时 `--check` 报「产物缺失」,六条 fixture 因此在它们真正要测的 删除 / 改名判定之前就红了。补种后沙箱重新是真实树的缩影。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
`ui/ElementRecordPickerProps:multiple` 在 #6281(SDUI props enforce-or-remove) 中被 `retiredKey()` 墓碑化,默认值随之消失。门禁按设计对此保持沉默 —— 墓碑属于 check (b) 的 RETIRED_KEYS_BY_MAJOR 通道,在这里再要一次声明就是同一次退役被记两遍。 记录相应少一行。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
PM 验收:ACCEPT — 已 ready + auto-merge。 先答你标出的 HONEST GAP:两条尾巴都已收敛为 维护者裁决(方向 B)的执行:结构性排除,而不是纪律性排除指纹只读发出的 JSON Schema 的一个字段 我要的那个数字,拿到了,而且它正好证成了裁决
方向 A 会把约束摘要压在全部 7943 个 key 上,而且压在 #4535 §1 已经在抱怨过度收集的那个文件里。B 把新数据放进新文件、让吵的那个文件一动不动。成本侧的账现在是可复核的数字,不是印象。 那条落空的预测,产出了本单最重要的东西你预测 sabotage 会红,理由是「基线取自本 commit 改不动的锚点」。方向对,机制错:在这条分支上 merge base 早于该产物,所以锚点回落到 顺着这条错追下去才是价值所在:同时手改 schema 与记录(逐字节 结论干净利落: 三处判断我要点名认可
对 issue 正文的更正,已采纳
Generated by Claude Code |
Fixes #4666
按维护者 2026-08-07 裁决执行方向 B —— 只对默认值做指纹,约束不纳入。
前提复核(当前
origin/main)盲区原样存在。
build-schemas.ts今天被移动过三次,正文行号已过期,但结论未变:可作者化面门禁只比较 key 的集合(currentKeys只记 key 到 isRetired 的映射),.default()对它完全不可见。#4661 的 sabotage 在本分支上原样复现(见下表 P1)。一处正文已过期的细节:
shared/retry-policy.zod.ts的 shape 并不发出shared/RetryPolicydef,它被./automation和./system两个 barrel 复用,所以改一个字符实际移动的是两个 def key。门禁按 def key 逐个报,这是对的 —— 声明也必须逐个写。指纹捕获什么,怎么规范化
新增
packages/spec/authorable-defaults/{category}.json(按 category 分片,与authorable-surface/同构,所以两个 PR 改不同 category 不共享文件)。每行形如"{def}:{key} = {规范化 JSON}",例如shared/CorsConfig:enabled = true,只收有默认值的 key。指纹读的是发出的 JSON Schema 的
default字段,不是 Zod 内部节点。理由:那是已发布契约真正对作者和 IDE 承诺的东西,也是 key 集合本身的来源;读_zod.def会把门禁绑死在「值由.default()还是.prefault()还是.meta()产生」上,而作者体验到的只有结果。规范化四条,每条都对着一类会误报的 churn:
default一个字段.describe()、title结构性地进不来最后一条就是方向 B 的边界,它是结构性的而不是靠自觉:门禁根本不读
minimum/maximum,所以收紧一个 bound 连意外移动指纹都做不到。这条边界另有单测钉住(两个只差 bound 和文案的 shape 必须指纹相同),免得后人「顺手改进」时把它侵蚀掉。只记有默认值的 key,不记
(none)行:7943 个可作者化 key 里 1326 个有默认值,只记这些让产物只有六分之一大;存在性并没丢 —— 新增默认值让一行出现,删除让一行消失,两者都被判为变更。基线取哪儿:这才是能不能防住的关键
upstream—— merge base 上的authorable-defaults/,本 commit 改不动。这是真正承重的那个。in-tree—— 树内已提交的记录。弱:本 commit 拥有这些字节。只在 upstream 读不到时用(本 ratchet 落地前 fork 的分支,以及 spec 的 #4650 删除闸门在「按 SHA 钉住的消费者构建」里无法锚定 origin/main,硬失败 —— cloud 的镜像构建与 pin bump 全线卡死 #5235 描述的离线构建 —— 那些是已合并的不可变树)。authorable-defaults/的树在--check下照样红。用了哪个锚点每次都打印。另一条纪律:判定跑在写产物之前。所以
gen:schema不会把未声明的变更吸收进它自己充当证据的那份记录里 —— 实测见下表。「已确认的变更」出口怎么保持诚实
出口是
DEFAULT_CHANGES_BY_MAJOR(scripts/lib/default-changes.ts),四条性质每次运行都从声明者控制不了的来源重新推导:from对基线(git 里的 merge base),to对本次构建发出的值。都不是填表人能设的字段。to必须等于活值。默认值再动一次、或者被回滚,声明立刻红。这就是「不再为真就死掉」的那条,也是 check (b2) 的镜像。to必须等于后一跳的from,跳不过中间真的发过的值。外加:被消费的声明每次都整条打印
reason。已确认的默认值翻转在构建日志里自报家门,而不是静默通过。本 PR 落地时表是空的 —— 这份空本身就是门禁的自证:表里什么都没有而
check:authorable-surface在origin/main上是绿的,等于说树里每个默认值都对得上记录。反向验证(预测先写后跑)
预测写在实现之前。P1 里 sabotage =
retryPolicyShape()的.default(0)改成.default(3),一个字符。1326 default(s) unchanged0 → 3automation/RetryPolicy:maxRetries: 0 → 3+system/RetryPolicy:maxRetries: 0 → 3gen:schema= 3📌 accepted,另一个仍被拦declared to end at 3, but this build emits 0P2 预测落空,而且这是本 PR 最有价值的发现。 预测的理由是「基线来自本 commit 拥有不了的锚点,所以重生成洗不掉」。在本分支上这个理由是错的 —— 本分支的 merge base 还没有这份产物,用的是
in-tree锚点,本 commit 恰恰拥有它。方向对了,机理是另一个:gen:schema直接拒绝写(判定在写之前),所以记录压根没被重生成。顺着这条继续挖,得到了真正该知道的那件事:把 schema 和记录同时手改成一致(把记录里的
= 0字节手术成= 3),in-tree锚点下门禁全绿。这就是 #4650 的攻击形状,原样长在新产物上。于是补了一次隔离 probe(独立 bare clone + linked worktree,
origin/main指向本 PR 的 commit,refs 与共享 checkout 完全隔离),把同一次「协同手改」重放一遍:结论:
in-tree锚点挡得住忘记,挡不住手改;upstream锚点两个都挡得住。 这个窗口在本 PR 合并的那一刻自动关闭(此后每条分支的 merge base 都带着这份产物),离线构建则是已合并的不可变树 —— #5235 对同一处境给的理由在这里逐字适用。这一点写进了代码注释,不留给下一个读者自己踩。另一条没预测到但很说明问题的:rebase 到新 main 后记录少了一行,
ui/ElementRecordPickerProps:multiple。查下来是 #6281(SDUI props enforce-or-remove)把它retiredKey()墓碑化了,默认值随墓碑消失。门禁按设计保持沉默 —— 墓碑归 check (b) 的RETIRED_KEYS_BY_MAJOR管,在这里再要一次声明就是同一次退役记两遍。写下来的排除规则在落地几小时内就被一次真实的上游事件验证了一遍。防空转的钉子
scripts/authorable-defaults.test.ts,23 条,全部关于区分度(这个输入出结论、那个不出),而不是「函数能跑」。把diffAuthorableDefaults改成无条件返回空数组之后,catches the #4661 sabotage: one character, one finding, named立刻红 —— 空转钉不住的正是 #4642 的那类病。产物成本(A-vs-B 决策的代价侧,要数字不要印象)
authorable-defaults/(新增,13 个 shard)authorable-surface/(既有)方向 B 的代价全部落在一个新文件上,
authorable-surface/一个字节没动 —— #4535 §1 抱怨的那个文件不因本 PR 变大变吵。方向 A 会把 7943 个 key 全部加上约束摘要,落点正是那个文件。登记点
.gitattributes(merge=os-regen)、scripts/regen-artifacts.mjs、packages/spec/scripts/check-generated.ts的 artifact 描述。没有新增gen:/check:脚本 —— 门禁跑在build-schemas.ts内,check:authorable-surface已经覆盖它,check:generated --reconcile-only双向对账通过。build-schemas-check-mode.test.ts的四个沙箱补种了这份产物:默认值门禁每次调用都跑,缺记录时六条 fixture 会在它们真正要测的删除 / 改名判定之前就红。changeset
不需要 —— 走
skip-changeset(已打标)。packages/spec的files白名单是[dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json]:authorable-defaults和scripts/都不在其中。本 PR 不含任何packages/spec/src/**改动(sabotage 是本地临时改动,已复原并逐条核过),所以dist/*.d.ts不变、api-surface不变,发布物零变化。验证
pnpm --filter @objectstack/spec test—— 333 files / 8523 tests 全绿pnpm --filter @objectstack/spec typecheck—— 绿pnpm --filter @objectstack/spec check:authorable-surface—— 绿(rebase 到当前 main 之后)pnpm --filter @objectstack/spec check:generated --reconcile-only—— 18 check: + 13 gen: 全部归类node scripts/check-nul-bytes.mjs—— 绿;新文件另做了越过门禁扫描面的自查,无命中Generated by Claude Code