Skip to content

fix(tooling): check-doc-authoring 的死 ROOT 改为点名硬报错 (#4916) - #4934

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4916-doc-authoring-dead-root-hard-error
Aug 3, 2026
Merged

fix(tooling): check-doc-authoring 的死 ROOT 改为点名硬报错 (#4916)#4934
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4916-doc-authoring-dead-root-hard-error

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4916

scripts/check-doc-authoring.mjscollectFiles() 把每条 root 的遍历包在 try { walk(r, files); } catch {} 里。.claude / skills / content 任何一条被改名、移动或删除,ENOENT 就地被吞,门禁跑完剩下的 root 然后打印 ✓ doc authoring guard: N files clean,exit 0。从外部看,"三个 root 都干净"和"其中一个压根没被打开"是同一行绿字,只是 N 小了 —— 而没人读 N。

缺陷实测(旧代码,当前这棵树)

=== OLD code, all roots present ===
✓ doc authoring guard: 219 files clean — no bare metadata literals.
exit=0
=== OLD code, .claude renamed away ===
✓ doc authoring guard: 215 files clean — no bare metadata literals.
exit=0
=== OLD code, --self-test with .claude renamed (still green) ===
✓ check-doc-authoring self-test: scope wiring (.claude in, .claude/worktrees out) and detection both hold.
exit=0

219 → 215,exit 仍是 0。第三段是本单相对 #4915 的增量所在:#4913 加的 --self-test自己的临时树上断言,所以仓库里真实的 .claude/ 被改名时它照样绿。

选的是硬报错,不是白名单 —— 理由

没有白名单,也没有 optional: true 开关,这是刻意的:.claudeskillscontent 三条都是 git 跟踪的目录、里面有跟踪的文件,所以任何一个能在仓库根跑 pnpm check:doc-authoring 的检出都不会合法地缺其中之一 —— 议题里假设的"某个 root 在某些检出形态下合法缺席"经核查不存在,白名单没有要装的东西。

而"先留着以防万一"是有代价的:一个 optional 标记等于给下一个作者一条受支持的路去让这个失败闭嘴,而不是去跟进改名 —— 那就是空 catch {},只是说得客气些。真有 root 变成合法缺席的那天,那是一个需要连同条件和测试一起记录的决定,不是把检查放松。

顺带把 walk 内层的容错也去掉了:遍历途中出错同样意味着语料只读了一半,那也不该打印成一次干净的扫描。

新增 assertRootsResolvable() 在任何遍历之前跑,对每条 root 抛 DeadRootError 并带上名字(缺失 / 读不到 / 存在但不是目录三种判据分开)。

双向证明

红 —— 真实仓库树,.claude/ 改名走开:

✗ doc authoring guard: declared ROOT(s) do not resolve, so the scan would have been silently narrower:

  .claude — does not exist

Every entry in ROOTS (scripts/check-doc-authoring.mjs) must be a directory in the checkout,
and this check runs from the repo root. If a corpus directory was renamed or moved, update
ROOTS to follow it; if it was deleted, remove the entry deliberately. Do NOT restore a
tolerant skip: this used to be `catch {}`, and a dead root simply shrank the reported file
count while the gate kept printing green (#4916).

exit=1

红 —— 同时两条死根,两条都点名(存活的 root 不背锅):

  .claude — does not exist
  content — does not exist
exit=1

绿 —— 两条都恢复之后:

✓ doc authoring guard: 219 files clean — no bare metadata literals.
exit=0
✓ check-doc-authoring self-test: ... and the dead-root hard error (red when a ROOT is renamed, green when restored) all hold.
exit=0

这条红-绿证明已经折进 --self-test,常驻每一次 CI,不是只活在这段 PR 描述里:自检现在会在运行中途把一条 root 改名走开、要求判红且点名那条 root、且不提及存活的两条,再把另一条 root 换成同名文件、要求 exists but is not a directory 判据,然后两条都恢复、要求重新判绿。只观察到绿,对一个"失败表现为少扫一点"的门禁来说什么也证明不了 —— 所以自检每次都先观察红。

对称方向的调查结论(#4851 的经验)

#4851(PR #4921)在隔壁脚本上揭示的不只有 16 条死条目,还有 48 份从未被列过的文档。所以本单也查了反方向:有没有真实存在、应当被这条规则约束、却从来不在 ROOTS 里的语料目录?

有。 全仓 md/mdx 逐个走 ts 围栏块统计后:

本 PR 刻意没有改 ROOTS:纳入哪些 docs/ 子目录(docs/audits/ / docs/handoff/ 这类一次性过程记录纳入等于让历史快照永久受当前 lint 约束)是个范围决定,单独立单 #4929 交维护者定。

顺带扫到的同形状(未在本 PR 修)

三条 check:* 脚本是同一缺陷的更彻底版本 —— 扫描根读不到时 walker 返回空数组,于是收集到 0 个文件、循环一次不进、exit 0:check-single-authz-resolver.mjs:38check-startup-registry-verdict.mjs:217check-driver-conformance.mjs:147。单独立单 #4930,顺手全改会让这条 PR 失焦。判定为不同形状、不该一起改的(glob 展开中途的跳过、已记账的跳过、已声明的 exit 0)在该单里逐条写明。

验证

$ pnpm check:doc-authoring
✓ check-doc-authoring self-test: scope wiring (.claude in, .claude/worktrees out), detection, and the dead-root hard error (red when a ROOT is renamed, green when restored) all hold.
✓ doc authoring guard: 219 files clean — no bare metadata literals.
exit=0

$ npx eslint scripts/check-doc-authoring.mjs
exit=0

范围

只动了 scripts/check-doc-authoring.mjs 一个文件,外加一份空 frontmatter 的 changeset(tooling 改动,不发版)。没有改根 package.json,没有.github/workflows/ —— check:doc-authoring 已经在 package.json:36 里是 --self-test && 实跑 的组合,lint.yml:95 已经调它,新的自检断言自动生效,无需碰这两个本仓最热的冲突点。


Generated by Claude Code

…s dead (#4916)

`collectFiles()` walked each root inside `try { walk(r, files); } catch {}`.
Rename, move or delete any one of `.claude` / `skills` / `content` and its
ENOENT was swallowed in place: the scan finished the remaining roots and
printed `✓ doc authoring guard: N files clean`, exit 0. Measured on this tree
with `.claude/` renamed away, the old code reports 215 files clean, exit 0,
where the honest answer is 219 — "all three roots are clean" and "one root was
never opened" are the same green line with a smaller N, and nobody reads N.

`assertRootsResolvable()` now runs before any walking and throws a
`DeadRootError` naming every root that is missing, unreadable, or not a
directory; `main()` renders that as a red gate pointing at the dead root. No
whitelist and no `optional` flag: all three roots are git-tracked directories
with tracked files, so no checkout that can run this gate at the repo root is
legitimately missing one, and an optional marker would be a supported way to
silence the failure instead of following the rename — the empty catch, spelled
politely. The inner try is gone too: an error during the walk also means the
corpus was only partly read.

The proof is bidirectional and permanent. `--self-test` (#4913) already walked
a real temporary tree with the real walker; it now renames one root away
mid-run and requires red naming that root and not the survivors, replaces
another root with a file and requires the `not a directory` verdict, then
restores both and requires green again. This closes what #4913's self-test
could not: it stayed green with the repo's real `.claude/` renamed away,
because it asserts over its own temp tree.

Same discipline as #4690 / #4804 / #4835 / #4851 / #4868 / #4890.

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

vercel Bot commented Aug 3, 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 3, 2026 4:33pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling size/m labels Aug 3, 2026
@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 3, 2026 16:35
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit bf973ae Aug 3, 2026
18 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-4916-doc-authoring-dead-root-hard-error branch August 3, 2026 16:55
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…tes (objectstack-ai#4930) (objectstack-ai#4941)

`check-single-authz-resolver`, `check-startup-registry-verdict` and
`check-driver-conformance` each concluded from a walk that opened with
`try { readdirSync(dir) } catch { return out/[]; }`. A root that was renamed,
moved or made unreadable produced zero files, and zero files produce zero
findings — the same verdict as a clean workspace. Same shape as objectstack-ai#4916/objectstack-ai#4934.

Each script now resolves every declared root before drawing any conclusion
from the scan, and fails naming the root and the reason ("does not exist" /
"exists but is not a directory"). The swallows inside the walks are gone too:
an error mid-walk means the corpus was only partly read, which must not print
as a clean pass either. No whitelist and no `optional: true` flag — every root
is a git-tracked directory, so an optional marker would be a supported way to
silence the failure instead of fixing the rename.

Measured on this tree, pre-fix: only check-single-authz-resolver was genuinely
vacuous end to end (it went red by luck, because both DELEGATORS live under the
same root, reporting "Delegator missing" for the wrong cause). The other two
already failed, but named the wrong cause — startup-registry-verdict's
`existsSync` accepted a file and then blamed an empty corpus, and
driver-conformance answered a renamed `packages/spec/src/data` with five
"CASE_SETS names X, which <file> no longer exports" errors that read as five
deliberate deletions. Both now name the directory.

The proofs are bidirectional and permanent, not one-offs in the PR body: each
`--self-test` renames a root away mid-run and requires red naming that root,
replaces one with a file and requires the not-a-directory verdict, dangles a
symlink inside the walk and requires the error rather than a smaller corpus,
then restores everything and requires green again. check-single-authz-resolver
had no self-test and gained one covering both of its original invariants;
`package.json` chains `--self-test &&` into `check:authz-resolver` to match the
other ten `check:*` entries.


Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-doc-authoring 的 walk 用 catch {} 吞掉不存在的 ROOT —— 目录一改名,门禁静默变瞎并继续报绿

2 participants