Skip to content

fix(scripts): check-single-authz-resolver 的 walk 收 .mts/.cts,排除项同族扩展 (#6070) - #6282

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-6070-walk-mts-extension
Aug 7, 2026
Merged

fix(scripts): check-single-authz-resolver 的 walk 收 .mts/.cts,排除项同族扩展 (#6070)#6282
hotlong merged 1 commit into
mainfrom
claude/issue-6070-walk-mts-extension

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #6070

'x.mts'.endsWith('.ts')false(ts 前一个字符是 m 而非 .),所以 walk() 从来没收过声明的扫描根 packages/ 下的任何 .mts / .cts。检查 (1)「不存在重复的请求上下文解析器」这个结论,是在一个结构性排除了 12 个文件的语料上得出的。

按 issue 方向 1(声明即执行)修:门禁声称读遍 packages/,那就该真的读遍。

过滤器 before / after

单文件改动 scripts/check-single-authz-resolver.mjs,walk() 的收集条件:

// before —— 后缀字符串,漏掉整个模块扩展名家族
else if (e.endsWith('.ts') && !e.endsWith('.test.ts') && !e.endsWith('.d.ts')) out.push(p);

// after —— 扩展名"家族",两条正则成对声明在模块顶部
const SCANNED_EXT  = /\.[mc]?ts$/;
const EXCLUDED_EXT = /\.(?:test|d)\.[mc]?ts$/;
else if (SCANNED_EXT.test(e) && !EXCLUDED_EXT.test(e)) out.push(p);

排除项与收集项同一步放宽,这是刻意的:只放宽收集器会把同一个 bug 原样种到下一层 —— x.test.mts 变成可扫描,而 x.test.ts 不是。仓内今天没有这四种形状(.test.mts / .test.cts / .d.mts / .d.cts 实测各 0 个),仍然照排 —— 排除项陈述的是"一个测试/声明文件是什么",不是"现存哪些文件"的清单。

语料 delta:1475 → 1487(+12)

用脚本自己的 collectScanFiles 实测(把 main(); 换成一行计数打印,分别跑改前/改后两版):

BEFORE (origin/main filter): corpus: 1475
AFTER  (widened filter):     corpus: 1487

新增的正是那 12 个文件,全部在 packages/spec/scripts/ 下(check-strictness-ledger.mtsliveness/*.mts 等)。

⚠️ 与 issue / 分诊正文里的 3053 → 3065 不是同一个量,这里如实记下:3053 是 packages/.ts 文件的原始个数(本 PR 基点 f6609e6 实测:全量 3113、去掉 dist/ __tests__/ 后 3029),而 1475 是门禁真正推理的语料(再去掉 .test.ts / .d.ts)。两个数都对,量的是不同集合;+12 这个差值两种口径一致,也是本单唯一实际变化的量。

现状仍绿(复测,非沿用分诊结论)

12 个新入语料的文件对启发式(sys_user_rolesys_user_permission_set)的命中数逐个复测 = 0/0,放宽后真实运行仍绿:

$ node scripts/check-single-authz-resolver.mjs --self-test
✓ check-single-authz-resolver self-test: duplicate detection, delegation, the extension
  family (.mts/.cts enter the corpus and their duplicates are named; .test./.d. shapes of
  every extension stay out), the dead-root hard error ... and the empty-scan hard error ...
  all hold.

$ node scripts/check-single-authz-resolver.mjs
✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.

语料变大不改变任何裁决;改变的是这个裁决从此出自门禁声称读的那个集合。

self-test delta:双向都钉,单向都不够

新增块(沿用 #6056 的 fixture idiom,真实临时目录 + 真实 walker):

  • 正向:.mts.cts 各写一个重复解析器形状 ⇒ 必须被抓出且按名报出(2 条,逐条断言文件名);
  • 反向(排除名单):x.test.mts / x.test.cts / x.d.mts / x.d.cts 四个形状 ⇒ 不得进语料;
  • 语料计数:collectScanFiles 必须恰好 beforeExt + 2,清理后必须回到 beforeExt

第三条不是冗余:只断言"被抓出"的话,一个收不到任何文件的过滤器同样产生 0 条错误 —— 那正是原始漏判之所以静默的原因;只断言计数的话,又证不出抓出来的错误指向正确的文件。两条都要,才封得住。

顺带补齐一处既有断言的空头支票:'tests, .d.ts and dist/ are out of scope' 这个 label 从写下起就宣称覆盖 .d.ts,但从来没写过 .d.ts fixture —— 本 PR 把它补上(与本次扩到同族排除是同一件事)。

反向验证(方向先声明后运行)

声明的预期方向 = 常规 red:把 walk() 的过滤器还原成 .ts-only、保留全部新断言,则 4 条新断言必须转红,且红必须指名漏掉了什么;既有断言(含新补的 .d.ts)不受影响。

实跑(在 scratchpad 的脚本副本上,不污染工作树):

✗ check-single-authz-resolver self-test failed:
  ✗ .mts/.cts join the corpus and their test/declaration shapes stay out: expected 7, got 5
  ✗ a duplicate resolver in .mts and one in .cts are both flagged: expected 2, got 0
  ✗ the .mts duplicate is named: expected true, got false
  ✗ the .cts duplicate is named: expected true, got false
exit=1

与声明逐条相符:语料 7 → 5(两个模块扩展名文件根本没被收),诊断数 2 → 0。恢复过滤器 ⇒ self-test 与真实运行双绿(上一节输出)。红只出在 4 条新断言上,说明红确由过滤器本身引起,而非别的改动。

范围

严格按分诊钉死的范围:只改扩展名过滤 + 回归断言。未动 SCAN_ROOTS、未重写 #5916 的下限断言逻辑、未搭高水位棘轮(维护者在 #5916 已明确否掉)。#5475(那 12 个文件不在任何 tsconfig 的 include 内)是类型检查覆盖的另一套机制,仅交叉链接,不并单。

改动仅 scripts/ 单文件,不发布任何东西 ⇒ 无 changeset,带 skip-changeset。门禁 pnpm check:authz-resolver 在 CI 的 ESLint job(lint.yml 的 "Single authz resolver guard" 步)内跑。

相关:#5916 / PR #6056(按根下限)、#4930 / #4916(死根按名报错)、#4932(同族先例)、#4690(「提取失败必须红」出处)。


Generated by Claude Code

…6070)

`'x.mts'.endsWith('.ts')` is false — the character before `ts` is `m`, not `.` — so
the collector walked past every `.mts`/`.cts` source under the declared scan root
`packages/`. Check (1) concluded "no duplicate request-context resolver exists" from
a corpus that structurally excluded 12 files.

Neither existing corpus assertion can see this: the root resolves (#4930) and yields
well over a thousand `.ts` files, far above the per-root floor of one (#5916). A floor
answers "did this root produce anything", never "did it produce everything it declares".

- walk() now filters on an extension FAMILY: SCANNED_EXT /\.[mc]?ts$/ minus
  EXCLUDED_EXT /\.(?:test|d)\.[mc]?ts$/, so the test/declaration exclusions widen in
  the same step — widening only the collector would re-plant the same bug one level
  down (`x.test.mts` scannable while `x.test.ts` is not). The repo has none of those
  four shapes today; they are excluded anyway.
- Corpus 1475 -> 1487 files (+12, all `packages/spec/scripts/**`). None trips check
  (1)'s heuristic, so the real run stays green — what changes is that the verdict is
  now drawn from what the gate says it reads.
- Self-test pins both directions: the corpus must GROW by exactly the collectable
  fixtures, AND the duplicates written in `.mts`/`.cts` must be caught and named. Either
  alone is satisfiable by a filter that collects nothing. Also writes the `.d.ts`
  fixture that assertion's label has claimed since it was written.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@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:00pm

Request Review

hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

施工中量到一条范围外的发现,按 Prime Directive #10 已单独立案,不在本 PR 修:

#6286 —— 本门禁检查 (1) 的判据词表已被 ADR-0090 D3 改名废掉:sys_user_rolesys_user_position,而启发式仍在找 sys_user_role。实测(就在本 PR 放宽后的 1487 个文件语料上):同时命中两词的文件 0 个,连规范解析器自己都不触发自己的启发式(它读的是 sys_user_position:317 + sys_user_permission_set:341);而按当前词表(sys_user_position + sys_user_permission_set)则有 20 个文件命中。

两者的关系,一句话:#6070 / 本 PR 治的是「语料」(读了什么),#6286 治的是「判据」(拿什么判)。本 PR 把语料从 1475 补到 1487;判据词表在放宽前后都一样匹配不到东西,所以本 PR 的"现状仍绿"是真绿,只是这个绿现在有了第二重原因 —— 那重原因归 #6286。刻意不并单:改判据必然改裁决(直接换成 sys_user_position 今天会命中 20 个文件,多数显然不是解析器),需要先定启发式的形状并重新策展 ALLOW,不该塞进一个申报面是"扩展名过滤"的 PR。

另:本 PR 首个(opened)工作流跑里 Check Changeset 红,是 #5580 那个已知竞态 —— 该 job 在第一步实时重读 labels,而它 13:00:48 启动、skip-changeset 13:01:09 落上,差 21s。其后由 label 触发的两次跑(runs 31180713120 / 31180713232)中 Check Changeset 均为 skipped,即 label 已生效。


Generated by Claude Code

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

Labels

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.

check-single-authz-resolver 的 walk 只收 .ts,packages/ 下 12 个 .mts 从不被扫描(observation)

2 participants