Skip to content

fix(spec): 锚点漂移提示按实测方向措辞,不再把「领先」说成 trails … by 0 key(s) (#5847) - #6309

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5847-anchor-drift-direction
Aug 7, 2026
Merged

fix(spec): 锚点漂移提示按实测方向措辞,不再把「领先」说成 trails … by 0 key(s) (#5847)#6309
os-zhuang merged 1 commit into
mainfrom
claude/issue-5847-anchor-drift-direction

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5847

前提复核(立单读数已失效,结论:前提仍然成立)

立单引用的 origin/main 77adf29 第 1313–1323 行已被两次改动移走,我在当前
origin/main(4dd9cbd15)上重新定位:

立单时 现在
build-schemas.ts:1313–1323 build-schemas.ts:1817–1830

中间落地的 PR #6200(#5371,lib/json-schema-out-dir.ts + clearOwnedOutputs())
与 PR #6256(#5898,重写检查 (c) 的墓碑老化时钟)都没有碰这条代码路径 —— 它们改的
删除门禁,本单改的是锚点漂移提示。缺陷本体逐字未变:

const behind = anchor.keys.filter((k) => !recorded.has(k)).length;

并且在沙箱夹具里实测复现了原文所述的那一行(见下方「反向验证」):

ℹ️  authorable-surface.base.json trails the baseline at b20f3c44e610 by 0 key(s)

缺陷

这句提示对两个方向只有一套措辞,而 behind 算的是 解析基线的键 ∖ 锚点的键
只有当锚点是两者中较旧的一方时,这个数才等于差距。当已提交的锚点更新时,
解析基线的键是锚点键的子集,behind 恒为 0,整句退化成「方向说反 + 唯一能
反驳它的数字被清零」。

这不是角落状态:#5370 已经把两条进入路径写清楚了(未 commit 的 merge 中途构建;
分支分叉早于锚点推进后又带入较新锚点),而且#5370--update-base 在同一
状态下会拒绝并把方向解释对
—— 于是我们自己的两条消息在描述同一件事时互相矛盾。

方向判定:复用,而不是新造

派发单要求「若 #5370 的判定在该点真的可用则优先复用」。可用,并且已复用。

#5370merge-base --is-ancestor 的三值解读(0 = 祖先 / 1 = 非祖先 / 其余 =
git 拒绝作答)外加 shallow 那第四读(截断只会丢失可达性、绝不会凭空造出
可达性 ⇒ 0 在任何检出下都算数,1 只在 history 可走时才算数)抽成共享的
probeAncestry(),再由 relateAnchorToBaseline() 给出 behind / ahead /
unordered

一个方向存在两套独立判定,正是它们日后各说各话的原因 —— 而本单就是那次分歧的账单,
所以没有再造第二套「锚点键 ⊇ 解析基线键」的子集推断。

三条消息都同时报告两侧键差(N key(s) only that baseline has, M only the anchor has),
因为任一侧都可能为空,成对出现才有信息量;没有任何一条会再打印
by 0 key(s) 式的零信息断言。

unordered 不是兜底而是诚实答案,三种可达来源:shallow 检出(CI 自己的
typecheck job 与所有 agent 容器都是
)、git 拒绝作答、两个 authentic 的
origin/main 祖先分处一次 merge 的两侧。在这些情形下声称方向,就是本单要消灭的
缺陷换个状态重演。

反向验证(预测在跑之前写下,并且有一条没中)

新测试 预测 实测
1. 锚点 BEHIND ⇒ 说 trails,并写出两个 rev 与两侧差 RED RED
2. 锚点 AHEAD ⇒ 说 AHEAD,绝不 trails-by-zero RED RED
3. shallow 检出 预测仍判定为 AHEAD(反向探针能证明) 实测判定为「无方向」 预测错
4. 真正无序的一对 rev ⇒ 不声称方向 RED RED
既有 51 条(含 #5358trails the baseline at#5370 全部四条) 保持 GREEN GREEN

回退 build-schemas.tsorigin/main、保留新测试后的实跑:

❯ scripts/build-schemas-check-mode.test.ts (55 tests | 4 failed)
  × says the anchor TRAILS when it is the older of the two, and names both revs and both deltas
  × says the anchor is AHEAD when it is the newer of the two — never "trails … by 0 key(s)"
  × claims NO direction in a shallow checkout, and names truncation as the reason
  × claims NO direction when the two revs are genuinely unordered
 Tests  4 failed | 51 passed (55)

那条没中的预测(照实报告)

我预测 shallow 夹具下方向仍然可测:正向探针 tip → older 因截断不可用,但反向
探针 older → tip 是走 parent 一步、不需要被截断的那段 history,exit 0 在任何检出
下都算数 ⇒ 判定为 ahead

实测两处都不对:

  1. 截断先动的不是探针,而是 merge-base 本身resolveSurfaceBase()
    git merge-base HEAD origin/main 在 grafted history 下直接失败,于是基线回退到
    origin/main 的 tip
    (脚本自己会打印 (shallow history — using origin/main tip … as the baseline anchor))。被比较的一对因此是 锚点@tip vs 基线@mainTip,
    根本不是分叉点。
  2. 这一对的祖先关系恰好是 grafted history 答不了的那个:mainTip 是它自己的 shallow
    root,从它往下走去够到 tip 正是被剪掉的那段;反向则是普通否定。两个探针都给不出
    可用答案 ⇒ unordered

旧代码在这里打印的是 trails the baseline at (mainTip) by 1 key(s) —— 对未截断
history 而言恰好是真的。这正是重点:它从来不是测出来的,而是假设出来的,而同一个
假设在隔壁一个夹具里打印了与事实完全相反的话。所以第 3 条测试改成钉「shallow 下不声称
方向,并说出截断这个理由」,与 #5370 对写入所采取的处置一致。

这条 miss 让 unordered 分支拿到了两个夹具(shallow 与真分叉),其中 shallow 那个才是
CI 每天都在跑的环境 —— 比我原来的预测更值得钉。

退出码与写入文件:实测逐字节相同,不是断言

两个内容完全相同、commit 日期固定(因而 SHA 相同)的沙箱,分别跑
origin/main 版与本 PR 版的 build-schemas.ts,在 behind / ahead 两个状态下对比:

状态 exit code git status --porcelain -uno 锚点 sha256 authorable-surface/ json-schema.manifest/ json-schema/ 整棵树
behind 0 = 0 空 = 空 07036bf2… = 07036bf2… 2b33f6b8… = 2b33f6b8… 7c1a3738… = 7c1a3738… eae4cc78… = eae4cc78…
ahead 0 = 0 空 = 空 c60c5967… = c60c5967… 2b33f6b8… = 2b33f6b8… 7c1a3738… = 7c1a3738… eae4cc78… = eae4cc78…
IDENTICAL (exit code + git status + every written artifact): YES   (behind)
IDENTICAL (exit code + git status + every written artifact): YES   (ahead)

唯一的差异是那句提示本身:

behind  OLD: ℹ️  authorable-surface.base.json trails the baseline at 957a099b093c by 2 key(s)
        NEW: ℹ️  authorable-surface.base.json trails the baseline at 957a099b093c: it mirrors the older
                cdcfbec5b75b, and they differ by 2 key(s) only that baseline has

ahead   OLD: ℹ️  authorable-surface.base.json trails the baseline at cdcfbec5b75b by 0 key(s)
        NEW: ℹ️  authorable-surface.base.json is AHEAD of the baseline this build resolved: it mirrors
                (锚点 rev), a DESCENDANT of the merge base cdcfbec5b75b that HEAD resolves to, and
                they differ by 1 key(s) only the anchor has

(cdcfbec5b75b 就是分叉点 older;旧行把「锚点是它的后代」说成了「锚点 trails 它」,
并且把唯一能拆穿这句话的数字打成了 0。)

json-schema/packages/spec 已发布 files 白名单里的目录,上表那一列因此同时也是
「本 PR 不改变任何已发布产物」的直接证据。

测试

pnpm --filter @objectstack/spec exec vitest run scripts/build-schemas-check-mode.test.ts
  Test Files  1 passed (1)
       Tests  55 passed (55)          # 51 既有 + 4 新增

pnpm --filter @objectstack/spec typecheck
  ✓ tsc --noEmit
  ✓ check:test-typecheck: OK

pnpm lint                              # 全仓 eslint,无输出即通过
pnpm check:nul-bytes                   # OK (5959 tracked text files)
check:doc-authoring / adr-anchors / error-code-casing / route-envelope /
org-identifier / role-word / slot-lookup / query-options-erasure  → 全部 OK

Changeset:skip-changeset(实测,非默认假设)

改动只有 packages/spec/scripts/ 下两个文件。packages/spec 的已发布
files 白名单是
["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"]
—— 不含 scripts/#6256 今天之所以合法地拿了 @objectstack/spec patch,是因为它的
registry JSDoc 会进 dist/*.d.ts;本 PR 不导出任何东西,distjson-schema 都在
上面的对比表里被实测为逐字节不变 ⇒ 什么都不发布 ⇒ 取 skip-changeset 标签,
不写 changeset(⛔ 空 frontmatter changeset 是被门禁禁止的)。

未触碰

packages/spec/src/**/*.zod.ts、strictness ledger、content/docs/releases/ 均未改动;
门禁的判定一处未动。


🤖 Generated with Claude Code

https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5


Generated by Claude Code

`gen:schema` 在锚点与本次解析基线不一致时只有一句固定措辞:
「trails the baseline at <rev> by <n> key(s)」,其中 n 算的是
`解析基线的键 ∖ 锚点的键`。只有当锚点是两者中较旧的一方时,这个数才等于差距。
当已提交的锚点更新时,解析基线的键是锚点键的子集,n 恒为 0,整句退化成
「trails the baseline at 9ce056a879ef by 0 key(s)」—— 方向说反,且唯一能反驳它
的那个数字被清零。

方向改为「实测」而不是「假设」:把 #5370 对 `merge-base --is-ancestor` 三值
(外加 shallow 第四读)的解读抽成共享的 `probeAncestry`,再由
`relateAnchorToBaseline` 给出 behind / ahead / unordered。重锚门禁与本提示因此
共用同一个判定 —— 同一个方向存在两套独立判定,正是它们日后各说各话的原因,
而本单就是那次分歧的账单。

三条消息都同时报告两侧的键差,因为任一侧都可能为空,成对出现才有信息量。
`unordered` 不是兜底而是诚实答案:shallow 检出(CI 的 typecheck job 与所有 agent
容器都是)、git 拒绝作答、以及两个 authentic 祖先分处一次 merge 两侧 —— 在这些
情形下声称方向,就是本单要消灭的缺陷换个状态重演。

门禁本身分毫未动:退出码、写入的文件、判定全部保持原样,实测在 behind /
ahead 两个状态下 exit code、`git status`、锚点字节、两个 ratchet 目录与整棵
~1600 个文件的 json-schema/ 产物树逐字节相同。

Fixes #5847

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

vercel Bot commented Aug 7, 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 7, 2026 1:34pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

@os-zhuang os-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 7, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 7, 2026 13:52
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

PM 验收:ACCEPT — 已 ready + auto-merge。

CI 复核:27 个 check 全部 completed、零 failure。ESLint success(13:37:52)、TypeScript Type Check success(13:45:53)、Test Core 1..3/3 + 聚合 success、Check Changeset success(13:39:19)、Spec property liveness success。判定前确认 Test Core 在名单中(名单在场守卫)。文件面 2 个,均在 packages/spec/scripts/;未碰 *.zod.ts、严格度台账、content/docs/releases/

派发令要求的那一格:复用而非新造,做到了

我要求「若 #5370 的判定在该点真的可用则优先复用」。你抽出了共享的 probeAncestry()(三值 + shallow 第四读),assertAnchorMovesForward() 成为消费者且输出逐字节不变#5370 的四条测试原样通过。

更重要的是两个消费者对 unknown 的处置故意不同,并写进了注释:门禁 fail-closed(拒绝写入),提示只是不声称方向。这是对的 —— 共享的是判定,不是处置;把处置也统一了才会出新问题。

那条落空的预测,是本单最有价值的产出

你预测 shallow 下方向仍可测(反向探针走一步 parent)。实测两处都不对,而第二处是真正的发现:

  1. 截断先动的不是探针,是 merge-base 本身 —— resolveSurfaceBase() 在 grafted history 下失败,基线回退到 origin/main 的 tip,于是被比较的一对根本不是分叉点;
  2. 这一对的祖先关系恰好是 grafted history 答不了的。

旧代码在这里打印的 trails … by 1 key(s) 对未截断的 history 恰好是真的 —— 它从来不是测出来的,是假设出来的,同一个假设在隔壁夹具里打印了与事实完全相反的话。把测试 3 改成钉「shallow 下不声称方向并说出理由」,比原计划的 pin 更值 —— shallow 正是 CI 自己的 typecheck job 与每个 agent 容器的日常环境

「预测落空 → 得到比原计划更好的 pin」,这是反向验证该有的样子。

字节相同是演示的,不是断言

两个内容相同、commit 日期固定的沙箱,对比 exit code、git status、锚点 sha256、authorable-surface/json-schema.manifest/ 以及整棵 ~1600 文件的 json-schema/ 树,behind / ahead 两态皆 IDENTICAL: YES。因为 json-schema/ 在已发布 files 白名单内,这张表同时就是「不改变任何已发布产物」的直接证据 —— 也正是 skip-changeset 的依据。这比写一句「行为未变」强得多。

我欠你一个更正,并已修正自己的纪律

是我误判了一次。 Check Changeset 首次判红时我按今日 7 次的模式直接重投,没有先核标签是否在位 —— 而当时 skip-changeset 确实不在,那一红是真红,我的重投是浪费的。我自己写下的纪律第一步就是「先核标签在位」,被 7 次重复磨掉了。

由此还查明一件我此前说错的事,记在这里:labeled 事件触发的那次 run 会把 Check Changeset skipped(pr-automation.yml 的 action guard,本 PR 的 run 31183466212 可见)。所以标签落地不会让它自愈,必须手动重投一次。我先前对另一单说过「403 = labeled 事件已自动重跑」,那是错的 —— 那次的绿来自 dev 自己的重投。已更正,后续派发令按更正后的说法写。

这也反过来加强了 #6260 的论点:真缺标签与时序竞态产生的红一模一样,唯一能区分的就是那一步「先看标签」,而它正处在被重复磨掉的压力之下 —— 我今天就是样本。


Generated by Claude Code

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gen:schema 在锚点「领先于」解析基线时打印「trails the baseline … by 0 key(s)」——方向说反,信息为零

2 participants