docs(notes): aggressive archive sweep of low-value decision records

Archive 21 implemented triplets whose shipped decisions are complete and
whose bodies no longer guide future work (one-off UI chrome, generator
applications whose scripts are self-explanatory, superseded implementation
detail, process history owned by current contracts/skills). Delete 4
rejected triplets whose premises are obsolete: the DeepReadonly proposal
(dev-invariants note now carries the alternative inline), the collapse
tool-owned presentation proposal (superseded by the shipped render-intent
union), retire-mid-turn-steering (steering is now load-bearing across
plan-mode/apiproxy/TUI), and single-session-ACP (automation-only ACP
resolved the question; multi-session isolation is pinned by tests).

Repair every inbound link: retarget intentional historical citations to
archived paths, replace decision-current citations with the surviving
authority, and fix the stale example-execute-over-tsx pointer in
pnpm-workspace.yaml. Re-record pairing sidecars and seal the archive
manifest (append-only; existing seals unchanged).
This commit is contained in:
Tianyi Cui
2026-07-27 23:56:18 +08:00
parent 0dcdf188e2
commit f2c0021941
103 changed files with 203 additions and 389 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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-02-remove-stream-chunk-mirror.md: 99086a179dfd1bbaac570e437a66b9be19c05ef4
2026-07-02-remove-stream-chunk-mirror.zh.md: 7a03e476dd8d0ba725d16d220e9aef3a3fc445f6

View File

@@ -0,0 +1,48 @@
# Agent Note: Stop mirroring the token stream as an agent event
Status: implemented
Archived: 2026-07-27
English | [中文](2026-07-02-remove-stream-chunk-mirror.zh.md)
## Problem
The loop records every model token delta as a durable `assistant/chunk` session event AND emitted a parallel live `agent/stream-chunk` Cordis event carrying the identical data. In `packages/core/agent-loop/src/agent.ts` the two sat one line apart:
```ts ignore-check
const chunkEvent = session.append('assistant/chunk', { turn, step, chunk })
chunkSeqs.push(chunkEvent.seq)
ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror
```
- Durable: `assistant/chunk: { turn, step, chunk }`.
- Live emit: `agent/stream-chunk(agent, turn, step, chunk)` — same `StreamChunk`, same `turn`/`step`.
The only thing the emit added over the session event was the live `Agent` handle, and the sole consumer discarded it (its handler signature was `(_agent, _turn, _step, chunk)`).
This is the same duplication the [boundary-mirror removal](2026-06-20-remove-agent-boundary-mirror-events.md) eliminated for turn/step boundaries: a consumer had two sources of truth for one durable fact, and every change had to touch both. That Agent Note deferred the chunk stream ("`assistant/chunk` persistence remains load-bearing, so the chunk stream could later be evaluated as a mirror, but that is a separate decision") rather than bundling it in. This Agent Note is that separate decision.
The premise the deferral hinged on is settled: chunk persistence is authoritative and staying. The proposal to stop persisting chunks and keep only a transient live stream event was [rejected](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md) — high-fidelity replay, partial failed streams, and snapshot replay all depend on the persisted `assistant/chunk` feed. So `assistant/chunk` on `session/event` is the durable, load-bearing token stream, and `agent/stream-chunk` is a pure redundant mirror of it.
## Decision
Remove `agent/stream-chunk` from the agent event taxonomy. The token stream is read off `session/event` as `assistant/chunk`, the same feed persistence and replay already use — `session/event` is the single live transcript stream (assistant chunks, turn/step boundaries, tool activity, todos).
**Consumers.** Persistence, replay, and interactive renderers consume the authoritative session stream directly. The [automation-only ACP bridge](2026-07-23-acp-automation-only-protocol.md) emits committed `assistant/message` text rather than raw chunks, so it needs neither event. No production consumer requires an `Agent`-first token mirror.
## Scope
Removed: `agent/stream-chunk`.
Not touched:
- `assistant/chunk` (the durable session event) — the authoritative token stream, kept exactly as-is. This Agent Note removes the LIVE MIRROR, not the persistence (the persistence-removal proposal was separately rejected — see above).
- `agent/steering` — not touched by THIS decision (a control signal, not the token stream). Its durable twin is `steering/message`, and the mirror emit was removed by its own follow-up: [Remove the `agent/steering` mirror emit](../../archived/simplification/2026-07-04-remove-agent-steering-mirror.md).
- `agent/status`, `agent/error`, `agent/created`/`agent/disposed`, `agent/queued`, `agent/session-start` — lifecycle/control events that are not transcript data and have no durable duplicate.
## Alternatives considered
**Remove the persistence and keep only a transient live stream** — the inverse cut, [rejected separately](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md): high-fidelity replay, partial failed streams, and snapshot replay all depend on the persisted `assistant/chunk` feed. With that settled, the live emit is the redundant half of the pair.
## Consequences
A plugin can no longer observe token deltas from an `Agent`-first event. It subscribes to `session/event`, filters `assistant/chunk`, and looks up the corresponding live handle directly with `ctx.agents.get(session.id)` when needed. No production consumer needed the live `Agent` at chunk time; this is the same acceptable trade the boundary-mirror removal made.

View File

@@ -0,0 +1,48 @@
# Agent Note: 停止将 token 流镜像为 agent 事件
Status: implemented
Archived: 2026-07-27
[English](2026-07-02-remove-stream-chunk-mirror.md) | 中文
## 问题
agent loop智能体循环将模型的每个 token delta 同时记录为持久的 `assistant/chunk` 会话事件,并发射一个携带相同数据的并行实时 `agent/stream-chunk` Cordis 事件。在 `packages/core/agent-loop/src/agent.ts` 中,二者仅相隔一行:
```ts ignore-check
const chunkEvent = session.append('assistant/chunk', { turn, step, chunk })
chunkSeqs.push(chunkEvent.seq)
ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror
```
- 持久事件:`assistant/chunk: { turn, step, chunk }`。
- 实时发射:`agent/stream-chunk(agent, turn, step, chunk)`——相同的 `StreamChunk`,相同的 `turn`/`step`。
实时发射相比会话事件唯一多出的东西是实时的 `Agent` 句柄,而唯一的消费方直接丢弃了它(其处理函数签名为 `(_agent, _turn, _step, chunk)`)。
这与[移除边界镜像](2026-06-20-remove-agent-boundary-mirror-events.md)为轮次/步骤边界消除的重复相同:消费方面对同一持久事实的两个真源,每次变更都必须同时触及两者。该 Agent Noteagent 决策记录)没有把分片流一并纳入,而是推迟处理(“`assistant/chunk` 持久化仍承载关键约束,所以以后可以将分片流作为镜像评估,但那是一项独立决策”)。本 Agent Note 就是那项独立决策。
推迟所依赖的前提已经明确:分片持久化是权威的,且将保留。停止持久化分片、仅保留瞬态实时流事件的提案已被[否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md)——高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 序列。因此 `session/event` 上的 `assistant/chunk` 是持久的、承重的 token 流,而 `agent/stream-chunk` 是它的纯冗余镜像。
## 决策
从 agent 事件分类体系中移除 `agent/stream-chunk`。token 流通过 `session/event` 以 `assistant/chunk` 的形式读取——持久化与回放已经使用的正是同一个序列。`session/event` 是唯一的实时 transcript文本记录assistant 分片、轮次/步骤边界、工具活动、todo
**消费方。** 持久化、回放和交互式渲染器直接消费权威的会话流。[仅面向自动化的 ACPAgent Client Protocol桥接层](2026-07-23-acp-automation-only-protocol.md)发出已提交的 `assistant/message` 文本而非原始分片,因此两种事件它都不需要。没有生产消费方需要一个 `Agent` 优先的 token 镜像。
## 范围
移除:`agent/stream-chunk`。
未触及:
- `assistant/chunk`(持久会话事件)——权威 token 流,原样保留。本 Agent Note 移除的是实时镜像,而非持久化(移除持久化的提案已单独遭到拒绝——见上文)。
- `agent/steering`——本决策未触及(它是控制信号,不是 token 流)。其持久孪生事件是 `steering/message`,镜像发射由其自身的后续 Agent Note 移除:[移除 `agent/steering` 镜像发射](../../archived/simplification/2026-07-04-remove-agent-steering-mirror.md)。
- `agent/status`、`agent/error`、`agent/created`/`agent/disposed`、`agent/queued`、`agent/session-start`——生命周期/控制事件,不是 transcript 数据,也没有持久副本。
## 曾考虑的替代方案
**移除持久化、仅保留瞬态实时流**——反向裁剪,已被[单独否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md):高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 序列。在此前提确定后,实时发射才是配对中冗余的那一半。
## 后果
插件不能再从 `Agent` 优先事件观察 token 增量。它需要订阅 `session/event`、过滤 `assistant/chunk`,并在需要时通过 `ctx.agents.get(session.id)` 直接查找对应的实时 handle。没有生产消费方需要在分片时刻取得实时 `Agent`;这与移除边界镜像所作的取舍相同,均可接受。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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-22-tui-titles-from-session-title-service.md: 946ef283f91249c70f82280b754ef991665dbaa3
2026-07-22-tui-titles-from-session-title-service.zh.md: f2fa4772c9fa404540dcf8a514cf571a284d4967

View File

@@ -0,0 +1,36 @@
# Agent Note: TUI titles come from the session-title service
Status: implemented
Archived: 2026-07-27
English | [中文](2026-07-22-tui-titles-from-session-title-service.zh.md)
## Problem
A per-session title makes terminal panes and tabs distinguishable, but a TUI-local model call would create a second title pipeline beside [log-backed session titles](../feature/2026-07-21-log-backed-session-titles.md). The local path needs its own prompt, cap, one-shot latch, resume derivation, cancellation, and failure fallback, while its process-local result remains invisible to session listings, forks, Web consumers, and replay. If both paths run, one session can also be titled twice by different strategies.
## Decision
The session-title service is the one title source. The TUI contains no `autoTitle` config, title-model request, latch, abort controller, prompt, or output cap. It folds the latest logged title on mount (`foldSessionTitle`), renders it as the banner subtitle, and calls `runtime.terminal.setTitle` with `<session title> — <configured title>` on every accepted `session/title` event. The same terminal-safe OSC 0 path handles the configured fallback title, resumed sessions, and live revisions without renaming tmux windows or adding another terminal-control surface.
Model-made titles are a composition choice: `examples/tui-agent/cordis.yml` (and the scripted PTY fixture) mount `@deepseek-ai/dsh-session-title-first-message-llm`, which inherits the main request's route and replaces the spine's deterministic fallback with a short model summary. Deployments without the provider keep the fallback title from `dsh-agent-spine-demo`'s bundled `SessionTitleService`.
## Alternatives considered
**Keep both, letting the logged title win.** This was the first merge resolution: auto-title owned the whole window title until a logged `session/title` arrived in suffix form. It preserved behavior but doubled the model calls on every fresh session and left the TUI's title unobservable in the log, violating model-visible ⟺ logged in spirit and splitting the title contract across two owners.
**Port auto-title's prompt and cap into the service as a third provider.** The first-message-llm provider already exists with the same cadence, a reviewed prompt contract, durable request records, and supersession fencing; a second near-identical provider would be pure duplication.
**Use only a truncated first prompt or only a model title.** A deterministic fallback provides an immediate, free title, while an optional model provider improves quality without delaying the main turn. Forcing either strategy removes that deployment choice.
**Make model titles a TUI default or block the first turn for them.** The cost and route belong to composition, and auxiliary title latency must stay off the interaction critical path. The TUI consumes accepted state instead of owning generation policy.
**Rename a tmux window or use a separate terminal escape.** Rejected because the existing terminal adapter's OSC 0 path labels the pane or tab without acquiring tmux ownership or adding a second control API.
## Verification
TUI tests pin restored and live `session/title` consumption, terminal-safe title rendering, the configured fallback, and the absence of a TUI-owned model path. The keyless PTY smoke boots the real composition, accepts a logged provider title, and observes the resulting terminal title. The [log-backed title decision](../feature/2026-07-21-log-backed-session-titles.md) owns provider, persistence, resume, fork, cancellation, and stale-completion coverage.
## Consequences
One title pipeline is durable, replayable, visible to every consumer, and fenced against stale completions by the service. The TUI has no `llm`-streaming title path. Model quality requires a provider plugin in the composition, while deployments without one keep the deterministic fallback; the terminal title consistently uses the suffixed `<title> — <product>` shape.

View File

@@ -0,0 +1,36 @@
# Agent Note: TUI 标题来自 session-title 服务
Status: implemented
Archived: 2026-07-27
[English](2026-07-22-tui-titles-from-session-title-service.md) | 中文
## 问题
每会话标题让终端窗格和标签页易于区分,但 TUI 本地模型调用会在[日志承载的会话标题](../feature/2026-07-21-log-backed-session-titles.md)旁形成第二条标题管线。本地路径需要自己的提示词、截断上限、一次性闩锁、恢复推导、取消和失败回退而其进程本地结果仍对会话列表、fork、Web 消费方和回放不可见。若两条路径同时运行,同一会话还可能被不同策略命名两次。
## 决策
session-title 服务是唯一的标题来源。TUI 不包含 `autoTitle` 配置、标题模型请求、闩锁、abort controller、提示词或输出上限。TUI 在挂载时折叠最新的已记录标题(`foldSessionTitle`),将其渲染为横幅副标题,并在每个被接受的 `session/title` 事件上调用 `runtime.terminal.setTitle`,传入 `<session title> — <configured title>`。同一条终端安全的 OSC 0 路径会处理配置的回退标题、恢复的会话和实时修订,既不重命名 tmux 窗口,也不增加另一套终端控制接口。
模型生成的标题是组合选择:`examples/tui-agent/cordis.yml`(以及脚本化 PTY fixture挂载 `@deepseek-ai/dsh-session-title-first-message-llm`,它继承主请求的确切路由,用简短的模型摘要替换 spine 的确定性回退。未挂载该 provider 的部署保留 `dsh-agent-spine-demo` 内置 `SessionTitleService` 的回退标题。
## 备选方案
**两者并存,已记录标题胜出。** 这是第一版合并决议auto-title 独占整个窗口标题,直到已记录的 `session/title` 以后缀形式到达。它保留了行为,但每个新会话产生双倍模型调用,且 TUI 的标题在日志中不可观察,实质上违反 model-visible ⟺ logged并把标题契约拆给两个所有者。
**把 auto-title 的提示词和截断移植为服务的第三个 provider。** first-message-llm provider 已经存在,节奏相同,且有经过评审的提示词契约、持久的请求记录和替换围栏;再造一个近乎相同的 provider 纯属重复。
**只使用截断后的首条提示词,或只使用模型标题。** 确定性回退可以立即且免费地提供标题,而可选模型 provider 可以提升质量,不会延迟主轮次。强制采用任一种策略都会移除这项部署选择。
**让模型标题成为 TUI 默认行为,或为此阻塞第一个轮次。** 成本与路由归组合所有辅助标题的延迟不得进入交互关键路径。TUI 只消费已接受的状态,不拥有生成策略。
**重命名 tmux 窗口,或使用另一种终端转义序列。** 不予采纳,因为现有终端适配器的 OSC 0 路径可以标记窗格或标签页,无需取得 tmux 归属,也无需增加第二套控制 API。
## 验证
TUI 测试锁定恢复后和实时的 `session/title` 消费、终端安全的标题渲染、配置的回退标题,以及不存在 TUI 自有模型路径。无密钥 PTY 冒烟测试启动真实组合,接收已记录的 provider 标题,并观察由此产生的终端标题。[日志承载标题决策](../feature/2026-07-21-log-backed-session-titles.md)拥有 provider、持久化、恢复、fork、取消和陈旧完成结果的覆盖。
## 影响
唯一的标题管线持久、可回放、对所有消费方可见并由服务防止陈旧完成结果生效。TUI 不再有 `llm` 流式标题路径。若要提升模型标题质量,组合中必须挂载 provider 插件;未挂载的部署保留确定性回退。终端标题始终采用 `<title> — <product>` 后缀形式。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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: 9ad27ff8b2fe7a68511c01a1125dd01289aa4a7f
2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md: b01133fad22c8d8244d787f8740d2d1c29b2c41e

View File

@@ -0,0 +1,34 @@
# Agent Note: Consolidate gate scripts on already-present deps and builtins
Status: implemented
Archived: 2026-07-27
English | [中文](2026-07-26-consolidate-gate-scripts-on-existing-deps.zh.md)
## Problem
The `scripts/` gates mostly used the right tools (`node:fs` `globSync` in 15+ gates, mdast/micromark in the markdown gates), but a handful of stragglers hand-rolled what a sibling gate already did with an existing dependency or builtin:
- **Duplicated fence scanners.** `scripts/md-fences.ts` (~55 lines, consumed by `doc-typecheck.ts`) and `extractEquivBlocks` in `scripts/verify-type-equiv.ts` (~39 lines) were two copies of the same regex line-scanner for fenced code blocks, while `scripts/verify-mermaid.ts` already extracted fences by visiting mdast `code` nodes — and `markdownProseLines` in `scripts/markdown.ts` itself parsed to mdast but then hand-tracked fence state with a second regex. The regex scanners only recognized backtick fences at column 0, so they silently disagreed with the mdast-based gates on tilde and indented fences.
- **Hand-rolled argv parsing.** `parseOptions` in `scripts/publint-all.ts` and its near-identical copy in `scripts/verify-built-package-invariants.mjs` (~26 lines) stepped argv indexes manually, while sibling scripts (`verify-runtime-closure.ts`, `build-exe-for-python-sdk.ts`, `packages/sdk/scripts/src/args.ts`) already used the `node:util` `parseArgs` builtin.
- **Hand-rolled directory walks.** Five sites re-derived nested `readdirSync` walks that `globSync` covers: `verify-runtime-closure.ts` (packages + vendor manifests), `dev-web.ts` `discoverPluginDirs`, `verify-package-paths.ts` `realPackageNames`, `verify-client-domain-graph.ts` `listSources`, and `publint-all.ts` `addPath` (~5565 lines total). `scripts/package-invariants.ts` shows the one-line `globSync` template.
No new dependency was needed anywhere; every replacement is an existing devDep or a Node builtin.
## Decision
- A shared mdast fence helper, `markdownFences` in `scripts/markdown.ts`, visits `code` nodes for the language, full info string, body, and 1-based opening-fence line; `doc-typecheck.ts` and `verify-type-equiv.ts` extract fences through it. `md-fences.ts` and the duplicated `extractEquivBlocks` scanner are deleted, and `markdownProseLines` derives fenced lines from the parsed `code` nodes' positions instead of a second regex.
- Both CLIs parse argv via `parseArgs`; unknown options and missing values still fail loud, with `parseArgs`'s own error text instead of the bespoke usage strings.
- The five straggler walks use `globSync`. The walks in `check-workspace-constraints.ts` and `clean.ts` stay: they need dirent-level detail to diagnose malformed trees, which glob-by-pattern cannot report.
## Alternatives considered
- **A new glob/walking dependency (`tinyglobby`, `fdir`).** Rejected: the builtin already won repo-wide; these were stragglers, not a gap.
- **`p-map` for `publint-all.ts`'s ~19-line ordered worker pool.** Deliberately left out: one new devDep for one small deletion is at the edge of the [dependency policy](../process/2026-07-26-dependencies-over-hand-rolling.md) bar, and the pool's requirements (bounded workers, deterministic order, env override) are documented in the [parallel-gates note](../process/2026-07-06-parallel-pre-push-gates.md). Fold it in only if `p-map` earns a second consumer.
- **Leaving the fence scanners.** Rejected: two drifting copies of a parser beside a third correct implementation is exactly the duplication the shared `markdown.ts` helper exists to prevent, and the column-0-backtick-only limitation was a latent inconsistency between sibling gates.
## 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` 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.)

View File

@@ -0,0 +1,34 @@
# Agent Note: 把门禁脚本统一到已有依赖与内置模块上
Status: implemented
Archived: 2026-07-27
[English](2026-07-26-consolidate-gate-scripts-on-existing-deps.md) | 中文
## 问题
`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`(合计约 5565 行)。`scripts/package-invariants.ts` 展示了一行式的 `globSync` 模板。
所有替换都不需要引入新依赖;每一处替换用的都是既有的 devDependency 或 Node 内置模块。
## 决策
- `scripts/markdown.ts` 中的共享 mdast 围栏辅助函数 `markdownFences` 访问 `code` 节点,读取语言、完整 info string、块体以及以 1 起始的开围栏行号;`doc-typecheck.ts``verify-type-equiv.ts` 通过它提取代码围栏。`md-fences.ts` 和重复的 `extractEquivBlocks` 扫描器已删除,`markdownProseLines` 也改为从解析出的 `code` 节点位置推导围栏内的行,而不再用第二个正则。
- 两个 CLI 都改用 `parseArgs` 解析 argv未知选项和缺失取值仍然大声失败只是错误文案换成了 `parseArgs` 自带的文本,而非原先手写的用法字符串。
- 那五处掉队的目录遍历改用 `globSync``check-workspace-constraints.ts``clean.ts` 中的遍历保留:它们需要 dirent 级别的细节来诊断结构异常的目录树,按模式匹配的 glob 报告不了这些信息。
## 曾考虑的替代方案
- **新的 glob/目录遍历依赖(`tinyglobby``fdir`)。**不予采纳:内置模块已在全仓库范围内胜出;这几处只是掉队者,不是能力缺口。
- **用 `p-map` 替换 `publint-all.ts` 中约 19 行的有序 worker 池。**刻意未纳入:为一次小删除引入一个新 devDependency正处在[依赖策略](../process/2026-07-26-dependencies-over-hand-rolling.md)门槛的边缘而且该池的需求worker 数量有界、确定性顺序、环境变量覆盖)已记录在[并行 pre-push 门禁决策记录](../process/2026-07-06-parallel-pre-push-gates.md)中。仅当 `p-map` 赢得第二个消费方时再顺带纳入。
- **保留这两个围栏扫描器。**不予采纳:在第三个正确实现旁边放着两份逐渐漂移的解析器拷贝,正是共享的 `markdown.ts` 辅助函数要防止的那种重复;「只认第 0 列反引号」的限制也是同类门禁之间的潜在不一致。
## 后果
- 只剩一个围栏解析器:所有 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 会在文件末尾静默闭合未闭合的代码块(其比较随后可能通过),因此共享辅助函数会报告闭合定界符是否存在,门禁在块未闭合时报错,保留了被删扫描器的这条拒绝路径。`doc-typecheck` 的扫描器本来就没有这条错误路径。
- `parseArgs` 对重复出现的选项保留最后一个值而不报错——一个测试未固定的开发工具边缘用例,作为删除两份手写解析器的交换被接受。(严格模式下,需要取值处遇到以 `--` 开头的 token 仍会拒绝,与被替换的解析器行为一致。)