Skip to content

test(scripts): 把 restatement 从记账升级成断言——每条 README peer 重述逐字钉住自家清单 (#3717) - #3751

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3717-restatement-assertion
Aug 8, 2026
Merged

test(scripts): 把 restatement 从记账升级成断言——每条 README peer 重述逐字钉住自家清单 (#3717)#3751
yinlianghui merged 1 commit into
mainfrom
claude/issue-3717-restatement-assertion

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3717

#3710 裁决 B 是「保留并钉住」两步:先收窄散文,再加一条测试断言每个 README 的 peer 重述与本包 peerDependencies 逐字一致。只落地了前半——ledger 里那句 why 一直写着 "re-verify with a one-line read of the manifest",也就是靠人。本 PR 用一次运行替掉那句话。

做了什么

对每条 restatement 条目:经棘轮用的同一份扫描结果(flaggedClaims)把字面量还原成行号 → 从该行解析 dep 与完整区间 → 读 README 旁边的 packages/NAME/package.json → 要求两个区间相等。今天实比 12 行(11 个 bullet 拼写的 README + react-runtime 那句散文)。

只改一个文件:scripts/__tests__/doc-version-claims.test.ts没有碰任何 README 或清单。KNOWN_CLAIMS 的行改动被压到最小(只给 plugin-chatbot 加了一个字段、共享 why 常量改了一句话),以便 #3715(create-plugin.mdx route B,已按同文件纪律排在本单之后)后续合并干净。

它补的是棘轮结构上看不见的洞

清单键就是被匹配的字面量,而匹配停在第一个版本 token:^18.0.0 || ^19.0.0 这行记下来的键只有 ^18.0.0。所以把清单加宽出第三个 arm(或收掉一个),字面量没变、条目没变、棘轮两个方向全绿,而 README 已经在说错话。#3690 就是这个形状,当时只能靠人普查发现、门禁只能事后记成 stale

反向验证(先预判方向,再跑)

扰动(未提交) 预判 实测
README 第二个 arm ^19.0.0^20.0.0(packages/layout/README.md) 新断言红;棘轮两向仍绿(键未变) 1 failed | 9 passed,红的只有新断言:packages/layout/README.md:20 react: README says "^18.0.0 || ^20.0.0", manifest says "^18.0.0 || ^19.0.0"
改清单侧(packages/auth 的 peer react 加 || ^20.0.0) 同一条断言红——这正是 #3690 的形状 1 failed | 9 passed:packages/auth/README.md:23 react: README says "^18.0.0 || ^19.0.0", manifest says "^18.0.0 || ^19.0.0 || ^20.0.0"
删掉 plugin-chatbot 的 notAPeerRestatement 断言红,报「字面量在、但该行不是 peer 陈述,什么都没比 如预判——证明这个豁免是承重的,不是空操作
notAPeerRestatement 挂到一条活的 peer 条目(layout)上 闭合性测试红:豁免已失效、正在压制真实检查 如预判:the opt-out is stale and is suppressing a real check

两次改动均已 git checkout -- 还原,git status 只剩测试文件一个。第一行是关键:棘轮全绿而新断言单独红,说明补的洞是真的,不是把同一件事测两遍。

两条边界,各有自己的钉子

  • 只判 peer 行(按卡自荐 + 分诊预裁)。plugin-chatbot 重述的是自家 dependencies["@ai-sdk/react"]主版本(v4^4.0.47),既不是 peer 也不逐字,故加 notAPeerRestatement 字段指名跳过并保留 kind: 'restatement'。字段带的是理由字符串而非布尔,豁免和条目一样要写清楚;断言不允许「豁免」与「真被覆盖」并存,所以将来新增一条没有 peer 锚的 restatement 不能靠「解析不到」悄悄溜出覆盖面——它会红到有人把那句话写下来。顺带说明:那一条的 why 里「人工复核」一句故意保留,因为它恰好是机器唯一没覆盖到的地方,留着才是实话。
  • 逐字,但区间内部空白不计packages/react-runtime/README.mdreact >= 18,清单写 >=18——同一个比较符、npm 解析完全相同,这里没有任何漂移。若按字节严格比,只会在一棵什么都没错的树上标红,而能回答这个红的只有两条:为门禁改一处纯装饰的拼写,或把这条判成不覆盖(丢掉真覆盖面)——两条都是白付代价。归一化只丢空白,所以要抓的形状全都活着:主版本变、|| arm 增删、^~、上界消失,一律仍不相等,并且以反向断言钉住(否则一个悄悄长成「连操作符也剥掉」的归一化,会让整条断言变空转而其余测试全绿)。

另外 RANGE_OPENS(区间必须以操作符或数字开头)是承重的:没有它,bullet 形状会把任何以 backtick 标识符开头的列表项吞进来(- \useChat()` returns a stream会被读成 dep =useChat()`),然后清单查不到而报红——为错误的理由报红,会教会所有人忽略这道门禁

验证

pnpm exec vitest run scripts/ --maxWorkers=2   →  20 files / 365 tests passed
                                                  (基线 361,新增 4 条)
pnpm run type-check:scripts                    →  tsc 干净退出
node scripts/check-control-bytes.mjs           →  OK (3691 tracked text files)
pnpm exec eslint <该文件>                       →  0 problems

控制字节另做了超出门禁扫描面的自查(grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]'),无命中。注意本仓的门禁叫 check-control-bytes.mjs,没有 check-nul-bytes.mjs(那是 objectstack 仓的名字)。

无 changeset

scripts/ 测试改动,不属于任何发布包、消费者装不到。与 PR #3711(新建本文件)、PR #3726(文档 + ledger)一致均不带 changeset;只有 PR #3727 因改了已发布清单才写。

越界发现:#3750

全量普查 README 的 Peer Dependencies 区块时发现:28 行 peer 陈述里只有 12 行是 ledger 登记的 react 行(本断言正好覆盖这 12 行),其余 16 行中9 行与自家清单不符,包括一处用户今天就会踩的实错——packages/components/README.md:41tailwindcss ^3.0.0,清单是 ^4.2.1。棘轮看不见它是因为 TOOLCHAIN 里的 \bTailwind\btailwindcss 上没有词边界;本断言看不见它是因为断言由 ledger 驱动。修法要动文档/清单加宽门禁,两头都在本单文件面之外,故据实另立 #3750 未认领,不在本 PR 修

另可对照 #3741(react-runtime 清单侧 &gt;=18 无上界):它一旦被收窄,本断言会立刻红到那份 README 跟上——正是这条断言要买的东西。


Generated by Claude Code

…ifest (#3717)

#3710 ruling B was "keep and pin": narrow the prose, then ADD A TEST asserting each
README's peer restatement equals its own `peerDependencies` verbatim. Only the first
half landed — the `why` strings kept saying "re-verify with a one-line read of the
manifest", i.e. a human. This replaces that sentence with a run.

For every `restatement` entry the assertion resolves the literal back to its line via
the same scan the ratchet judges, parses the dep and the FULL range off that line, reads
the manifest beside the README, and demands the ranges match. 12 comparisons today: 11
bullet-spelled READMEs plus react-runtime's prose sentence.

The hole this closes is structural, not laziness. The inventory key is the matched
literal and a match stops at the FIRST version token, so a key recorded for
`^18.0.0 || ^19.0.0` is only `^18.0.0`. Widen a manifest to a third arm and the literal,
the entry and both directions of the ratchet are untouched and green over a README that
now misstates the range. Reverse-verified in that exact direction: perturbing a README's
SECOND arm fails this assertion alone (1 failed, 9 passed, both ratchet tests green), and
perturbing the manifest instead fails it identically — #3690's shape, which the ratchet
could only record as `stale` after a human noticed.

Two boundaries, each pinned by its own test:

- Peer lines only. plugin-chatbot restates the MAJOR of its own
  dependencies["@ai-sdk/react"], not a peer range, so it carries `notAPeerRestatement`
  and is skipped BY NAME — one rule judges one kind of fact. The opt-out is not an
  escape hatch: deleting it turns the assertion red ("NOTHING was compared"), and
  attaching it to a live peer entry turns the closure test red as a stale opt-out
  suppressing a real check. Both verified.
- Equality is verbatim up to whitespace INSIDE the range, because react-runtime's README
  writes `react >= 18` where its manifest writes `>=18` — the same single comparator, so
  byte-strict equality would demand a cosmetic edit or lost coverage in exchange for
  nothing. Every drift shape survives the normalisation and is pinned negatively.

No changeset: scripts-test-only, same as PR #3711 which added this file.

Out-of-scope finding filed as #3750 — the peer BLOCK is wider than the ledger, and 9 of
its other 16 lines disagree with their manifests, including a live drift
(`components` README says tailwindcss `^3.0.0`, manifest says `^4.2.1`) that neither the
ratchet nor this assertion can see. No README or manifest touched here.

Fixes #3717

Co-Authored-By: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 8, 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)
objectui Ignored Ignored Aug 8, 2026 11:39am

Request Review

Copy link
Copy Markdown
Collaborator Author

补一条零命中纪律的反向验证(派发要求「字面量找不到就该红,但别和现有棘轮重复报」,这是那条的证据):

预判:删掉 packages/mobile/README.md 里整条 react peer 行 → 现有的向下棘轮(「no entry may outlive the claim it excuses」)红;新断言绿(该条按 absent 跳过),同一个缺陷只有一个报告者。

实测:

FAIL  doc version claims - the ratchet > keeps the inventory honest - no entry may outlive the claim it excuses
  KNOWN_CLAIMS names version claims that are no longer in the tree:
    - packages/mobile/README.md :: react` ^18.0.0

Test Files  1 failed (1)
     Tests  1 failed | 9 passed (10)

如预判:红的是棘轮那条,新断言没有跟着报第二遍。这也是断言flaggedClaims 解析行号、而不是自己重新 grep README 的原因——「字面量不在树里」在两个测试里是同一个事实,两边才谈得上分工。代价是那条跳过分支:所以本测试末尾留了空转下限(实比行数 >= 10,今天 12),否则条目被清空或行解析坏掉时,循环会在「什么都没有」上报绿。

git checkout -- 还原,工作树只剩测试文件本身。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 11:48
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 0c28a07 Aug 8, 2026
17 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3717-restatement-assertion branch August 8, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant