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
Second relay merge: #5300 / #5304 / #5306 / #5308 / #5318 / #5326 / #5327. Textually clean, but the os-regen driver defers generated artifacts rather than text-merging them, so `json-schema.manifest.json` again came out holding this branch's pre-merge side — this time still listing `ui/EmbedConfig` and `ui/NotificationAction`, both retired by #5300. Reset the deferred artifacts to `origin/main`, rebuilt from the merged tree, regenerated wholesale. Post-regen assertions (a silent one-side drop is exactly what this catches): api-surface delta vs `origin/main` is exactly this PR's four additions and ZERO removals; manifest delta is one addition (`ui/ViewItemWire`) and zero removals; every sibling retirement stays removed (`ui/EmbedConfig`, `ui/NotificationAction`, `system/HttpServerConfig`, `ui/Animation`, `ui/ZIndex`) and every sibling addition stays present (`FilterArray` ×7, `EmailProvider` ×2). `check:authorable-surface` (+ its #5304 `.base.json` anchor) is green and the anchor file is byte-identical to `origin/main` — not hand-edited. `metadata-form-zod-reconciliation.test.ts` co-edited with #5280/#5318 and merged SEMANTICALLY, not by taking a side: #5318 rewrote the docblock, imports, helpers and test bodies, while this PR's only edit is `unwrap`'s `pipe` case, so the two did not overlap textually — but they do interact, and in the direction that matters. #5318's `isRetiredAt` / `authorableKeysOf` both route through `unwrap`/`keysOf`, and `view`'s root is now a `z.preprocess` pipe. Measured both ways: without this PR's #4488-style fix `unwrap(view root)` resolves to `transform` and `keysOf` returns NULL, so #5318's brand-new tombstone assertions would be VACUOUS on `view` (and the pre-existing key-bearing assertion would fail outright); with it, 89 keys. Both PRs' assertions are live on every type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
三处冲突全部语义合并,无机械取边: - `ui/index.ts` barrel:保留 #5300 为 notification.zod / sharing.zod 写的两段 「幸存导出」说明,同时落实本单五个退役模块的 export 删除。 - `migrations/registry.ts`:step17 rationale 与 semantic[] 两处冲突都是**双方各自 追加**,两边全留。rationale 里把 #5015 的段落排在前、本单排在后,并把本单开头 从「最后一个不需要改源码的退役」改写为「同类的第二个」—— #5015 落地后那句 独占措辞已不成立。semantic[] 两个条目并存。 - 台账:双方各删各的行,**两边的删除都生效**(#5300 删 sharing/notification 两行, 本单删五行交互行);`no door` 小结改写为同时反映两次退役,并记下这一类九个条目 里已有七个在同一个发布窗口内由「退役」而非「收紧」闭合。 按 AGENTS.md §9 刷新构建状态(frozen-lockfile install + 重建),再按 os-regen 四步 把九条生成路径整体重算。⛔ 未手改 #5304 新引入的 authorable-surface.base.json 锚点 —— 该文件只由 gen:schema 从 git 解析出的基线写入。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
#5304 引入的树内锚点。keys 因兄弟 PR 落地而漂移,gen:schema 据此把 baseRev 从 26e1029(#5300)推进到当前 merge base 1c3da1f,并整体重写 keys。 ⛔ 未手改一个字节:该文件只由 gen:schema 从 git 解析出的基线写入,手改它就是 #4650 要防的那种攻击本身。authenticity 两条均已核:baseRev 是 origin/main 的 祖先(此处即等于 origin/main),且 keys 与该 commit 的 surface 逐行一致 —— 以 check:authorable-surface 转绿为准,而不是要求 baseRev == 任何特定值。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
…objectstack-ai#5306) * feat(spec): declare FilterArray as input-only authoring sugar (objectstack-ai#5285) `FilterArray` was a name with no definition. Three READMEs, `llms.txt`, four skills, the query-adapter docs and this package's own react-blocks prop table all taught authors to write it, while the protocol never declared it anywhere. Declare it in `data/filter.zod.ts`, beside the operator vocabulary it is built from and the sink it lowers through: - `FilterArray` + `FilterArrayComparison` / `FilterArrayGroup` / `FilterArrayList` — the three shapes the measured producers emit - `FilterArraySchema` — the Zod authoring gate - `FilterArrayOperator` — canonical spellings, derived from `AST_OPERATOR_MAP` rather than restated, so it cannot drift from the lowering (objectstack-ai#3948) - `FILTER_ARRAY_LOGIC_KEYWORDS` / `FilterArrayLogicKeyword` Input-only: lowered to a `FilterCondition` at the single sink `parseFilterAST` on arrival. The storage/wire contract is unchanged — a query's `where` is a `FilterCondition` and deliberately does NOT accept the array dialect, pinned as a negative test so a future widening of the protocol face fails loudly. `AST_OPERATOR_MAP` keeps its literal key set via `satisfies` so the operator type can be derived; the two runtime lookups go through one `astOperatorLowering` helper. Behaviour identical. Step 1 of objectstack-ai#5158's ruling C. No engine or driver changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(spec): regenerate os-regen artifacts against main @ 04fab5e (objectstack-ai#5285) Serial-landing sync for PR objectstack-ai#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 — objectstack-ai#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: - objectstack-ai#5293: HttpServerConfig / HttpServerConfigInput / HttpServerConfigSchema - objectstack-ai#5289: Animation / AnimationSchema / ZIndex / ZIndexSchema - objectstack-ai#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 objectstack-ai#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 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ack-ai#5074) (objectstack-ai#5319) * feat(spec)!: split ViewItemSchema into an authoring gate and a wire variant (objectstack-ai#5074) `ViewItemSchema` carried two contracts at once: the authoring surface `defineViewItem()` and Studio's view-create form parse, AND member 1 of the `ViewMetadataSchema` union that `saveMetaItem` validates every persisted `view` body against. The wire role needed Studio's round-trip keys through, so the shape stayed open — and `defineViewItem({ …, confg: {…} })` parsed clean, handing back a ViewItem with no view configuration at all. Per the maintainer's ruling (option A, 2026-08-04): - `ViewItemSchema` is strict on both arms — the authoring gate. - `ViewItemWireSchema` is the `.strip()` wire variant and is member 1 of `ViewMetadataSchema`, with `isPinned` / `sortOrder` DECLARED on it instead of surviving because nobody closed the member. Both are built from one `viewItemArmShape()`, so the two postures cannot drift into two transcriptions (a `discriminatedUnion` cannot be `.extend()`ed). The scope addendum's hard requirement was recursive-effective openness, which a posture flip cannot deliver: `.strip()` re-opens a member's TOP level only, so the console-decorated NESTED blocks were still reached at full strictness. `stripViewConsoleDecorations` (+ the declared `VIEW_CONSOLE_ROW_DECORATIONS` vocabulary) removes them at the wire door before the union runs — the write-path mirror of `stripReadDecorations`. That let both blocked sites close: - `ViewFilterRuleSchema` — objectstack-ai#5114's hotfix was explicitly provisional; retired. - `ListView.sort[]` — 批 18's revert (objectstack-ai#5070); the `direction → order` alias (objectstack-ai#4721, a silently REVERSED sort) comes back with it. `id` is still not declared anywhere: it is a React list key, and declaring it would teach an AI author to emit a UUID (批 18 Q1, rejected on record). Two gate walkers went silent on `view` under a preprocess-rooted registration — the exact blind spot objectstack-ai#4488 already fixed in `check-liveness.mts` — and are fixed the same way here. A gate that stops covering a type is worse than one that fails. Fixes objectstack-ai#5074 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(spec): regenerate baselines after merging origin/main (objectstack-ai#5074) The os-regen merge driver defers generated artifacts rather than text-merging them, so `json-schema.manifest.json` came out of the merge holding this branch's pre-merge side — which still listed `system/HttpServerConfig`, `ui/Animation` and `ui/ZIndex`, all retired by the objectstack-ai#5289/objectstack-ai#5293 chain. Reset the deferred artifacts to `origin/main`, rebuilt, and regenerated wholesale. Asserted after regenerating, because a silent one-side drop is exactly what this step exists to catch: the api-surface delta vs `origin/main` is exactly this PR's four additions and ZERO removals; the manifest delta is one addition (`ui/ViewItemWire`) and zero removals; the sibling retirements (`HttpServerConfigSchema`, `ui/Animation`, `ui/ZIndex`) all stay removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(spec): regenerate baselines after merging origin/main (spec 车道接力) Second relay merge: objectstack-ai#5300 / objectstack-ai#5304 / objectstack-ai#5306 / objectstack-ai#5308 / objectstack-ai#5318 / objectstack-ai#5326 / objectstack-ai#5327. Textually clean, but the os-regen driver defers generated artifacts rather than text-merging them, so `json-schema.manifest.json` again came out holding this branch's pre-merge side — this time still listing `ui/EmbedConfig` and `ui/NotificationAction`, both retired by objectstack-ai#5300. Reset the deferred artifacts to `origin/main`, rebuilt from the merged tree, regenerated wholesale. Post-regen assertions (a silent one-side drop is exactly what this catches): api-surface delta vs `origin/main` is exactly this PR's four additions and ZERO removals; manifest delta is one addition (`ui/ViewItemWire`) and zero removals; every sibling retirement stays removed (`ui/EmbedConfig`, `ui/NotificationAction`, `system/HttpServerConfig`, `ui/Animation`, `ui/ZIndex`) and every sibling addition stays present (`FilterArray` ×7, `EmailProvider` ×2). `check:authorable-surface` (+ its objectstack-ai#5304 `.base.json` anchor) is green and the anchor file is byte-identical to `origin/main` — not hand-edited. `metadata-form-zod-reconciliation.test.ts` co-edited with objectstack-ai#5280/objectstack-ai#5318 and merged SEMANTICALLY, not by taking a side: objectstack-ai#5318 rewrote the docblock, imports, helpers and test bodies, while this PR's only edit is `unwrap`'s `pipe` case, so the two did not overlap textually — but they do interact, and in the direction that matters. objectstack-ai#5318's `isRetiredAt` / `authorableKeysOf` both route through `unwrap`/`keysOf`, and `view`'s root is now a `z.preprocess` pipe. Measured both ways: without this PR's objectstack-ai#4488-style fix `unwrap(view root)` resolves to `transform` and `keysOf` returns NULL, so objectstack-ai#5318's brand-new tombstone assertions would be VACUOUS on `view` (and the pre-existing key-bearing assertion would fail outright); with it, 89 keys. Both PRs' assertions are live on every type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB --------- Co-authored-by: Claude <noreply@anthropic.com>
…fline (objectstack-ai#4988) (objectstack-ai#5321) * feat(spec)!: 退役 ui/ 五个没有承载键的交互配置文件 (objectstack-ai#4988) touch / dnd / keyboard / animation / offline 五个模块声明了完整的交互配置词表 (22 个 z.object 站点、32 个 def、64 个导出名),而协议里**没有任何承载键** —— 没有 schema 声明过 `touch:` / `dnd:` / `keyboard:` / `animation:` / `offline:` 槽位,所以没有元数据文档能到达这些形状,也从来没有 parse。 三条独立测量在 origin/main 上重跑,每条的阳性对照都在同一次运行内通过: 静态(除 ui/index.ts barrel 外零 importer)、图可达(25 roots / 4742 nodes, 21 个具名对象形状全不可达,Page/Webhook/StateMachine 全部 direct,注入合成 承载键后 21 个全部翻转)、调用点(三仓零 parse)。 真正的缺陷在**文档侧**:authorable-surface.json 躺着 109 个键, content/docs/references/ui/{touch,dnd,keyboard,animation,offline}.mdx 把它们 渲染成授权表 —— AI 作者照 dnd.mdx 往页面组件写 dnd: 块,会被 PageComponentSchema 以未知键拒绝。这是 PD#10 的宣传运行时不提供的能力, 不是 strictness 缺口:收紧只会精确校验一个没人够得到的槽位(objectstack-ai#4583)。 按 spec-property-retirement 路线 3(nothing parses it → 既不墓碑也不 D2), 与 objectstack-ai#4834 / objectstack-ai#4938 同形;注册 ADR-0087 D3 SemanticMigration `ui-interaction-config-family-retired`,并扩写 protocol-17 步骤的 rationale。⚠️ ui/animation.zod.ts 与 objectstack-ai#5021 退役的 theme animation 块是两张面:不同文件、 不同 def、不同 manifest 条目。那一张有承载键、走墓碑;这一张没有、走删除。 Fixes objectstack-ai#4988 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(spec): 末段同步 —— os-regen 基线整体重算于 origin/main (objectstack-ai#5293 已并入) 按 .gitattributes 的 os-regen 四步互保执行:merge origin/main → 把全部 os-regen 路径 checkout 回 origin/main → 整体重算 → 断言兄弟条目仍在。 互保断言(重算后的基线上实测): - 本单 objectstack-ai#4988:32 个 ui/ 交互 def 不在 manifest,109 个 authorable 键为 0; - 兄弟 objectstack-ai#4938 / PR objectstack-ai#5293:system/HttpServerConfig 三张表全部仍为删除态; - 兄弟 objectstack-ai#5021 / PR objectstack-ai#5289:6 个 theme 族 [RETIRED] 标记全部仍在 (ui/Theme:animation、ui/Theme:zIndex、ui/Typography:fontSize/fontWeight/ letterSpacing/lineHeight)—— 与本单删除的 ui/ComponentAnimation 一族是 两张互不相干的面,名字撞车但条目分离,已逐条核对; - 阳性对照:ui/Page 的 authorable 键仍在,ui/ResponsiveConfig 仍被 emit。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(spec): gen:schema 推进 authorable-surface 锚点至 1c3da1f objectstack-ai#5304 引入的树内锚点。keys 因兄弟 PR 落地而漂移,gen:schema 据此把 baseRev 从 26e1029(objectstack-ai#5300)推进到当前 merge base 1c3da1f,并整体重写 keys。 ⛔ 未手改一个字节:该文件只由 gen:schema 从 git 解析出的基线写入,手改它就是 objectstack-ai#4650 要防的那种攻击本身。authenticity 两条均已核:baseRev 是 origin/main 的 祖先(此处即等于 origin/main),且 keys 与该 commit 的 surface 逐行一致 —— 以 check:authorable-surface 转绿为准,而不是要求 baseRev == 任何特定值。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…摆纠偏、飞行中重叠、预期红停放 (objectstack-ai#5441) (objectstack-ai#5501) * docs(pm-dispatch,os-dev): 串行接力一夜沉淀的六条缺口补进 SKILL —— 接力模式、锚点措辞、裁决传播扫描、停摆纠偏、飞行中重叠、预期红停放 (objectstack-ai#5441) 2026-08-04/05 夜 spec 车道以串行接力连落 10 个 PR(objectstack-ai#5304 → objectstack-ai#5365),六个情形是 现行 SKILL 没有覆盖、靠现场即兴的,各有实付学费。按 issue 注明的落点章节逐条插入: - 第 7 步「入队与落地」新增平行小节「串行接力」:每棒一整圈(auto-merge 由 PM 挂、 dev 永不碰,ready 与 auto-merge 顺序不可反且每棒各走一次)、相邻棒同文件交接语义 而非文本(objectstack-ai#5318/objectstack-ai#5319 实例)、两棒散文互锁由 PM 指派分工(objectstack-ai#5323↔objectstack-ai#5365、objectstack-ai#5335)。 - 「入队与落地 A」新增锚点断言措辞:authenticity = baseRev 是 origin/main 祖先 且 keys 与该 commit 逐行一致;baseRev 允许滞后;⛔ 不得要求 baseRev == merge-base (那会教唆手改锚点,即 objectstack-ai#4650 攻击自身)。四步序的第 3 步补上「先 commit merge」, 并引用 objectstack-ai#5370 / objectstack-ai#5371 两个新陷阱(仅引用,不实现)。 - 第 5 步派发词 + 第 7 步 review 新增「裁决传播 = 全仓 pin 扫描」:翻 pin 一轮翻完 且必须保留承重(objectstack-ai#5322 裁决、objectstack-ai#5365 的 REST 层漏翻)。 - 第 6 步 Collect 的 subagent 半边新增停摆纠偏:watcher 永不触发,中途状态即停摆 信号,第三次视为不可靠改走接手协议。生产端半边同步落到 os-dev.md 资源纪律第 6 条。 - 第 5 步 same-day churn 新增姊妹段「飞行中范围重叠」:每轮读 origin/main 时对每个 在飞 dispatch 做相交判断,相交即预警(objectstack-ai#5322 × objectstack-ai#5335 实例)。 - 「入队与落地 B」新增依赖 PR 的预期红停放:draft 停放 + 签名级预期红清单 + 解除 条件,新签名才是真问题(objectstack-ai#5365 的 REST 红即由此识别)。 仅改 `.claude/` 内部 agent 协议文本,不发布任何包;六条落点之外未动任何段落。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE * docs(pm-dispatch): 停摆纠偏第三条的交叉引用改指「step 5」—— 接手协议在第 5 步,不在第 6 步之下 自查发现的方向错误:「Handing off an interrupted dev」小节是第 5 步的子节 (SKILL.md:839),而新增段落写在第 6 步(:923),原文写「below」会把读者指向 第 6 步之后的 Cloud mode 段。同段里对 cloud-mode ~2h 阈值的「below」是对的,保留。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE * docs(pm-dispatch): 飞行中重叠一段的时间戳改用可核验值 —— objectstack-ai#5335 的 merged_at 是 23:49:44Z,不是 00:0xZ issue objectstack-ai#5441 正文写「objectstack-ai#5335 在 00:0xZ 合入」,核 GitHub API 的 merged_at 与 main 上 squash 提交的 committer date,两者一致给出 2026-08-04T23:49:44Z。改写成「起飞后 32 分钟合入(merged_at 2026-08-04T23:49:44Z)」,把不可核验的钟点换成可核验的 时间差 + 权威字段;起飞时刻 23:17Z 沿用 issue 的记述(无仓内产物可核,且非承重)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE --------- Co-authored-by: Claude <noreply@anthropic.com>
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