fix(spec): #4650 删除闸门改用树内基线锚点,按 SHA 钉住的离线消费者构建不再硬失败 (#5235) - #5304
Conversation
…eeds no network (#5235) `gen:schema` resolved the authorable-surface deletion baseline only out of git — `origin/main`, with a self-heal fetch — and exited 1 when neither worked. That is right for a developer who forgot to fetch and wrong for every build environment with no route to GitHub: image-build stages that COPY a SHA-pinned framework tree into a container, air-gapped builds, forks, historical-tag reproductions. Those trees are immutable and already merged, so the question the gate asks ("what did this commit delete relative to main?") has no subject there — yet it failed them. The baseline is now also committed as `packages/spec/authorable-surface.base.json`: the keys of `authorable-surface.json` at `baseRev`, a commit on origin/main. - origin/main reachable: unchanged. The gate anchors on the merge base exactly as before, and additionally verifies the committed anchor against it — `baseRev` must be an ancestor of origin/main and its keys must BE that commit's baseline. So the environments that can police the anchor do, and a commit cannot edit it to hide a deletion. - origin/main unreachable: the gate anchors on the committed file and the build proceeds. It still runs — a recorded key this build no longer emits is as fatal as before — and only a git-resolved baseline may write the file, so an offline build can never advance the anchor to its own state. No env-var skip: a deletion check that can be switched off is the bypass #4650 closes. With neither anchor available the build still exits 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
CI 红诊断(PM,三个失败 job 同一签名,非 flaky)Build Core / Dogfood 1-3 全部死在 根因:祖先校验不是 shallow-safe。锚点写入时 merge base 为 返工方向(细则由 dev 定,性质不能变):
dev 报告到达后按 REWORK 重派,本条即返工简报。 Generated by Claude Code |
…5235) First CI run on this branch failed every job: `merge-base --is-ancestor` answers "not an ancestor" in a depth-1 checkout, because the anchor's commit is fetched as its own shallow root and there is no history to walk between it and the tip. The ancestry half of the anchor verification therefore rejected a baseRev that demonstrably IS on main, exactly the way the merge-base fallback a few lines above already anticipates for the baseline itself. Ancestry is now judged only where it can be: `rev-parse --is-shallow-repository` gates it, and a shallow run says so and verifies the recorded KEYS alone — the half truncation cannot take away, since the fetched commit's tree is present. A full clone (every dev checkout — where the anchor is regenerated, and where a hand-edit is therefore caught) still checks both. Two tests pin it, using `$GIT_DIR/shallow` to truncate the sandbox repo exactly as `--depth=1` does: a lagging-but-authentic anchor stays green, and a shed line still goes red there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
补充:第一轮 CI 抓到的一个真问题(已修,e80df68)第一轮 CI 全线红,原因不是闸门本身,而是我给锚点加的祖先校验在浅检出里判错: 88b9b2d 确实在 main 上。问题是 CI 是 修法:祖先只在能走历史的地方判( 两条新测试钉住:用
这条也顺带说明了树内锚点的定位:在浅检出里能证的是"锚点没被改",完整克隆里才连"它指向的 commit 属于上游"一起证;而 #4650 的实体判定在这两种环境里走的都还是 merge base,没有变。 Generated by Claude Code Generated by Claude Code |
…line-surface-anchor
…aseline (#5235) `git merge origin/main` brought #5289 / #5293 / #5296 / #5300, three of which change the authorable surface (theme token tombstones, the HttpServerConfig removal, the NotificationAction / EmbedConfig removals). The anchor is written from the merge base, so it moves with it: baseRev 88b9b2d → 26e1029, 8045 → 8016 keys, regenerated by `gen:schema` rather than text-merged — this file is on the os-regen list precisely because a textual merge of it means nothing. Verified line-for-line against `git show 26e1029:packages/spec/authorable-surface.json`, and the siblings' entries survived in it: `ui/Theme:animation [RETIRED]` / `ui/Theme:zIndex [RETIRED]` are carried, HttpServerConfig / NotificationAction / EmbedConfig are gone from every generated witness at once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
已同步 main 并整体重生成(2adc419,入队前)
锚点按设计随 merge base 前移: 断言(a) 兄弟 PR 的条目都在
顺带记一句免得下个人误判:#5293 没有 D2/D3 条目是对的,不是合并丢的——它的变更集写明"零 reader 且没有任何作者面入口",没有作者可迁移,按 ADR-0087 只由 manifest ratchet 与 (b) 锚点机制完好
(c) 布线没被合并吃掉: 门
Generated by Claude Code Generated by Claude Code |
) Serial-landing sync for PR #5306. `git merge origin/main`, then all NINE os-regen paths reset to origin/main and regenerated wholesale from the merged sources (the path list read from the merged tree's `.gitattributes`, not from memory — #5304 added `authorable-surface.base.json` as the ninth). The merge driver had deferred `api-surface.json` and `json-schema.manifest.json`, and git's textual result RESURRECTED symbols three sibling PRs had retired — this branch's pre-merge copies still listed them. Wholesale regeneration removes them again: - #5293: HttpServerConfig / HttpServerConfigInput / HttpServerConfigSchema - #5289: Animation / AnimationSchema / ZIndex / ZIndexSchema - #5300: EmbedConfig / EmbedConfigSchema / NotificationAction / NotificationActionSchema Verified after regeneration: this PR's 7 exports and `data/FilterArray` still present, the FilterArray docs section still carries its describe text, all three siblings' retirements absent from every witness, the #5304 anchor authentic (baseRev an ancestor of origin/main, keys identical line-for-line to that commit's surface), and `check:merge-driver` reconciling 9 paths both ways. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
Fixes #5235
按维护者指示走方向 2:把删除闸门的基线锚点固化成树内产物,同时保住 #4650 的防旁路性质。
问题
resolveSurfaceBase()只从 git 取基线(origin/main+ 自愈 fetch),两条都拿不到就process.exit(1)。对"忘了 fetch 的开发者"这是对的;对一整类根本没有 GitHub 路由的构建环境是错的——cloud 把 frameworkCOPY进 buildx 阶段再构建、气隙构建、fork、按 tag 复现历史版本。这些树是不可变的、已经合并过的,闸门要问的那句"我这个 commit 相对 main 删了什么"在那里没有主语,却照样把整个构建打红。方案
新增提交进树的
packages/spec/authorable-surface.base.json:{ "description": "…", "baseRev": "88b9b2d…(40 hex)", "keys": ["ai/Agent:model", "…"] }keys就是authorable-surface.json在baseRev(一个 origin/main 上的 commit)时的内容。baseRev必须是 origin/main 的祖先,且它记录的 keys 必须就是那个 commit 的基线。防旁路性质为什么还在
想靠改锚点作弊,只有两条路,都被上面那两条校验堵死:
baseRev那个 commit 的基线 → 红。要让它不红,得让一个已经合并的上游 commit 少掉那一行,而 PR 做不到这件事。baseRev指到自己分支上的 commit(那个 commit 的基线确实被改过) → 祖先校验红:它不是 origin/main 的祖先。再加一条写入侧的约束:锚点只能由 git 解析出来的基线写(
gitResolvedAnchor),离线构建永远不写它——否则离线构建就能把锚点推进到"自己现在的样子",等于自己给自己做证。⛔ 没有
SKIP=1之类的环境变量。两种锚点都拿不到时,构建仍然 exit 1(原来那条 test 改写成钉这一半)。刷新故事 / 怎么防漂移
gen:schema(写模式)。它在删除闸门判完之后才写——顺序是有意的:一次因为"没证明的删除"而退出的运行根本走不到写入那一行,所以锚点永远不可能被推过一个它没有放行的删除。baseRev单独变化不触发),所以它的 churn 频率 =authorable-surface.json自己的 churn 频率,不是每个 PR。main上 merge base 就是 HEAD,所以这个文件必然比自己的 surface 落后一个 PR。因此--check只证明它真实(authentic),不要求它最新(current);落后时打一行ℹ️提示,由下一次gen:schema顺手补上。这一点在代码注释和regen-artifacts.mjs的条目里都写清楚了,免得下一个人把它"修"成 fatal 而在 main 上自伤。归属与配套
.gitattributes+scripts/regen-artifacts.mjs,pnpm check:merge-driver双向自检通过)。理由:同一个生成器、同样的 8k 行有序数组、同样的冲突形状,解法永远是"按合并后的树重算",不是文本合并。条目注释里注明了它与邻居的不同点:陈旧不算错。packages/spec/package.json的files不动:authorable-surface.json本来就不在发布清单里(闸门跑在仓库树上,不在 npm tarball 里),锚点跟它保持一致。content/docs/releases/。验证证据
1. RED(修之前,复刻消费者构建形状) — 在没有可解析 git 的环境里跑 spec 构建:
与 issue 正文里 cloud 那条报错逐字一致。
2. GREEN(同一条命令,修之后):
3. 开发检出仍然绿:
pnpm --filter @objectstack/spec check:authorable-surface→ exit 0。4. 反篡改(在真实开发检出里手改锚点,删掉一行) → 红:
5. 测试:
packages/spec/scripts/build-schemas-check-mode.test.ts→Tests 18 passed (18);pnpm --filter @objectstack/spec typecheck→ exit 0;pnpm check:merge-driver→ 全绿。新增 6 条(沿用该文件已有的 sandbox:真 git 仓 + 伪造的
refs/remotes/origin/main,删掉这个 ref 就是消费者构建的忠实模型):离线能构建、离线仍然会红(锚点记着而本次构建产不出的 key)、离线写模式不推进锚点(成功和失败两条路都钉了)、手改锚点被抓、baseRev指向本地 commit 被抓、锚点缺失时--check只报不写而gen:schema会生成。原有那条 "fails LOUDLY when origin/main cannot be resolved" 按新契约改写:它钉的性质(没有任何锚点时不许静默跳过)完好,只是触发条件从"没有 origin/main"收窄成"两种锚点都没有"。
下游
cloud 的 pin bump(cloud#1012 / cloud PR #1091)在这条合并后解锁。
另外一条实测到的下游收益:cloud 已经用"把
.git放进构建上下文 + 在 builder 里git update-ref refs/remotes/origin/main HEAD"绕过这个闸门(cloud#1098 / cloud PR #1106),代价写在它自己的 dockerignore 注释里——COPY objectstack/那层每次都变、缓存全失效、每次部署冷构建约 20 分钟,并把"买回缓存"记成了 cloud#1102。这条合并后那个绕法可以撤掉:锚点在树里,不需要.git,也不需要那个update-ref(顺带一提,它把origin/main指到 HEAD,等于让闸门拿自己比自己——树内锚点比它严格)。cloud 侧的清理不在本 PR 范围内。🤖 Generated with Claude Code
https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
Generated by Claude Code