fix(spec): packages/spec/scripts/** 进入类型检查程序 —— 新增 tsconfig.scripts.json,修掉全部 43 条错误 - #6398
Conversation
…son,修掉全部 43 条错误 `packages/spec/tsconfig.json` 的 include 是 `["src/**/*"]`,#5286 的 `tsconfig.test.json` 同样止于 `src`。于是 `scripts/**` 不在本包任何 tsconfig 的 program 里 —— 不是「被 exclude 排除」,而是压根没被 include 选进来。29 个 `scripts/**/*.test.ts` 由 vitest 真的在跑,另有约 48 个文件就是 `gen:schema` / `gen:openapi` / `check:liveness` / `check:strictness-ledger` 等门禁本体;tsx 只转译。 新增 `tsconfig.scripts.json`(strictness 全部继承,未放松任何一项),由 `check:scripts-typecheck` 指名,并接入 `typecheck` 脚本链。合并后实测 43 条错误, 本 PR 全部修完,`scripts/**` 以零台账条目进入 —— 因此 plain tsc 比 shrink-only 台账更严:台账允许加一行来增长,这里一条也不允许。 其中 `build-schemas.ts` 的 13 条 `Property 'keys'/'rev' does not exist on type 'never'` 是本单最有价值的产出:`gitResolvedAnchor` 是模块级 `let ... = null`, 唯一赋值在 `resolveSurfaceBase()` 函数体内,而 TypeScript 的控制流分析不跟踪函数体 内的赋值,于是每个顶层读取处它仍被收窄为 `null` —— `if (gitResolvedAnchor)` 的整个 块体类型为 `never`,即 #5235/#5358/#5370/#5847 那约 100 行 in-tree anchor 写入逻辑 从未被类型检查过。改为经返回值 `SurfaceBaseResolution.gitAnchor` 传出,赋值回到调用方 自己的控制流里。运行时行为不变。 Fixes #5475 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
PM 验收:ACCEPT — 已 ready + auto-merge。唯一一处红不是本 PR 的,详见末节。 CI:32 个 check,除 你纠正了我的判断,而且是往更保守的方向我在派发令里写「若是真潜伏 bug,这是本单最大产出」,并把它设成期待。你的结论是:不是潜伏 bug,是类型谎言 —— 「correct code that was never verifiable」。 支撑这个结论的是一条落空的预测,你如实报了:你预期那 ~100 行脱离 RV-3 是我今天见过最锋利的一条验证同一行
也就是说,那个区域里一条真实的类型错误此前会被吞掉,因为 而 RV-1c(把同一个植入错误放在 RV-5 也很细:scripts 测试文件里的 新覆盖当天就抓到了当天的漂移
两个方向性判断,理由都是测出来的后果而非偏好选独立 sibling config 而非并入
43 条全修、零台账条目,并给出了比我更好的理由:plain tsc 比 shrink-only 台账更严,因为台账允许「加一行」来增长,这里一条也不允许。 一处主动的不邀功,我要点名
你在 PR 正文里明说了这一点,而不是宣称计数涨了。 我在派发令里要的是「说清它现在数到了什么以前数不到的」—— 诚实的答案是「没有;真正变的是真实编译 + PINS_CHECKED 覆盖」。答一个不讨好的真话,比答一个讨好的假话有用。 首跑那 6 条红(
|
Fixes #5475
缺陷
packages/spec/tsconfig.json的include是["src/**/*"],#5286 新增的tsconfig.test.json同样止于src。于是整个packages/spec/scripts/**不在本包任何 tsconfig 的 program 里 —— 不是「被exclude排除」,而是压根没被include选进来,所以连check:type-check-coverage的TESTS_COVERED都数不到它(该计数只统计 PRIMARY 配置 include 根之下的测试文件)。这批文件不是死代码:29 个
scripts/**/*.test.ts由vitest.config.ts真的在每次pnpm test里跑;另有约 48 个文件就是门禁本体 ——build-schemas.ts(gen:schema,本包全部 JSON Schema 与 #4650 authorable-surface 删除门禁的判定都由它产出)、build-openapi.ts、liveness/check-liveness.mts、check-strictness-ledger.mts等。tsx 只转译,不做类型解析。同一失效类的第三种形态:#4311 是「包级没有 typecheck 任务」,#5286 是「测试层被 exclude 出唯一的 program」,本单是「根本没被 include 进来」。
重测:原单基线已过时
原单基线
01c0baef9(08-05)记录 16 文件 / 33 错误。在当前origin/main上重测为 16 文件 / 43 错误。逐层对照:.mts后缀 import,需allowImportingTsExtensionsfrom 'vitest'导入,无一依赖全局.mjsimport,消费者从 1 个涨到 3 个build-schemas.ts的never簇,比原单大一倍多build-schemas.ts今天由 #6360 引入authorable-defaults.test.ts的readonly元组原单引用的
build-schemas.ts:1208-1228行号确已失效(真实位置在 1733–1954 区间)。方向选择:单开
tsconfig.scripts.json(原单选项 2)原单给了两条路且未定。选 2,两条实测理由:
allowImportingTsExtensions只有这里需要。scripts/liveness/下的.mts模块互相以 TS 后缀导入(./evidence.mts),tsx 能解析、tsc 不能。把这个开关打到tsconfig.test.json上,等于额外允许src/**/*.test.ts写import './x.ts'—— 而发射产物的 build 配置仍然拒绝这种拼写。一个对src毫无收益、却在那里开出新分歧的开关,应该只装在需要它的 program 上。test-typecheck-debt.json是每包唯一且精确的台账。check-test-typecheck.mts双向判定(增长红、缩小红、归零红),第二个 project 共用同一台账文件会让 79 条src/**条目立刻全部报「graduated」。而scripts/**是零台账条目进入的 —— 合并后暴露的 43 条全部在本 PR 修完,所以 plaintsc反而是更严的门禁:台账允许「加一行」来增长,这里一条也不允许。tsconfig.scripts.json不放松任何 strictness:strict/noUnusedLocals/noUnusedParameters/noImplicitReturns全部从根配置继承,只动 module 语义与 program 根(rootDir放宽到.,因为scripts/在src/之外;noEmit开着,不写任何东西)。never簇:是类型谎言,不是潜伏的运行时 bug本单最有价值的产出。
build-schemas.ts里:TypeScript 的控制流分析不跟踪函数体内对模块级
let的赋值,所以在之后每一个顶层读取点,它仍被收窄为初始化式的null。后果不是「少检查了一个变量」,而是:gitResolvedAnchor?.rev→Property 'rev' does not exist on type 'never';if (gitResolvedAnchor) { const anchor = gitResolvedAnchor; ... }→ 整个块体的anchor类型是never,即 spec 的 #4650 删除闸门在「按 SHA 钉住的消费者构建」里无法锚定 origin/main,硬失败 —— cloud 的镜像构建与 pin bump 全线卡死 #5235 /check:authorable-surface在--check模式下也会重写authorable-surface.base.json—— 一次纯核验会改工作区,且任何无关 PR 都能因此静默推进删除门的锚点 #5358 / os-regen 驱动指示的gen:schema在 merge 未 commit 时运行,会把 authorable-surface 锚点倒退回旧 merge-base —— 生成器写入、门全绿、静默撤销 main 的锚点推进 #5370 / gen:schema 在锚点「领先于」解析基线时打印「trails the baseline … by 0 key(s)」——方向说反,信息为零 #5847 那约 100 行 in-tree anchor 写入逻辑(含今天 fix(spec): 锚点漂移提示按实测方向措辞,不再把「领先」说成 trails … by 0 key(s) (#5847) #6309 才落地的漂移方向报告)从未被类型检查器读过一行。最小复现已单独验证:
修法是契约式的:把这个模块级旁路改成显式返回值
SurfaceBaseResolution.gitAnchor,赋值回到调用方自己的控制流里,CFA 就能看见。git 路径设值、in-tree 路径留 null,运行时行为逐字不变。never吞掉。另一条实打实的漂移:
readSurfaceKeysAtRev的参数类型sharded-artifacts.ts导出ShardArrayField = 'keys' | 'schemas' | 'defaults',而build-schemas.ts的readSurfaceKeysAtRev手抄了一份更窄的'keys' | 'schemas'。今天 #6360 给这个联合加了'defaults'成员并新增了一个传'defaults'的调用点,手抄的那份没跟上 —— 运行时无害(值只是转发给接受宽类型的readShardedKeysAtRev),但正是这个 program 存在的意义。修法是消费导出的ShardArrayField,而不是加宽手抄件。逆向验证(预测先写、后跑)
预测原件:
/tmp/.../issue-5475/predictions.md,六条全部在执行前写定。build-schemas.ts)植入类型错误 → 新门禁TS2322 ... 1580,7红tsc --noEmitcheck:test-typecheck: OKnever块内植入const probe: number = anchor.revTS2322: Type 'string' is not assignable to type 'number'never版本never,看不见真实类型TS2339: Property 'rev' does not exist on type 'never'@ts-expect-error→check:type-check-coveragecheck:scripts-typecheck报TS2578: Unused '@ts-expect-error' directivecheck:type-check-coverage的testFiles计数RV-3 那对是本 PR 最锋利的一处证据:同一行代码,改前被
never吞掉(never可赋给任何类型),改后报出真实的string/number不匹配。这就是「那 100 行不可证伪」的直接演示。RV-5 同样值得单独读:那条
@ts-expect-error的判定换了门而不是消失了 —— 从「PINS_CHECKED:没有任何 program 编译它,指令永不求值」变成「真的编译了,而且这条指令是多余的(TS2578)」。指令被求值,正是「真进了 program」的定义。覆盖是真的 —— 但要如实说清它数了什么
check:type-check-coverage仍然通过,且它的testFiles计数没有变(前后皆 556)。这不是遗漏,是该门禁的设计:它只统计 PRIMARYtsconfig.json的 include 根(src)之下的测试文件,因为exclude只能从include选中的集合里减,scripts/从来就没被选中过。所以本 PR 不能声称「TESTS_COVERED 多数了 29 个文件」。真正变了的是两件事,都已实测:
scripts/**/*.test.ts从「无 program 覆盖的幽灵 pin 候选」变成「被真实编译」(RV-5)。其余修复
build-schemas.ts:prop是 build-schemas.ts 检查 (c) 的「墓碑已满 2 个 major」证明仍用叶名匹配 —— 无关簇的登记可以替一次退休提前起算 #5898 移除 leaf-name 匹配后留下的死局部;AUTHORABLE_SURFACE_DESCRIPTION导入与MANIFEST_DESCRIPTION别名是 spec 生成物按 category 分片:拆掉三个单体 ratchet 文件的合并队列串行税(维护者 2026-08-06 已拍板) #5837 把描述移进sharded-artifacts.ts后的残留。build-openapi.ts:import * as Data绑定后从未读取。已实测:TS import elision 本就在 tsx 求值前丢弃未使用的 namespace import,所以删的是名字不是副作用 ——json-schema/openapi.json前后逐字节相同(已 diff 验证)。若它曾是 load-bearing,正确写法是import '../src/data';,那是另一条语句。check-liveness.mts:report是刻意的any,导致v.*全部退化为any—— 这正是stale工作列表被隐式 any 迭代、而它的邻居们各自手写: string的由来。在边界处标注一次生产者自己的VerificationReport,四处读取一起变真。authorable-defaults.test.ts:两个as constfixture 造出readonly元组,根本不是Iterable,即这两条断言从来没类型检查过。改标注为 collector 自己的参数类型。build-docs.ts:generatedFiles是死累加器(唯一引用是它自己的声明);_zodFile下划线化而非删除,以把改动压到渲染函数的一行(当时 fix(spec): 参考文档顶层长枚举移入 Allowed Values,联合变体印数量 (#6225, #6226) #6377 在该面上,现已落地并已 rebase 通过)。scripts/check-regen-pending.d.mts(新增):.mjs保持.mjs(pre-commit与check:merge-driver用裸node调它)。声明的动机是具体的 —— 无类型时谓词退化为any,if (distIsStale)(漏掉调用,正是这个 guard 要防的错误)会类型检查通过。check-generated.ts:新check:*脚本必须在check:generated台账里归类(该门禁在跑测试时红了,如实分类)。归入NO_GENERATOR而非check:test-typecheck旁边的 GATED —— 差别就是本 PR 的设计:那条比对签入产物、有生成器和方向棘轮;这条两者皆无,没有可重新生成的文件,也没有能让它变绿的--fix。变更集:
skip-changesetscripts/、tsconfig*.json都不在packages/spec的files白名单里。按提示实测而非假设:gen:schemaA/B(我的build-schemas.tsvsorigin/main的),1623 个 schema 的json-schema/树哈希逐字节相同 ——177f4a81ab3a...两侧一致;json-schema.manifest/、authorable-surface/、authorable-defaults/)跑完gen:schema后git status干净;json-schema/openapi.json前后diff为空。发布物零变化,故取
skip-changeset。验证
Generated by Claude Code