소스 검색

fix(scripts): address review findings on the gate consolidation

- publint-all: the recursive publication view uses readdirSync
  {recursive} again instead of globSync('**/*') — the glob skips
  dot-prefixed segments (verified empirically), but npm pack publishes
  dotfiles inside included directories, so hidden exports were
  reported missing and other hidden files escaped validation
- markdown.ts/verify-type-equiv: markdownFences now reports whether a
  closing delimiter terminates the block (mdast silently closes an
  unterminated fence at EOF), and verify-type-equiv rejects unclosed
  type-equivalence fences again — the Agent Note claimed such a block
  still fails at the manifest checks, but its comparisons can succeed
- Agent Note EN+ZH: record the restored rejection; rewrite the zh
  Problem section into past tense to match the English side's shipped
  reality; pair re-recorded
Tianyi Cui 1 개월 전
부모
커밋
ebdcb5776a

+ 2 - 2
.agents/notes/implemented/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
-2026-07-26-consolidate-gate-scripts-on-existing-deps.md: 31823979e16544a77284eeeab02983c6090cbb51
-2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md: 366fd5acff0ec4ddaf2dffa2ec373c90d2e964f3
+2026-07-26-consolidate-gate-scripts-on-existing-deps.md: 6370c8f92eff7296327e941e698ec4f733100bb2
+2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md: 3587c92e0e9655d8b38d24d184c1c68c44b131d4

+ 1 - 1
.agents/notes/implemented/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.md

@@ -29,5 +29,5 @@ No new dependency was needed anywhere; every replacement is an existing devDep o
 ## Consequences
 
 - One fence parser: every markdown gate now classifies fences through mdast, so tilde, indented, and 4-backtick container fences behave identically everywhere. The docs tree contained no fence shape the regex scanners mishandled, so gate results are unchanged on the tree that landed the swap: `pnpm run doc-sync` and each rewritten gate ran before and after with byte-identical output (`doc-typecheck` block/opt-out counts, `verify-type-equiv` match counts, `publint`, `verify-built-package-invariants`, `verify-runtime-closure`, `verify-package-paths`, `verify-client-domain-graph`, and both package-README prose gates).
-- `verify-type-equiv` no longer errors on an unterminated fence: mdast closes an unterminated block at end-of-file, so such a block reaches the manifest checks and still fails there as an orphan or drift rather than as a dedicated scanner error. The `doc-typecheck` scanner never had that error path.
+- `verify-type-equiv` still rejects an unterminated type-equivalence fence: mdast silently closes an unterminated block at end-of-file (its comparisons could then pass), so the shared helper reports whether a closing delimiter exists and the gate errors on an unclosed block, preserving the removed scanner's rejection. The `doc-typecheck` scanner never had that error path.
 - `parseArgs` keeps the last value of a duplicated option instead of erroring — a dev-tool edge case the tests don't pin, accepted in exchange for deleting the two bespoke parsers. (Strict mode still rejects a `--`-prefixed token where a value is expected, matching the replaced parsers.)

+ 5 - 5
.agents/notes/implemented/simplification/2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md

@@ -6,11 +6,11 @@ Status: implemented
 
 ## 问题
 
-`scripts/` 下的门禁大多已在用正确的工具(15 个以上的门禁使用 `node:fs` 的 `globSync`,markdown 门禁使用 mdast/micromark),但少数几个掉队的脚本仍在手写同类门禁早已用既有依赖或内置模块完成的事情:
+`scripts/` 下的门禁大多已在用正确的工具(15 个以上的门禁使用 `node:fs` 的 `globSync`,markdown 门禁使用 mdast/micromark),但少数几个掉队的脚本手写同类门禁早已用既有依赖或内置模块完成的事情:
 
-- **重复的围栏扫描器。**`scripts/md-fences.ts`(约 55 行,由 `doc-typecheck.ts` 消费)和 `scripts/verify-type-equiv.ts` 中的 `extractEquivBlocks`(约 39 行)是同一个围栏代码块正则行扫描器的两份拷贝,而 `scripts/verify-mermaid.ts` 已通过访问 mdast `code` 节点来提取代码围栏;`scripts/markdown.ts` 自己的 `markdownProseLines` 也先解析成 mdast,再用第二个正则手工跟踪围栏状态。这两个正则扫描器只识别第 0 列的反引号围栏,因此在波浪线围栏和缩进围栏上与基于 mdast 的门禁悄悄不一致。
-- **手写的 argv 解析。**`scripts/publint-all.ts` 中的 `parseOptions` 和 `scripts/verify-built-package-invariants.mjs` 中与之几乎相同的拷贝(约 26 行)手工推进 argv 下标,而同类脚本(`verify-runtime-closure.ts`、`build-exe-for-python-sdk.ts`、`packages/sdk/scripts/src/args.ts`)已在使用 `node:util` 的内置 `parseArgs`。
-- **手写的目录遍历。**五处代码各自重写 `globSync` 已覆盖的嵌套 `readdirSync` 遍历:`verify-runtime-closure.ts` 对 packages 与 vendor manifest(元数据清单)的扫描、`dev-web.ts` 的 `discoverPluginDirs`、`verify-package-paths.ts` 的 `realPackageNames`、`verify-client-domain-graph.ts` 的 `listSources`,以及 `publint-all.ts` 的 `addPath`(合计约 55–65 行)。`scripts/package-invariants.ts` 展示了一行式的 `globSync` 模板。
+- **重复的围栏扫描器。**`scripts/md-fences.ts`(约 55 行,由 `doc-typecheck.ts` 消费)和 `scripts/verify-type-equiv.ts` 中的 `extractEquivBlocks`(约 39 行)是同一个围栏代码块正则行扫描器的两份拷贝,而 `scripts/verify-mermaid.ts` 已通过访问 mdast `code` 节点来提取代码围栏;`scripts/markdown.ts` 自己的 `markdownProseLines` 也先解析成 mdast,再用第二个正则手工跟踪围栏状态。这两个正则扫描器只识别第 0 列的反引号围栏,因此在波浪线围栏和缩进围栏上与基于 mdast 的门禁悄悄不一致。
+- **手写的 argv 解析。**`scripts/publint-all.ts` 中的 `parseOptions` 和 `scripts/verify-built-package-invariants.mjs` 中与之几乎相同的拷贝(约 26 行)手工推进 argv 下标,而同类脚本(`verify-runtime-closure.ts`、`build-exe-for-python-sdk.ts`、`packages/sdk/scripts/src/args.ts`)已在使用 `node:util` 的内置 `parseArgs`。
+- **手写的目录遍历。**五处代码各自重写 `globSync` 已覆盖的嵌套 `readdirSync` 遍历:`verify-runtime-closure.ts` 对 packages 与 vendor manifest(元数据清单)的扫描、`dev-web.ts` 的 `discoverPluginDirs`、`verify-package-paths.ts` 的 `realPackageNames`、`verify-client-domain-graph.ts` 的 `listSources`,以及 `publint-all.ts` 的 `addPath`(合计约 55–65 行)。`scripts/package-invariants.ts` 展示了一行式的 `globSync` 模板。
 
 所有替换都不需要引入新依赖;每一处替换用的都是既有的 devDependency 或 Node 内置模块。
 
@@ -29,5 +29,5 @@ Status: implemented
 ## 后果
 
 - 只剩一个围栏解析器:所有 markdown 门禁现在都经由 mdast 归类代码围栏,因此波浪线围栏、缩进围栏和四反引号容器围栏在各处的行为完全一致。文档树中不存在正则扫描器处理有误的围栏形态,所以在落地这次替换的代码树上门禁结果不变:`pnpm run doc-sync` 及每个被改写的门禁在改动前后各跑一遍,输出逐字节相同(`doc-typecheck` 的块数/opt-out 计数、`verify-type-equiv` 的匹配计数、`publint`、`verify-built-package-invariants`、`verify-runtime-closure`、`verify-package-paths`、`verify-client-domain-graph`,以及两个包 README 散文门禁)。
-- `verify-type-equiv` 不再对未闭合的围栏报专门的错误:mdast 会在文件末尾闭合未闭合的代码块,这样的块会进入 manifest 检查,并在那里以孤儿或漂移的形式照样失败,而不是触发专门的扫描器错误。`doc-typecheck` 的扫描器本来就没有这条错误路径。
+- `verify-type-equiv` 仍然拒绝未闭合的类型等价围栏:mdast 会在文件末尾静默闭合未闭合的代码块(其比较随后可能通过),因此共享辅助函数会报告闭合定界符是否存在,门禁在块未闭合时报错,保留了被删扫描器的这条拒绝路径。`doc-typecheck` 的扫描器本来就没有这条错误路径。
 - `parseArgs` 对重复出现的选项保留最后一个值而不报错——一个测试未固定的开发工具边缘用例,作为删除两份手写解析器的交换被接受。(严格模式下,需要取值处遇到以 `--` 开头的 token 仍会拒绝,与被替换的解析器行为一致。)

+ 10 - 1
scripts/markdown.ts

@@ -31,6 +31,12 @@ export interface MarkdownFence {
   info: string
   /** Block body without the fence delimiters. */
   code: string
+  /**
+   * Whether a closing fence delimiter terminates the block — mdast silently
+   * closes an unterminated fence at end of file. False on indented
+   * (non-fenced) blocks, whose end line is code.
+   */
+  closed: boolean
 }
 
 /** Parse GitHub-flavored Markdown with the repository's standard extensions. */
@@ -56,13 +62,16 @@ export function visitMarkdown(node: Nodes, visitor: (node: Nodes) => boolean | v
  * @returns each block's opening line, language, info string, and body.
  */
 export function markdownFences(source: string): MarkdownFence[] {
+  const lines = source.split('\n')
   const fences: MarkdownFence[] = []
   visitMarkdown(parseMarkdown(source), (node) => {
     if (node.type !== 'code' || node.position === undefined) return
     const lang = node.lang ?? null
     const meta = node.meta ?? ''
     const info = lang === null ? '' : meta === '' ? lang : `${lang} ${meta}`
-    fences.push({ line: node.position.start.line, lang, info, code: node.value })
+    const endLine = lines[node.position.end.line - 1] ?? ''
+    const closed = /^ {0,3}(`{3,}|~{3,})\s*$/.test(endLine)
+    fences.push({ line: node.position.start.line, lang, info, code: node.value, closed })
   })
   return fences
 }

+ 5 - 1
scripts/publint-all.ts

@@ -2,6 +2,7 @@
 
 import {
   globSync,
+  readdirSync,
   readFileSync,
   statSync,
 } from 'node:fs'
@@ -91,7 +92,10 @@ function publicationFiles(target: PackageTarget): PackFile[] {
 function addPath(path: string, paths: Set<string>): void {
   const stat = statSync(path)
   if (stat.isDirectory()) {
-    for (const entry of globSync('**/*', { cwd: path, withFileTypes: true })) {
+    // readdirSync, not globSync: `**/*` skips dot-prefixed segments, but npm
+    // pack publishes dotfiles inside included directories, and this view must
+    // match what npm publishes.
+    for (const entry of readdirSync(path, { recursive: true, withFileTypes: true })) {
       if (entry.isFile()) paths.add(resolve(entry.parentPath, entry.name))
     }
   } else if (stat.isFile()) {

+ 3 - 0
scripts/verify-type-equiv.ts

@@ -88,6 +88,9 @@ function extractEquivBlocks(docRel: string): EquivBlock[] {
       throw new Error(`verify-type-equiv: ${docRel}:${fence.line} — use the concise \`ts public-api\` fence`)
     }
     if (fence.info !== 'ts type-equiv' && fence.info !== 'ts public-api') continue
+    if (!fence.closed) {
+      throw new Error(`verify-type-equiv: ${docRel}:${fence.line} — unterminated type-equivalence fence (missing closing \`\`\`)`)
+    }
     const symbol = blockSymbol(fence.code)
     if (symbol === null) {
       throw new Error(`verify-type-equiv: ${docRel}:${fence.line} — type-equiv block has no parseable interface/type/class declaration`)