Skip to content

chore(ci): ADR-0087 台账存量一次性回补审计口 --audit-stock —— 238 条 breaking 逐条过一遍门禁自己的判定逻辑 (#6350) - #6500

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6350-breaking-changeset-backfill-audit
Aug 8, 2026
Merged

chore(ci): ADR-0087 台账存量一次性回补审计口 --audit-stock —— 238 条 breaking 逐条过一遍门禁自己的判定逻辑 (#6350)#6500
os-project-manager merged 1 commit into
mainfrom
claude/issue-6350-breaking-changeset-backfill-audit

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Refs #6350

⚠️ 刻意不写 Fixes 本 PR 落地的是 #6350器械半边(一次性存量审计口),没有动任何一条台账。原因见下「为什么这一版不带回补」—— 抽样那 2 条以及审计新翻出来的另外 49 条,判定全部悬在同一个未回答的口径问题上,而登记决定是维护者的。#6350 需要保持 open 直到那个口径被裁定,否则 51 条待判清单会随 issue 一起消失。

一、落地了什么

scripts/check-adr-0087-registration.mjs 新增 --audit-stock,把 #6148 门禁自己的判定函数(breakingDeclaration / readDisposition / findMigrationPrescription / workspacePackagesAt)对着存量跑一遍。

这里没有第二份逻辑 —— 只是换一批被判对象。这一点是刻意的:审计若自己抄一份判定,它就会和门禁各自漂移,那样得出的清单谁也不敢信。

刻意不接 CI,也不该接。 package.jsoncheck:adr-0087-registration 与 Check Changeset 工作流都是不带旗标地调门禁;--audit-stock 只能手敲。它是 --list 的邻居 —— 算子按需跑的常设审计面 —— 不是第二道门。⛔ 把门禁改成回溯全量比对是 #6129 明确拒绝的那条路(会给每个 PR 加上与其改动无关的历史负担),本 PR 一个字都没往那个方向动:scan() 与 CI 的调用点原样未改。

分桶:238 收窄成人可核的残差

存量 changeset 一条都没有处置标记(从来没有东西问过它们),所以把门禁逐字重放只会把 238 条全报一遍、什么也没说。有信息量的是它本来有资格拿哪条处置:

判据 条数
answered 已带处置标记(门禁上线后才落地的) 3
exempt-unpublished 所有 bump 的包都 private: true,破坏不出门 0
exempt-no-prescription 正文没框出任何改写,catch-all 对它开着 143
RESIDUE 已发布的破坏 + 正文带改写指令 ⇒ 只剩 registered / already-registered,两者都要台账条目 92

残差再叠一条机械事实,把「作者确实登记过」和「从来没人问过」分开:动过这条 changeset 的 commit 里,有没有哪个同时动了台账源?这正是 #6350 手工抽样用的信号,只是算到了全量而不是三个点上。结果:41 条动过台账(标 ~,需核对该条目是否覆盖本次面),51 条一次都没动过(标 !)。

一条踩到的坑,已写进脚本

浅克隆下这个信号会说谎,而且谎得很像真的:嫁接历史里每个 pre-graft 文件都读作「由嫁接 commit 添加」,而那个 commit 携带整棵树、含两个台账源。本容器默认检出就是浅的(104 commit,graft 在 2bc187641,该 commit 5978 个文件),实测 92 条残差里 91 条被报成「作者登记过」—— 条条都是错的,而输出在纸面上完全合理。

所以浅克隆时该信号被抑制,而不是被近似:打印 ? 与一段说明,并告诉算子跑 git fetch --unshallow --filter=blob:none(只读 tree、不拉 blob 就够)。照 #4690 的口径 —— 报不出来就说报不出来,不许拿一个像样的答案顶上。

self-test

新增 S1-S6 钉住分类器四个臂与其间的优先级(unpublished 优先于 prescription;不声明任何包的 changeset 买不到 unpublished 豁免 —— 门禁本身也拒)。

反向验证(事前判定方向:删守卫 ⇒ 恰好 S5 转红):删掉 parsed.bumps.length > 0 守卫后,[].every() 真空为真,空 frontmatter 的 changeset 会被静默判成 exempt。实测恰好 S5 一条转红,方向与事前判定一致。

二、抽样那 3 条:逐条落到结论

先说事实层(穷举 grep 两个 registry,不是抽查):

changeset 台账命中 结论
runtime-httpserver-wrapper-retired.md(#5122) HttpServer 仅 2 处,均为别的条目正文里顺带提到 HttpServerConfig(#4938) 确无条目
record-details-sections-object-form.md(#5611) RecordDetails / hideFields / record:details 两个 registry 各 0 命中 确无条目
data-driver-query-omit-object.md(#5181) DriverQuery 仅 1 处,在 aggregate 别名条目正文里(作为参数变成的类型),不是被登记的面;IDataDriver 的命中全是 findStream 与无关行 确无条目(#6350 存疑的第 3 条,现已确认)

三条都没有覆盖它们的条目。 这一层是可核的事实,#6350 的抽样成立。

判定层见第三节 —— 三条的方向不一致,而且分歧全部来自同一个口径。

三、为什么这一版不带回补(#6350 派单里那条例外条款)

派单写明:「若审计发现的真实漏登记多于寥寥几条,不要闷头批量修 —— 先报数量和形状」。实测 51 条(去掉正文自带 tombstone / D2 conversion 的 17 条后仍有 34 条),不是 2 条。数量已经改变了这件事的性质。

更要紧的是:51 条(含抽样那 2 条)的判定全部悬在同一个未回答的问题上。 台账里五条同族条目(data-driver-find-stream-retired #4484storage-service-list-retired #5540actor-user-roles-to-positions #6011spec-type-alias-input-suffix-retired #6083driver-aggregate-undeclared-key-aliases-removed #6321)各自写下了两条触发口径,措辞几乎逐字相同:

  • T1 —— 无类型的 JS 消费者根本没有被强制的渠道;
  • T2 —— 强制渠道(tsc)对「缺了什么」很响亮,对「改成什么」却是哑的,升级指南是唯一承载后半句的渠道。

按 T1/T2 的字面读法,HttpServer 退役(T2:TypeError 不会说「改成直接注册适配器实例」)与 RecordDetails.sections(#5611 当时 properties 根本不被 parse ⇒ T1;#5068 接上门后 safeParse 报 expected object 也不说「每个 ID 换成 { fields: [...] }」⇒ T2)都该登记;而 IDataDriver 那条反而最弱 —— 它的运行期行为零变化,无类型 JS 调用方多传 object: 会被静默忽略、什么都不用做,TS 侧 TS2353 又逐字点名了要删的键(对「改成什么」并不哑)。

同一条口径,三条给出三个方向,而按字面读法要补的是 ~34 条而不是 2 条。 台账是被声明的 forever artifact(每次发布 CI 全链重放,条目喂 migrate meta / spec-changes.json / 已发布的升级指南),一条错加的条目会直接发到消费者手里且很难撤回。这个不对称让「先问」明显优于「先猜」,故本 PR 不带回补,问题连同三轴分析回给维护者裁。

四、顺带发现,已另立单:#6497

#6350 抽样的第 1 条(runtime-httpserver-wrapper-retired.md),门禁的探测器今天根本够不着它。 它的迁移说明写成 ## Migration 标题下的无箭头两列表,而 findMigrationPrescription() 两条分支都要求出现箭头 —— 于是它落进 exempt-no-prescription,压根进不了残差面(它是人眼发现的,不是门禁)。

这是前向缺口而不只是存量问题:今后任何 PR 把迁移说明写成表格,就能合法写下 not-required (no-migration-prescription) 并被批准 —— #6419 那个形状的第三种拼写。实测存量 13 条带此形状,其中 4 条声明为 breaking 且正靠这条盲区拿豁免。

按派单口径,门禁本身的改动另立一单,不在本 PR 内落地#6497

五、验证(真实输出)

$ node scripts/check-adr-0087-registration.mjs --self-test
✓ check-adr-0087-registration --self-test: 73 assertions over real temp git repos (real scan()/assertInputs() path)

$ node scripts/check-adr-0087-registration.mjs          # 门禁本体,路径未改
✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (0 non-breaking changeset(s) seen).

$ node scripts/check-nul-bytes.mjs
check-nul-bytes: OK (scanned 6106 tracked text file(s); skipped 5 binary, 1 non-regular; no raw ASCII control bytes).

$ npx eslint scripts/check-adr-0087-registration.mjs
(exit 0)

$ node scripts/check-adr-0087-registration.mjs --audit-stock | head -12
ADR-0087 stock backfill audit (#6350) -- 1372 changeset(s) in stock, 238 declared breaking.

ALREADY ANSWERED -- carries an adr-0087 marker (landed after the gate): 3
    .changeset/adr-0122-phase-2-bare-name-flip.md  [registered spec-type-alias-input-suffix-retired]
    .changeset/aggregate-driver-query-and-alias-retirement.md  [registered driver-aggregate-undeclared-key-aliases-removed]
    .changeset/ui-widget-i18n-family-retired.md  [registered ui-widget-i18n-family-retired]

EXEMPT (unpublished) -- every bumped package is private; nothing ships: 0

EXEMPT (no-migration-prescription) -- the body frames no rewrite: 143

检测器自身的标定:审计在没有被告知的情况下,把 tidy-donkeys-yawn.md(#6048,唯一已确证的那次漏登记)独立地标成了 !。它同时说明 ! 不等于漏登记 —— 该面早已由 PR #6138 另行补登,今天正确的处置是 not-required (already-registered actor-user-roles-to-positions)

六、changeset

没有,故需 skip-changeset:本 PR 只改一个仓内工具脚本(scripts/**),不发布任何东西,读者/消费者可见面零变化。标签已由本座位手动打上。


🤖 Generated with Claude Code

https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o


Generated by Claude Code

#6148 的门禁是**前向**的(#6129 的理由,本单不推翻它),代价是 v17 列车
已有的 238 条 breaking changeset 一条都没被同一套逻辑看过。本次给同一个
脚本加一个**算子调用**的一次性审计口,把门禁**自己的**判定函数
(breakingDeclaration / readDisposition / findMigrationPrescription /
workspacePackagesAt)对着存量跑一遍 —— 没有第二份逻辑,只有换一批被判对象。

刻意不接 CI:`package.json` 的 `check:adr-0087-registration` 与 Check
Changeset 工作流都是不带旗标地调门禁,`--audit-stock` 只能手敲。它是
`--list` 的邻居,不是第二道门。

分桶把 238 收窄成人可核的残差:answered / exempt-unpublished /
exempt-no-prescription / RESIDUE(已发布的破坏 + 正文带改写指令 ⇒ 只剩
registered / already-registered,两者都要台账条目)。残差再叠一条机械
事实:动过这条 changeset 的 commit 里有没有哪个同时动了台账源。

浅克隆下该信号会**说谎**(嫁接 commit 携带整棵树、含两个台账源,实测
104 commit 的默认检出把 92 条残差里的 91 条报成「作者登记过」,条条错),
因此浅克隆时**抑制**而不是近似 —— 按 #4690 的口径,报不出来就说报不出来。

self-test 加 S1-S6 钉住分类器四个臂与其间的优先级;反向验证:删掉
`bumps.length > 0` 守卫后恰好 S5 转红(`[].every()` 真空为真)。
@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)
objectstack Ignored Ignored Aug 8, 2026 2:30am

Request Review

@github-actions github-actions Bot added the size/m label Aug 8, 2026
@os-project-manager os-project-manager added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 8, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

PM 验收:仪器这一半通过,已开启自动合并进队列。 判据问题已升维护者,#6350 保持 open(本 PR 用 Refs 而非 Fixes,这个取舍是对的)。

独立复核过的四点

  1. 审计不会与门禁漂移 —— --audit-stock 喂给存量的是门禁自己导出的判定函数(breakingDeclaration / readDisposition / findMigrationPrescription / workspacePackagesAt),没有第二份逻辑副本。这正是派单要求的形态。
  2. 边界守住了 —— 仅算子手动调用,scan() 与两个 CI 调用点一字未动,门禁仍是前向的(Check Changeset 会因为「别人的 PR 合进了 main」而变绿 —— merge ref 里的他人 changeset 被算成本 PR 新增的 #6129 的取向未被推翻)。派单里「这是一次性审计不是第二道门」的硬约束成立。
  3. 自测不是空绿 —— 73 条断言(+6),且反向验证方向先预言后跑:预言「删掉空 bumps 守卫则唯 S5 转红([].every() 恒真会让无包 changeset 被静默豁免)」,实测恰好且仅有 S5 红。
  4. CI 22/22 全绿,含 ESLint job 携带的家族门禁。

一条工具链陷阱值得所有座位记下:容器默认检出是浅克隆(104 commits,graft 于 2bc187641,那一个提交带着 5978 个文件、含两个台账源)。首跑审计把 92 条残留中的 91 条报成「作者登记过」—— 条条皆错,且在页面上看起来完全合理。处置正确:脚本在浅克隆下抑制该信号并给出 git fetch --unshallow --filter=blob:none 的处方,而不是近似一下蒙混过去。这是 #4690 那类「响亮失败胜过似是而非」的正确落地。

未回补台账是遵守派单、不是偷懒。 派单原文写了例外条款:「若审计发现的真实漏登记多于少数几条,先报数量与形态」。实测 92 条残留里 51 条从无登记提交(剔除带墓碑/D2 转换的还剩 34),远超少数几条,且每一条的判定都悬在同一个未答的判据问题上 —— migrations registry 是声明为永久、发布给消费者的产物,错加一条的撤回成本很高。先问后写是对的顺序。

抽样三条已落到结论,并且推翻了 issue 自己的排序:穷举两个台账确认三条均无覆盖条目;但 data-driver-query-omit-object(#5181)是三条里最弱的候选而非最强 —— 运行时行为不变,未加类型的 JS 调用方继续传 object: 也毫无问题,而 TS2353 会精确点名要删的键,并不「哑」。派单要求「逐条落到结论、不许只交总数」,这条满足了。

另记一条本 PR 之外的发现 #6497(live bypass,不是观察级):findMigrationPrescription() 两个分支都要求箭头,所以一张无箭头的两列改写表能让 changeset 一边宣称 not-required (no-migration-prescription)、一边在正文里发布成套改写指令。实测 13 条 changeset 是这个形状,其中 4 条已声明 breaking 的正在走这条豁免 —— 包括 #6350 自己的第一个样本,也就是说门禁根本够不着它,它是靠人眼发现的。已按派单不在本 PR 内顺手修。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 8, 2026 02:40
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit e6a7e26 Aug 8, 2026
23 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6350-breaking-changeset-backfill-audit branch August 8, 2026 02:55
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants