Skip to content

fix(spec): --update-base 只许锚点前移,MERGE 态与倒退一律拒绝 (#5370) - #5851

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5370-update-base-merge-guard
Aug 6, 2026
Merged

fix(spec): --update-base 只许锚点前移,MERGE 态与倒退一律拒绝 (#5370)#5851
baozhoutao merged 1 commit into
mainfrom
claude/issue-5370-update-base-merge-guard

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5370

前提复核(先证伪,再实现)

按重新定价的要求,先在 origin/main(77adf29)上核对 #5807(a3a884d)的落地形态,再证明根因仍然存在:

EVIDENCE exit=0 anchorBefore=9ce056a879ef anchorAfter=c26ac407eeda
         older=c26ac407eeda mainTip=e4d113dded9e forkedKeyStillAnchored=false

锚点从 origin/main 上的 9ce056a(领先)被写回分支的旧分叉点 c26ac40,data/Object:label 从锚点里消失,退出码 0,无任何门禁报警。前提成立,范围按重新定价执行。

改动

两条拒绝,都只作用于 --update-base,gen:schema 与任何构建的行为完全不变。

1. MERGE 态拒绝(生成之前)

MERGE_HEAD 存在即拒绝,位置与既有的 --check --update-base 互斥拒绝并列,在读取任何 schema 之前退出。

路径经 git rev-parse --git-path MERGE_HEAD 解析而不是手拼 .git/MERGE_HEAD:AGENTS.md §11 要求每个 agent 都在 linked worktree 里干活,而那里 .git文件,真正的 MERGE_HEAD 在主仓 .git/worktrees/ 下的每-worktree 目录里 —— 手拼路径会让这条守卫恰好在仓库要求使用的检出形态下失效。实测本 PR 自己的 worktree:

git-path in this LINKED worktree: /home/user/objectstack/.git/worktrees/objectstack-issue-5370/MERGE_HEAD
.git is a: FILE

处方与 regen-artifacts.mjs 的注释是同一句话的两端:先 commit merge,再重锚。

2. 锚点单调性(写入之前)

只有当已提交的 baseRev 是新解析 rev 的祖先时才写;两者相同时仍走原有的 "nothing to re-anchor"。

放在写入点而不是脚本开头,有两个理由:要写入的 rev 到那里才确定;以及要拒绝的是写入而不是这次运行 —— 一次本来就没东西可写的 --update-base 不该被一个 rev 比较变成失败。删除门先裁决、锚点后写的既有次序也因此保持不变。

祖先关系判不出来时,拒绝而不是猜

merge-base --is-ancestor三种答案读:0 是祖先,1 不是祖先,其余(带 fatal:128、超时的 null)是 git 拒绝回答。塞进 &&/|| 链里第三种会塌进第二种,错误就变成了裁决(cloud#1116 付过的学费)。这里 fail closed。

shallow 检出多出第四种读法,而且是不对称的:截断只会丢失可达性,不会凭空造出可达性,所以 0 在任何检出下都成立;而 shallow 下的 1 什么都不说明 —— #5358 自己的首次 CI 就是被这个假 1 判红的。因此 shallow 只用来决定否定答案算不算数,绝不用来丢弃一个肯定答案(否则每一次 shallow 下本该合法的重锚都会被诬告成倒退)。

这条不是纸面推演:本容器的检出就是 shallow,锚点的 baseRev 正躺在 .git/shallow 里当自己的 grafted root。真实跑一次:

❌ --update-base cannot establish which way authorable-surface.base.json would move (#5370).

   committed baseRev: 5acb93add664
   resolved baseline: 77adf297fd21   (merge base of HEAD with origin/main)

   This is a shallow checkout: history is truncated, so `merge-base --is-ancestor` reports
   "not an ancestor" about commits that plainly are one — 5acb93add664 is very likely one of
   them (a `--depth=1` fetch grafts it in as its own root, unreachable from origin/main).

代价是明说的:shallow 检出里一次本来合法的重锚也会被拒,提示给出 git fetch --unshallow origin 的出路。gen:authorable-surface-basecheck-generated.ts 里属 EXPLICIT_GENERATORS(手动专用,--fix 不会跑),CI 无任何作业调用它,所以这条 fail-closed 不影响流水线。

测试

build-schemas-check-mode.test.ts#5358 块旁新增 #5370 describe,沿用同一套真 git 沙箱(refs/remotes/origin/main 是真 ref)。夹具:三个线性上游 commit,锚点镜像中间那个(authentic、是 origin/main 祖先、合法滞后),HEAD 分叉在最早那个并把 main 的改动逐字节做了一遍 —— 于是 merge 无冲突且不 stage 任何东西,git status 保持全空,「这次运行没写东西」就能直接用空 status 断言,没有 merge 噪声要减。

四个用例:

  1. 真 MERGE 态(git merge --no-commit --no-ff,MERGE_HEAD 实存):拒绝、退出 1、输出不含 Generating JSON Schemas(即生成之前)、锚点字节不变、工作树干净;同一夹具下普通构建不被拒(守卫足够窄,驱动的 gen:schema 处方照常);随后按处方 git commit 完成 merge 再重锚,写入成功且 baseRev 前移到 mainTip —— 拒绝有可用的出路,不只是一条规矩。
  2. 无 merge 的倒退:同一夹具直接跑,报 BACKWARDS 并写零。
  3. shallow 下的否定答案不可信:把 .git/shallow 截在 mainTip,此时移动其实是前移,断言拒绝理由是截断而不是 BACKWARDS。
  4. 把 origin/main 倒回锚点之前:这正是重新定价里字面写的那个夹具,它到不了单调性守卫 —— verifyCommittedSurfaceBase 先一步以「NOT an ancestor of origin/main」判红,那是另一个事实、另一条处方。用例把这个次序钉住并注明:单调性守卫要拦的是两个 rev 都 authentic 的那种倒退,这恰好也是别的门看不见它的原因。

反向验证(方向先预测,再运行)

预测:1/2/3 转红(#5807 落地版根本没有这两条拒绝,会照写不误),4 保持绿(它钉的是既有次序)。把 build-schemas.ts 换回 #5807 落地版、保留新测试后实跑:

 × refuses while a merge is uncommitted — before generating anything — and the commit-first remedy works
 × refuses a re-anchor whose new rev is an ancestor of the committed one, and writes nothing
 × refuses rather than guesses when a NEGATIVE ancestry answer cannot be trusted — a shallow checkout
 Tests  3 failed | 28 passed (31)

与预测一致。#5358 的既有 7 个用例(plain build clean / --check clean / 前移重锚 / already-current no-op / offline powerless / 删除门次序等)全程绿。

实跑输出

pnpm --filter @objectstack/spec test        → Test Files 319 passed | Tests 8169 passed (319)
pnpm --filter @objectstack/spec typecheck   → tsc --noEmit 通过;check:test-typecheck: OK
vitest run scripts/build-schemas-check-mode.test.ts → Tests 31 passed (31)
node scripts/check-nul-bytes.mjs            → OK (5699 tracked text files, no raw control bytes)
pnpm --filter @objectstack/spec check:authorable-surface → exit 0,工作树无改动

Changeset

#5807 的先例给 @objectstack/spec 一个 patch changeset(同一文件类、同一命令的行为变化):.changeset/authorable-surface-anchor-monotonic.md

范围外


Generated by Claude Code

#5358 把重锚收窄成显式模式,回答了「何时可写」,没有回答「从哪儿写」——基线
仍是 merge-base(HEAD, origin/main),而它并不总是领先于已提交的锚点。

merge 停在未 commit 时,HEAD 还是合并前的分支 tip,merge-base 因此落在分支的
旧分叉点而不是被合入的 main tip,重锚就把 baseRev 倒退回去。#5312 同步接力实
测:1c3da1f 回退到 5aae790,#5321 刚退役的 109 键整批回到锚点。

倒退后的文件依然 authentic——旧 rev 是真的 origin/main 祖先、键与该 commit
的 surface 逐行一致——所以 verifyCommittedSurfaceBase、check:authorable-surface、
pre-commit 的 os-regen 守卫前后全绿,唯一痕迹是 diff 里一次反向 baseRev 移动,
形状与 #4650 的攻击无异,而它是生成器自己写的。

两条拒绝,都只作用于 --update-base:

- MERGE 态:MERGE_HEAD 存在(经 git rev-parse --git-path 解析,linked worktree
  下 .git 是文件,手拼路径会让守卫失效)即在生成 1600 份 schema 之前拒绝,并给出
  处方——先 commit merge,再重锚。与 regen-artifacts.mjs 的
  "Re-anchor after the merge is committed, or not at all" 同一句话的两端。
- 单调性:仅当已提交 baseRev 是新解析 rev 的祖先时才写;键一致仍走原有的
  "nothing to re-anchor"。

无法判定祖先关系时同样拒绝而不猜:merge-base --is-ancestor 按 0 / 1 / 其他
(带 fatal 的 128)读成三种答案;shallow 检出下的 1 判为不可用(截断会让 git 对
明明是祖先的 commit 报「不是祖先」),而 0 在任何检出下都成立——截断只会丢失
可达性,不会凭空造出可达性。

gen:schema 与任何构建不受影响:#5358 之后它们本就不写这个文件。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 8:08am

Request Review

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 6, 2026 08:27
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit cfed092 Aug 6, 2026
25 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5370-update-base-merge-guard branch August 6, 2026 08:40
baozhoutao pushed a commit that referenced this pull request Aug 6, 2026
`api/discovery.zod.ts` gained `CapabilityDescriptor` on main (#5851 line),
so the index's api/discovery row and both totals moved: 1608 -> 1609.

Worth noting what this merge demonstrates. The frontmatter count and the
Total row moved TOGETHER, without anyone touching either, because both are
`reduce` over the rows they head — the disagreement this page shipped for
months (133 in the frontmatter, 169 in the nav table) has no way to recur.
And the drift was caught by `check:docs`, which could not see this file at
all before #4759.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01559M8FVm6W6vDLABL3jvdW
qq9340100 pushed a commit that referenced this pull request Aug 6, 2026
#6000(#4723:check:docs 不再以 gen:schema 开头,并在 check-generated 的 GATED 表
里声明 `readsSchemaTree`),S2 的门禁构成改动叠在它之上而不是并排。

冲突一处,packages/spec/scripts/check-generated.ts 的 GATED 表:两侧都改了
`check:docs` 与 `check:api-surface` 两行。取并集 —— 保留 #6000 的
`readsSchemaTree: 'check:authorable-surface'`(它声明的是 check:docs 依赖
check:authorable-surface 先生成 json-schema/ 树,与分片无关),同时保留本分支把
artifact 标签从 `api-surface.json` 改成 `api-surface/`。

生成物按 os-regen 纪律处理:merge 提交先落地(MERGE 态跑 gen:schema 会被 #5851 守卫
拒绝),随后整体重生成并补跑 gen:openapi(#5371)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant