# Changelog [3.3.3]: https://github.com/Fishsb/dsh-prompt-enhancer/compare/v3.3.2...v3.3.3 [Unreleased]: https://github.com/Fishsb/dsh-prompt-enhancer/compare/v3.4.2...HEAD [3.4.2]: https://github.com/Fishsb/dsh-prompt-enhancer/compare/v3.4.1...v3.4.2 [3.4.1]: https://github.com/Fishsb/dsh-prompt-enhancer/compare/v3.4.0...v3.4.1 [3.4.0]: https://github.com/Fishsb/dsh-prompt-enhancer/compare/v3.3.3...v3.4.0 [3.3.2]: https://github.com/Fishsb/dsh-prompt-enhancer/compare/v3.3.1...v3.3.2 [3.3.1]: https://github.com/Fishsb/dsh-prompt-enhancer/compare/v3.3.0...v3.3.1 本项目所有重要变更记录于此文件。 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)。完整发布说明见 [GitHub Releases](https://github.com/Fishsb/dsh-prompt-enhancer/releases)。 > 实测标注纪律:每条变更须在文末标注「已实测:<方式/等级>」;未实测不得合入(2026-08-24 门禁化)。 > 🗺️ 条目内的 `flow:` 标注为功能链路标签(原 pmg 项目地图 `docs/map/` 已随 pmg 于 2026-09-10 移除,该路径不再存在);agent 开工前先读 [`AGENTS.md`](AGENTS.md)。 ## [Unreleased] ## [3.4.2] - 2026-10-09 - **修复「Windows 宿主被自己的 worker 清扫杀掉」——win32 枚举只按进程名,把全机 node 进程(含宿主自身)当 worker 回收(2026-10-09)**:现象是 Windows 上宿主启动后**约 5 秒静默退出**(stdout / stderr 无任何读数、无栈、无 `fatal load failure`),前端页面已渲染但连接永不成代,界面**永久停在「自动重连中」**(用户侧第一反应是"服务起不来")。**根因**:`workerPidsByProcessScan()` 两条腿判据不一致——posix 腿遍历 `/proc//cmdline`,只认「某参数等于 worker 脚本路径或以 `asr-worker.cjs` 结尾」;**win32 腿只按进程名** `Get-CimInstance Win32_Process -Filter "Name='node.exe'"` 枚举,与本函数自述判据(「命令行里出现本插件的 `asr-worker.cjs` 路径」)相悖 ⇒ 返回本机**全部** node 进程。后果不是「少发现」而是「全都发现」:`sweepStaleWorkers()`(挂在 `ensureWorker()` **入口最前**、无条件执行,见 3.4.1 前的 PE-F02 条目)按「只留一个」把发现集其余项逐一 `process.kill()` ⇒ **宿主把自己杀掉**。该 win32 腿自 `8abe49c` 引入起**从未被执行**(开发机为 Linux;`.github/workflows/ci.yml` 只跑 `ubuntu-latest`;既有 `test/asr-worker-reap.test.cjs` 全用 `pgrep` / `/proc` / 硬编码 `/home/lk/...`,Windows 上跑不了)。**改前取证(本机 win32,真进程非夹具)**:① 直接调 `workerPidsByProcessScan()` ⇒ 返回 `[32392,12932,12352,39556,21496,3516]`——本机 6 个 node 进程**全部**入列,其中 `3516` 即调用者自身;逐 pid 核对命令行,含 `asr-worker.cjs` 者 **0 个**(100% 假阳性)。② `process.kill` 拦截取到决定性栈:`pid=42068 sig=undefined self=42068`,`at sweepStaleWorkers (lib/asr-models.cjs:486)` ← `at Object.ensureWorker (:509)` ← `at Timeout._onTimeout (lib/index.cjs:835)`(即 `apply()` 里那个 5s 定时器)。③ 用桌面端同款命令行连续启动宿主 3 次,均在打印 `dsh web: http://…/?token=…` 后约 5 秒静默死亡(exit 1)。**改动(最小)**:win32 腿补 `Where-Object { $_.CommandLine -like '*asr-worker.cjs*' }`,与 posix 腿同源;枚举结果排除 `process.pid`;`sweepStaleWorkers()` 的 kill 循环增加 `pid === process.pid` 跳过(纵深防御:发现面将来再退化也不得自杀)。未改 posix 腿、未改 keep 挑选策略、未改 worker 启停语义、未引新依赖。**反向自证(有牙)**:新增 `test/asr-worker-reap-win32.test.cjs`(3 条,已登记进 `npm test` 列表;编号用 `ASR-WIN*` 以避开 `asr-worker-linux.test.cjs` 已占用的 `ASR-W*`)——把 lib 改动**还原为改前版本**后 **ASR-WIN1(结构腿)与 ASR-WIN2(反向行为腿)报红**,WIN2 的失败读数即上述假阳性集合(原文:`进程扫描不得返回自身 pid 3516(返回它 = 清扫会杀掉宿主自己);实测 [32392,12932,12352,39556,21496,3516]`);打上修复后 **3/3 绿(连跑 3 次)**。**如实登记的边界(三条,不冒充)**:① `ASR-WIN3`(正向行为腿,用"命令行签名与 worker 相同"的替身进程取材)在改前改后**均绿**——它对本次 bug **无牙**,其存在只为堵「改成 `return []`」这类假修复(发现面归零同样坏,只是坏得安静)。② win32 每次扫描要起一次 PowerShell,机器繁忙时单次耗时可达模块内 5s 头寸(`spawnSync` timeout ⇒ stdout 空 ⇒ 返回 `[]`),此时 WIN3 打印 `[未取材]` 并跳过、**不冒充 PASS**(该分支已实测触发过一次)。③ **上游 CI 为 `ubuntu-latest`,win32 腿在 CI 上不被执行**,本条所有 win32 读数均为**本机实测**(非 CI 覆盖);`ASR-WIN1` 结构腿即为此设的 CI 可见信号(CI 上唯一能咬住这条腿的东西)。**已实测(方式/等级)**:上述全部读数(命令与原始输出见本次执行记录);`npm run gate -- --accept-unjudged=arch-claims,cards` ⇒ **PASS 3 · FAIL 0 · SKIP 2**,链级退出码 **0**(落在消费者声明的接纳基线内)。**发布前复核(2026-10-09 · 独立复跑,纠正本条原先的跨平台自述)**:本条首次提交时记录的「本机 win32 全量 `npm test` pass 298 / fail 8 / tests 307」系**提交者本机(win32)**读数,与维护侧复核读数不同,现按两侧实测分别订正并注明方法——① **CI(`ubuntu-latest`,Actions run 37877423288,PR #14 head)**:23 文件 / **tests 307 · pass 306 · fail 0**,job `test` `conclusion=success`,10 个步骤全绿(读数取自该 run 的 `test-report` artifact,`test-report.txt` 逐文件汇总;CI 上 `sys` 文件为 13/13/0);② **维护侧本机(Linux/WSL,逐文件隔离复跑)**:23 文件 / **tests 307 · pass 303 · fail 4**,失败为 `SYS-09` / `SYS-10`(各 1 例,同文件 13/11/2)+ `GATEEXIT-06` + `P5-10`。**四例均已定位、且与本改动无关**:`SYS-09` / `SYS-10` 的失败**根因是本机 3080 被宿主自身占用**——用例 POSIX 分支的前提写明「本机 3080 空闲(CI runner 无 DSH 服务)」(见 `test/sys.test.cjs:311-313`),而本机当时正跑着 `dsh web --port 3080`(pid 124566 监听 127.0.0.1:3080),同步原语 `dshPrimPortHolder` 真探到监听者 ⇒ 走「有监听者」分支、返回项形状少一个 `warn` 字段,断言 `['detail','key','level','ok']` vs 期望 `[...,'warn']`;**对照实测**:在独立网络命名空间内(`unshare -rn` + `ip link set lo up`,使 3080 在本进程内不可见)同法复跑 `test/sys.test.cjs` ⇒ **13/13/0 全绿**,而 `gate-exit` / `ring-state` 在该隔离环境下**读数不变**(仍各 1 例红)⇒ 证明本机红项由**外部环境占用 3080** 引起、非代码缺陷、非本改动引入;`GATEEXIT-06` / `P5-10` 为 `[3.4.1]` 条目已登记的**既有红项**(见下方 3.4.1 区块),本轮同样复核为既有状态。**边界**:未 bump `version`、未打 tag / 建 Release、未改 `cordis.patch.yml` 与槽位契约、未动 posix 腿与 Windows 端口通道、未手改生成物(`plugin-host.js` / `lib/client.cjs` 未受影响,`npm run build` 后 `git diff` 为空)。— [PE-F02](flow: 语音 ASR 链) ## [3.4.1] - 2026-10-06 - **修复「插件在新版 DSH 上被拒绝装载」——移除已被官方删除的 `@deepseek-ai/dsh-client-runtime` 声明 + 放宽 locale 区间(2026-10-06)**:现象是远端 **issue #11 / #12**(用户在 DSH `0.2.0-rc.2` 上装/启 `dsh-prompt-enhancer@3.4.0` 被提示「不兼容」,✨/🎤 与设置页卡片消失)与本机宿主日志 `skipping profile bundle "dsh-prompt-enhancer"` 的同根因。**根因(两条叠加,非单一)**:① 该包**自 DSH `0.2.0` 起已从发行物中消失**(0.2.0-rc.2 的 62 个 `dsh-client-*` 包名中无它;npm 末版 `0.1.1-rc.2` @2026-08-21 后停发),而它同时被列在 `peerDependencies` 与 `dsh.client.inject`;② `dsh-client-locale` 的区间写作 `^0.1.0-rc.6`——**caret 在 0.x 上只允许 0.1.x**,故对 `0.2.0-rc.2` 判 false。宿主的 compatibility 门(`dsh-app-boot/lib/index.js` `evaluatePluginCompatibility`,只审 `peerDependencies` 里 `@deepseek-ai/dsh*` 命名空间)因此 deny 整个 bundle。**改前取证(先证后改,逐处列出)**:`src/`、`lib/`、`scripts/`、`test/`、`cordis.patch.yml`、`plugin-host.js`、`README*` 对 `dsh-client-runtime` **全部零命中**;`lib/client.cjs` 唯一 `require` 是 `'react'`(`@deepseek-ai/dsh-client-ui-renderer` 的 3 次出现**全在注释文本内**,非 require);全仓仅 `package.json`(peer + inject 各 1 处)与 `package-lock.json`(root 条目镜像 + 传递依赖,非我方声明)出现该名 ⇒ 判定为**「仅声明、无真实引用」⇒ 直接移除**,未做任何版本放宽或兼容层(放宽只对「仍被使用但版本不匹配」成立)。**改动**:`package.json` 的 `peerDependencies` 与 `dsh.client.inject` **同时**删去该名,locale 改为 `>=0.1.0-rc.6`(保持两处包名集合逐项相等);`package-lock.json` root 条目同步(以 `npm install --package-lock-only` 实算结果逐字节比对一致)。**反向自证(防判据没牙)**:同一判据函数两侧对照——改前 manifest vs `0.2.0-rc.2` ⇒ `REJECT`(列出 locale + runtime 两名不满足,exit 1);改后同输入 ⇒ `PASS`(exit 0)。同法在**真实装载门** `loadProfileDirectory` 上对照:改前 `skippedBundles = 1`(`✗ dsh-prompt-enhancer :: incompatible`)/ 改后 `skippedBundles = []`(21 个 bundle 全部装载,插件在册)。**semver 双向实算**:`>=0.1.0-rc.6` 对 `0.1.0-rc.6` / `0.1.0-rc.8` / `0.1.7-rc.2`(旧版)与 `0.2.0-rc.1` / `0.2.0-rc.2`(新版)**六值全 true**;对照原 `^0.1.0-rc.6` 对 0.2.x 三值全 false。**部署与生效复验(§6.3)**:`npm run build`(幂等,三产物 md5 与构建前一致)→ `build-host/build-client --check` **零漂移** → `sync-runtime` 部署到 system + web 两个 profile(19 项 md5 全过,运行环境副本与仓库 HEAD **逐字节一致**,部署账本更新为 `system@3.4.0 / web@3.4.0 (source=sync-runtime)`)→ **重启后复验**:① 宿主启动日志 `skipping profile bundle` 由 **1 处 → 0 处**;② 界面复验(独立实例 + CDP)`.dsh-enh-btn` 与 `.dsh-vi-btn` **各渲染 1 个**,插件样式已注入,设置页出现「✨ Prompt Enhancer」导航项且卡片本体渲染(`.dsh-plg-root`×1 / tab×3 / `.dsh-plg-mselect`×8);③ RPC 复验:`config/get`、`models/list` 返回真实数据,**增强链一次真实调用**成功(返回 `{ok:true, text:"请编写一个排序函数…", model:"deepseek-v4.1-flash"}`);同机对照——重启后 `POST /dsh-prompt-enhancer/rpc` **HTTP 200**,而改前该端点与任意不存在路径同为 **405**(不可区分)。**已实测**:上述全部读数(命令与原始输出见 `docs/internal/适配勘察-2026-10-06.md` 与本次执行记录);`build-host --check` / `build-client --check` 零漂移。**未实测/如实登记**:`npm test` 本次 **302/304**——两处失败(`GATEEXIT-06`、`P5-10`)经**临时回退到改前基线复跑同样失败**,判定为**既有状态、与本改动无关**;`npm run gate` 第 4 环 `arch-claims` 报「投影读数漂移」(判据面指纹 `sha256:a664b8bd8ad77f94` 未变)同样在改前基线**逐字复现**,亦属既有状态,**本轮未处置**(跑 `--write` 重生成投影或修该环不在本任务边界内)。**边界**:未 bump `version`(仍 `3.4.0`)、未打 tag / 建 Release / 推送发布、未向本地 profile 执行安装动作(仅经 `sync-runtime` 落运行环境)、未改槽位契约与设置卡片体系、未加 npm 通道、未用 `allow-version` 例外冒充修复、未手改生成物、未动 `.gitignore`。— [PE-F01](flow: 装配与加载链) - **独立复核 ASR worker 两轮修复:发现并补上一条「假绿判据」(2026-09-27 三轮 · 复核席)**:接手时该两轮改动**未提交**(工作区在途 4 小时无写入),按纪律先做独立复核而非采信自述。**复核结论:两轮修复的实现本身正确**(平台分派逐字核对:`win32` 走 `netstat -ano`,posix 走 `ss -ltnp` → 退路 `netstat -tlnp`;新增 `workerPidsByProcessScan` / `sweepStaleWorkers` 在位;`npm test` 303/303)。**但变异自证不成立**——实测把 `workerPidsByProcessScan` 变异为 `return []` 后,声称钉住该变异的 **ASR-R1b 与 ASR-R3 仍全绿(7/7)**。根因:两条用例都经 `workerListenerPids()`(= 端口通道 ∪ 进程扫描),而真实 worker 常驻 **3082 固定口**,端口通道照样发现它 ⇒ 该变异在被测面上**无差别**(判据无牙,假绿)。**修法(最小)**:① 导出 `workerPidsByProcessScan`(原为内部函数,判据无法直接断言这条腿);② 新增 **ASR-R6 直接腿**——不经端口通道,直接要求进程扫描自主枚举出真实 worker,并反向断言「不得返回非 worker 进程」。**有牙实测**:注入 `return []` ⇒ **仅 ASR-R6 报红**(7 pass / 1 fail);还原 ⇒ 8/8 绿。**边界**:只加一处导出 + 一条用例,未改任何行为语义、未动 Windows 路径。`npm test` **304/304/0**;门禁五环 `PASS 3 · FAIL 0 · SKIP 2`;产物三查零漂移。— [PE-F02](flow: 语音 ASR 链) - **补 ASR worker 的「回收」与「防护」——事故根因之外的结构缺口:发现面只覆盖至多 2 个 worker + 全链路无任何回收机制(2026-09-27 二轮)**:上一笔修好了 Linux 通道的发现(`isWorkerUp()` 由恒 false 转为可判定),但**审下来另有两个独立缺口**,它们才是「为什么能堆到 30 个」的直接原因。**缺口①:发现面结构性不完整。** `worker.port` 是**单值文件**(worker 侧 `writeFileSync` **覆盖**写,见 `asr-worker.cjs` `onListening`),N 个 worker 竞争写同一个文件 ⇒ 只留下**最后一个**的口;加上 3082 固定口,`workerListenerPids()` 的 `ports` 集合恒为**至多 2 个**。故 v4.8 自称修好的「重复 spawn 造成进程堆积」在动态口路径上**只修好了最后一个**:拿事故当时的真实快照复放(30 个 worker 各占一个动态口)⇒ 端口通道只能看见 **2 个**,其余 **28 个(93%,≈11.5GB)永久隐身**且永远 `kill` 不到。**缺口②:全链路无回收。** worker 以 `detached: true, stdio: 'ignore'` 启动(`restartWorker`),与 host **无父子生命周期绑定**;`asr-worker.cjs`(192 行)内**无** `setTimeout`/`setInterval`/`process.exit`/`server.close` ⇒ 无空闲 TTL;插件侧 `maxWorker|quota|watchdog|sweep|reap|prune` 命中**全为 0**,`onunload|dispose|deactivate|teardown` 命中**全为 0** ⇒ 卸载/停用插件也**不清 worker**。即:一旦堆积,**没有任何机制会把它收回来**,只能人工清理或重启整机。**修复**:① 增加 `workerPidsByProcessScan()`(posix 遍历 `/proc//cmdline` 匹配 worker 脚本路径 + 属主校验;win32 走 PowerShell `Get-CimInstance`),与端口通道**取并集**——发现面自此与 worker 数无关;② 增加 `sweepStaleWorkers()`:枚举全部 worker,**优先保留监听 3082 的那个**,其余全部清掉;③ 把它挂在 `ensureWorker()` **入口最前**(先于全部三处早退分支)。**③ 的位置是端到端实测逼出来的**:初版把清扫放在 `modelInstalled` 判定**之后**,实测在「模型未装」的机器上人为堆 6 个 worker 后调 `ensureWorker()` ⇒ 返回 `model-not-installed` 直接早退,**worker 数 6→6 完全不自愈**;移到入口最前后同一场景 **11→1**(`{killed:10, kept:621514}`,3082 仍在)。**已实测**(真进程,非夹具):人为堆 7 个 ⇒ 端口通道见 **2** 个 / 新发现面见 **7** 个;`sweepStaleWorkers()` 收敛 **7→1** 且幂等(再扫 `killed:[]`);端到端 `ensureWorker()` 在堆积 11 个时收敛到 **1**;`npm test` **303/303/0**(本轮 +7)。**变异自证 5/5 全红**:撤掉进程扫描兜底(1 红)· 移除清扫调用(2 红)· 清扫移到判活之后(1 红)· `keep` 不再来自发现集合(1 红)· **清扫移回早退分支之后(1 红,即上述实测抓到的缺口)**。⚠ 其中「撤掉进程扫描兜底」**初版判据漏判**(纯文本断言 ⇒ 变异 pass 7/fail 0 = 假绿),补了**行为腿**(临时把 `worker.port` 指向无效口,要求进程扫描仍能发现真实 worker)后才有牙。**边界**:未改 worker 自身启停语义、未改 Windows 服务路径、未引新依赖;清扫只杀**本插件** `asr-worker.cjs` 且按属主校验,不碰他人进程。— [PE-F02](flow: 语音 ASR 链) - **修复 asr-worker 结构性泄漏(Linux/WSL 通道)——同机累积 30 个 worker 共 9.6GB,压满客机 swap(2026-09-27)**:`workerListenerPids()` 的 v4.8「统一发现」只在 **Windows** 成立:它无条件 `spawn netstat -ano` 且只认 `LISTENING` 字样,而 Linux 上 ①多数发行版默认不再预装 net-tools ⇒ 命令不存在(本机实测 `netstat: command not found`)②即便存在,Linux netstat 输出的是 `LISTEN` 而非 `LISTENING`。两种情形都让该函数**恒返回 `[]`** ⇒ `isWorkerUp()` 恒 `false` ⇒ `ensureWorker()` 永不命中 `worker-already-up` 短路、`restartWorker()` 杀旧循环杀掉 **0 个** ⇒ **每次 host 启动/模型切换新增一个 ~420MB worker 且永不回收**。**实测事故面**:本机累积 **30 个** worker(合计 **9.6GB**,各自监听不同动态口),叠加既有 `app.slice` 9G 护栏后把 16G 客机的 4G swap 压到 **4096/4096 全满**、宿主物理可用跌至 **2.6GB**(该机 2026-09-27 曾因此整机卡死)。**修复**:posix 走 `ss -ltnp`(iproute2 主路径,**`-p` 才有 pid**)→ `netstat -tlnp`(net-tools 退路),解析口径与 `sys.cjs` 的 `dshPrimPortHolder` 同源(`LISTEN` + `:PORT` 锚定 + `pid=N` 提取);win32 分支**逐字未动**,Windows 行为零变化。**已实测**(本机 Linux,真数据取材):`isWorkerUp()` **false → true**;`ensureWorker()` 由「每次 spawn」变为返回 `{"ok":true,"skipped":"worker-already-up"}` 且 worker 数 **1→1 未新增**;清理 29 个残留后 60s 观察**无回升**;`npm test` **296/296/0**(新增 7 条,含 2 条**行为腿**——注入 `spawnSync` 桩观测真实调用序列,因纯文本断言实测被变异骗过)。**变异自证 4/4 全红**:退回无条件 `netstat -ano`(复现事故,2 红)· `LISTEN`→`LISTENING`(3 红)· 端口锚定丢 `$`(1 红)· `ss` 去 `-p`(1 红)。**边界**:只改 `lib/asr-models.cjs` + 登记一条测试(无新依赖、未改 worker 启停语义、未动 Windows 路径)。⚠ **运行中的 host 须重启 DSH 才吃到修复**(Node `require` 缓存:盘上文件改动对已加载该模块的运行实例无效)。— [PE-F02](flow: 语音 ASR 链) - **门禁三支成果首次合体(T1/P5 环级机读状态出口 + T3/N3 链级三态退出码 + M-1/P4 投影漂移归因)——合并期语义对齐(2026-09-27)**: 三条分支各自独立开发并各自通过复核,但**从未共存过**:T1 交付环级 `RING-STATE` 契约(五环自报、`gate.mjs` 删尽文本启发式)、T3 交付链级三态退出码(0 全过 / 1 有 FAIL / 2 未判全 + `--accept-unjudged` 基线)、M-1 交付投影漂移的归因两分。合体时暴露四处**只有共存才显形**的语义冲突,逐条裁定并修复: ① **`UNKNOWN` 与 `SKIP` 必须分开**(T3 裁定优先于 T1 旧口径):T1 原将「环零输出 + exit 0」判 `SKIP`,而 `SKIP` 可被接纳基线吸收——两者同码时,**把环脚本换成 0 字节空文件即可让链级报 0**,正是本专题要堵的假绿。按 T3 裁定改为 `UNKNOWN`,且**永不纳入基线**(实测:0 字节 `arch-claims` + 完整基线 ⇒ 链级仍 `exit 2`)。 ② **`missing-doc` 落 SKIP 而非 FAIL**:治理档不在(CI/干净 clone 必然缺位,gitignore 覆盖)是**结构性缺位**,不是漂移;记 FAIL 会产出「`RING arch-claims FAIL` 而 `exit 0`」的自相矛盾(干净 clone 实测踩到)。 ③ **环级状态行的两处接线**:合并后 `gate.mjs` 出现 `n()` 未定义(静默模式与 `--json` 模式退出码分叉:前者抛异常退 1、后者正确退 2)与 `verdictOf` 别名未接线——**两者都通过 `node --check`**,只在实跑时暴露。 ④ **测试期望对齐契约**:三处用例的期望写在裁定之前(P5-05 期望 `SKIP`、P5-08/GCAT-1 期望「无 FAIL 即 exit 0」),按权威语义改为对齐三态;GCAT 夹具排除本地 `docs/internal`(该测试测的是**下探归因面**,与投影漂移无关,带上它会在任何含治理档的机器上必红)。 **已实测**(本机 + 干净 clone 双形态):`npm test` **289/289/0**(20 文件,含 T1 的 13 例契约测试与 T3 的 12 例);`npm run gate` 五环各自输出 `RING-STATE` 契约行;三态退出码 **2 / 0 / 1**(裸跑 / 显式接纳 / 有 FAIL);`gate.mjs` 内文本启发式标识符 **0 处**(`SELF_REPORTS` / `locateConclusion` / `scanLines` / `GENERIC_CONCLUSIONS` 全清零);`build-host` / `build-client` / `sync-prompts` 三处 `--check` 零漂移。 **边界**:未改任何环的判定语义(各环判定与退出码契约原样),未改 CI 工作项结构,未引入新依赖。— [PE-F01](flow: 装配与加载链) - **CI 门禁步补自身硬顶:三档上限由「取等」改「严格递增」,消除「超时吃掉读数」(零对外行为变更)**:背景——`.github/workflows/ci.yml` 只有作业级 `timeout-minutes: 15`(:12)与测试步 12min(:53),**Structure gates 步无自身 timeout**;而环上限 `RING_TIMEOUT_MS` 默认 **180000ms**,五环串行最坏 **5×180 = 900s 正好等于作业硬顶**——三档**取等 ⇒ 零余量**:任一环挂死到上限,作业级 `cancelled` 会先于步骤级兜底生效,**同时吃掉**其后所有步骤(Run tests / 产物一致性)与唯一 artifact(`test-report.txt`)⇒ 门禁与测试**一起**失去读数(正是本仓 P0 与「判据之间不得互相遮蔽」要防的静默面)。修复(两处,缺一不可——只加步上限无法在 900s 作业硬顶下得到严格递增):① `scripts/gate.mjs` 环上限 **180000 → 120000ms**(5×120 = **600s**);② gate 步补 `timeout-minutes: 12`(**720s**)。三档自此**严格递增**:**环之和 600s < 步上限 720s < 作业上限 900s**(余量 120s / 180s),且三档算式与取值依据**写进两处注释**(`ci.yml` 步上方 + `gate.mjs` 常量处)。**取 120s 的依据(本机实测,非估计)**:五环全跑合计 **580ms**(dead-code 104 · rpc 58 · prompts 65 · arch-claims 300 · cards 52)⇒ 单环 120s 仍有 **~400×** 头寸;步上限 720s 只做**兜底**(环之和已留 180s,正常路径下编排器先打完 5 行 `RING` + 汇总退出码再退出,**不被拦腰杀掉**——若被杀,未打印的 `RING` 行即丢读数)。**不短路语义未动**:`if: always()` 五步原样在位(Structure gates / Run tests / Upload test report / Build client bundle / Verify generated artifacts),timeout 与 `if: always()` 是**两个正交面**(前者决定本步何时停,后者决定其后步骤照常执行),注释内已显式写明「不得为压时限删任一者」。**已实测**:① YAML 经 js-yaml 解析通过,读回作业 15 / gate 步 12 / tests 步 12;② 三档机检 **600 < 720 < 900 => PASS**;③ **负控(证明判据有牙)**——删掉 gate 步 `timeout-minutes` 后 `arch-claims --check` 的 **B234-2 转红**(`✗ 结构判据冲突 1 条`),还原后逐字节一致;④ 环上限**非恒真验证**:`GATE_RING_TIMEOUT_MS=1` 时五环全部被 SIGTERM 杀,**5 行 `RING` 仍全打印**、汇总 `exit 1` ⇒ 环级超时**不短路**(这就是「超时也能留下读数」的直接证据);⑤ `npm run gate` 五环 **PASS 3 · FAIL 0 · SKIP 2**(与改动前基线同形,两个 SKIP 为本地治理档缺位的既有口径,非本批引入);⑥ `npm test` **257/257/0**(16 文件,30.7s);⑦ `node --check scripts/gate.mjs` 通过;⑧ 行尾 LF 无 CR 字节(S-8 契约面)。**未实测(明确边界)**:本机无 `gh`,**未能读真实 CI run** ⇒ 「作业级 cancelled 吃掉读数」这一现象在**真实 CI 上**未复现,本轮判据取自 `ci.yml` 三档取值的算术矛盾(900 = 900)+ 环级超时不短路的本机探针;推送后以真实 run 的 step 结论为准。— [PE-F01](flow: 装配与加载链) - **知识卡-架构档(gate 第 5 环)册壳豁免换口径:形态条件 → 显式声明行(T7·判据上界 A)**:`isShellBody()` 原以**结构性四条件**(首行含「知识卡册」+ 无二级小节 + 无卡字段行 + 措辞仅在块引用)判「空册壳」并**整体豁免**——豁免是**文件级全有全无**(被豁免文件脱离全部判据)。经独立复核核定:四条件**全部是形态条件**,仿冒者逐条复刻形状即同形(实测:/tmp 仿冒件在旧版 **EXIT=0 静默免检**)⇒ 属**判据上界**,不是判据,再往四条件里加条件只是把上界往后推一格。改为**显式声明行** `SHELL-DECL <本文件相对路径>`:① 必须**逐字点名自己**(写别的文件名不算数);② 行首锚定 + 整行匹配(夹在句子里的同串不构成声明);③ 至多一条,≥2 条视为声明有歧义 → fail-closed 不豁免;④ **册内有卡**(有卡条目小节或卡字段行)**一律不得被声明豁免**(防「谁想免检就在自己文件里加一行」这一退化)。真实面两册壳 `cards/how-to.md` / `cards/decision.md` 第 2 行各加自身声明;其**逐字副本**另存 `test/fixtures/shell-decl/`(md5 相等)作为「真实面回归」的证据面。**净增量只有一条 = 多要求一行自陈**(旧版对「无自陈但形态全中」判绿、新版落回未引用集判红);真正拦住仿冒件的仍是「有卡」「措辞位」两道形态闸,**不**写作「从此绕不过」。**残余①(判据内不可解,如实登记、不冒充已堵)**:**任何文件都能写下点名自己的那条声明** ⇒ 豁免集**无唯一性上界**——实测把真册壳那一行复制进另一份同形文件、只把文件名改成自己即同样被豁免,**无需碰真册壳那个文件**(探针第 ③ 组把它钉成可见断言,将来引入唯一性约束时该处应改为判红)。仓内可给的唯一增量是**可观测性**:豁免集/未引用集进机读面(`--json` 的 `summary.exempt`(含声明依据原串)与 `summary.unreferenced`),人读串 A-5「实测」行同时打印**豁免件数 + 文件名 + 依据**(只给名单等于让人自己去找凭什么);配套新增 `test/gate-shell-decl-consistency.cjs` 与 **`.gate-shell-decls.json` 台账(进版本控制)**——声明集**任何变化**都必须先改台账,即一次显式、可 diff、可回滚的提交(本仓 `docs/devref/` 被 `.gitignore:19` 忽略、声明行本身进不了 git,这是「可见性」在本仓唯一能落地的形态);台账**不**限制谁能加声明(那需要外部裁决面,仓内无此面)。另订正同段两处**已失效断言**:A-5 头注仍写仿冒件形状是「措辞落普通正文段」(旧口径语)、称 `cards/reference.md`「同时也是册壳」(它其实一直被卡表引用;真正的册壳是另两份,各 0 张卡)。**已实测**(worktree 分支,WSL):`node --check` 通过;全量 `npm test` **258/258/0(17 文件,含新增 1 项)**;`npm run gate` **5 环 → PASS 3 · FAIL 0 · SKIP 2**(`cards` 环在**无 `docs/devref` 的干净 checkout** 上如实报 SKIP,不冒充 PASS);把真实 `docs/devref` 呈现给 `--devref` 后该环 **13 通过 · 冲突 0 · SKIP 0 · 真判 3/3 · EXIT 0**,豁免行打印 `cards/decision.md` / `cards/how-to.md` 各带声明依据;探针四组全绿——真实面豁免集 = 两册壳(带依据)· **原样抄件 EXIT=1**(落未引用集 `cards/ghost.md`)· **无声明同形件 EXIT=1**(旧口径下此形状为 EXIT=0)· **改名点自己 EXIT=0(残余①实证)**。**边界**:未改任何判定语义(A-1/A-2/A-3、未闭环词表、退出码均原样)、未引入依赖、未动治理根(`/mnt/d/FF` 只读)、真实面零写入(全部夹具在 `os.tmpdir()`)。 - **gate 第 5 环退出码契约修复(F3:头注与实现相反 ⟹ 带真冲突仍 exit 0)**:红队审查 F3 实测——`scripts/card-arch-consistency.mjs` 原退出码是 `if (has('--check') && conflicts.length)`,而**当时的头注(:36)自称「--check(= 默认行为,gate 第 5 环显式写它)」**:同一份冲突数据「✗ 卡<->档冲突 1 条」红字照打,**不带旗却 exit 0** ⇒ 以退出码为准的调用方(含任何「先跑跑看」的人)把红读成通过(复现:无参 exit 0 / `--check` exit 1,**输出正文逐字相同**)。修复取**选项①「改实现让默认即门禁语义」**——有冲突一律 exit 1(**带旗与否一致**),头注三处(:41 退出码契约 / :61 用法 / :683 实现处)同步为逐字现状。**二选一的判据**:仅改头注(选项②)会留下一份「默认调用=无人看守」的门禁,与红队 F1/F2 同族(失败不可观测);且仓内**唯一调用方 `scripts/gate.mjs:48` 本就显式带旗**(`args: ['--check']`),改实现**零调用方受影响**——故取选项①。**`--check` 的语义随之收窄且据实声明**:它**不再改变任何退出码**,只改「未判全」那行读数措辞(门禁口径 / 人工直跑);保留该旗是因为 `arch-claims` **B234-1** 有硬门「实装 `--check` 的环在链上必须带 `--check`」(删旗会让第 4 环判红)。**未判全(SKIP)/读侧不可判仍 exit 0——这是链上硬约束、不是遗漏**:`gate.mjs:104` 以「code !== 0 → FAIL」归因,环若在未判全时退非零,「没判上」会被折进「判失败」,**SKIP 与 FAIL 不再可分**(反证已实测:临时注入 `if (partial) process.exit(1)` 后 `RING cards` 由 **SKIP** 变 **FAIL**,验毕即还原、diff 干净);链级 SKIP/PASS 退出码语义归 T3(`t-muii4b04`),不在本卡扩张。**新增可见性**:未判全时除尾行「⏭ …未判全…不记 PASS」外,另打一行 ⚠ 读数区分两态;**该行排在尾行之前**(gate 的归因 digest 取最后一条非噪声行,排在尾行之后会在带真冲突时把「✗ 卡<->档冲突 N 条」挤掉、使 FAIL 归因串变成这句读数——已实测守住:末行仍为 `✗ 卡<->档冲突 1 条:A-1`);`--json` 时该行改走 **stderr**,保证 stdout 仍是**可 parse 的单行 JSON**(头注的兼容性承诺,实测 `JSON.parse` 通过)。**已实测(四组合 + 回归)**:夹具(`--devref` 指向临时目录,一行 closed 卡 + 正文含「索引待修正」且无闭环标记)——干净×无旗 **exit 0** / 干净×带旗 **exit 0** / 冲突×无旗 **exit 1**(修复前为 **exit 0**)/ 冲突×带旗 **exit 1**;`npm run gate` **5 环 → PASS 3 · FAIL 0 · SKIP 2**(`cards=SKIP` 仍被 gate 自报结论行正确读作 SKIP,非 PASS 非 FAIL);`npm test` **257/257/0**;`build-host --check` / `build-client --check` 零漂移。**调用方全仓列举(tracked + gitignored 两遍 grep)**:① **唯一实际调用方** `scripts/gate.mjs:48`(显式带旗;CI 经 `.github/workflows/ci.yml:38` 的 `npm run gate` 触达)② `scripts/arch-claims.mjs:104` 只登记环名、不调用(其 :155 注释按 `has('--check')` 形态识别本环,本卡保留该形态故不受影响)③ `CHANGELOG.md:24` 为文字记述,非调用。**无任何调用方依赖旧语义**(旧语义本就无调用方,仅人工直跑可见)。— [PE-F01](flow: 装配与加载链) - **SYS-08 超时硬杀用例去 flaky:挂起来源由「黑洞地址」换为「环回停滞服务」(测试基础设施,不改产品代码)**:`test/sys.test.cjs` 的 `SYS-08 C-3 超时硬杀` 连 `http://10.255.255.1/` 指望 connect 挂起,实际**吃网络栈/路由行为**——实测直连该地址 3 次 = `exit 28(5020ms) / exit 7(31ms) / exit 7(29ms)`:首连挂起、随后被邻居/路由缓存的下一条不可达**秒回** `EHOSTUNREACH` ⇒ curl 秒退 **7**,探针根本等不到超时,断言 `code==='ETIMEDOUT'` 随机变红(同文件连跑 3 次 = OK/FAIL/FAIL 即此因)。**改法**:用例内起一个 `net.createServer` **停滞服务**(`listen(0,'127.0.0.1')` 取内核分配端口、连接必被 accept、服务端只读不写响应)⇒ curl 阻塞等响应直到 `-m` 到点,挂起变成**服务端行为**而非网络运气;探针 env 剥掉代理变量并加 `--noproxy 127.0.0.1`(**注意**:`*` 不在 `validateProbe` 参数字符集内,写 `'*'` 会先被判 `BAD_PROBE_ARG`)。用例改为三腿:**腿①**真实 curl + 真实 spawn/杀进程链路(POSIX);**腿②**经既有 `__setProbeExecutor` 注入缝钉住归一化契约(形状取自本机实测:`execFile` 超时拒绝 = `code:null / killed:true / signal:'SIGTERM'`,**不带 `'ETIMEDOUT'` 字样**——归一化读的是 killed/signal,不是某平台特有的 code),并断言硬超时确被透传;**腿③**反向对照(`exit 7` 必须原样透传,不得被误判为 ETIMEDOUT),防把失败做成不可观测。**已实测**:① `node --test test/sys.test.cjs` **连跑 10 次全绿(10/10,13 项全过)**;② **负控双变异**——抽掉 `lib/sys.cjs` 的 `execOpts.timeout` → **红**(真实 curl 挂满 8s 后 `actual: 28`,证明挂起真实存在)、抽掉 ETIMEDOUT 归一化行 → **红**(`actual: 'PROBE_FAILED'`);两轮均 `git diff --exit-code -- lib/sys.cjs` 零差异、md5 还原如初(`9970261e...`);③ **网络无关性直接取证**——在 `unshare -rn` 隔离网络命名空间(**只有 lo、无任何外网与路由**)内跑 A/B:**旧用例红(`actual: 7`,与线上现象同源)/ 新用例绿且连跑 10 次全绿**;④ 全量 `npm test` **257/257/0**;`npm run gate` 5 环 **FAIL 0**(`arch-claims`/`cards` SKIP 系 worktree 缺 `docs/devref/INDEX.md` 所致,与本改动无关)。**未实测**:Windows 侧读数(腿①按既有惯例 POSIX 分支限定;腿②③跨平台恒跑)。— (flow: service-and-env) - **门禁归因修复(I4):`gateChain()` 下探失败被读成「链 0 环|点名却缺席 5 件」——红对了、话说错了**:病历(`scripts/arch-claims.mjs` · B234-1)——编排器形态下探失败(入口脚本**用变量拼接**调用各环,`scripts/<名>.mjs` 不是字面量)时取到零环,读数写成「链 0 环(源 package.json)|未点名 无|点名却缺席 scripts/dead-code-gate.mjs,…×5」,读者会去 **package.json / 点名面**找一个并不存在的问题,而真凶在**下探面**;「点名却缺席 5 件」只是零环的**算术后果**,不是一条发现。同类第二个形状:入口不在位时读数写成「链 1 环(源 **package.json**)|未点名 scripts/gate.mjs」——零环明明来自缺位,读数却把源指成 package.json。**修法(只改归因与措辞,判定语义一字未动)**:① `gateChain()` 增返 `diag` 归因态(`not-applicable` 无需下探 / `nested` 下探成功 / `zero-literal` 入口在但取不到环 / `missing` 入口不在位 / `unparsed` 命令面认不出入口),并抽出 `ringLiterals` / `stripCommentLines` 两个原语(等价替换,行为不变);② 新增 `chainZeroReason(diag)`:零环**不再**报点名面结论,改为「⚠ 编排器入口解析到 0 环:请检查 scripts/gate.mjs 是否以**字面量** `scripts/<名>.mjs` 调用各环」这类**指向入口脚本**的区分性消息(缺位态点名缺失入口;认不出态如实说「认不出」并提示变量拼接形态,**不猜一个入口路径**——猜出来的入口会把读者指到第二个错文件上);③ 下探失败时**零环也置 `source`/`nested`**,读数里的「源」不再写成 package.json;④ 部分缺席(有环、少几环)在 `点名却缺席` 后补一句**下探面提示**(该环字面量不在入口里 ⇒ 可能是被变量拼接调用,而非被删)。**fail-closed 不变**:`diveFailed || !actual.length` 仍返回 `no(...)` → 判红(不静默取空集)。**已实测(两列对照,同一夹具同一脚本语义)**:拿 **HEAD 版**与新版在**同一批夹具**上并跑,**退出码逐场景相同**——正常态 0/0、I4 夹具 1/1、删环负控 1/1、入口不在位 1/1、命令面认不出 1/1、链上多一环(未点名 rogue)1/1、旗标腿负控(rpc 丢 `--check`)1/1、直连形态 0/0;**基线列复现了病历原文**(「链 0 环…|点名却缺席 …×5」、「链 1 环(源 package.json)|未点名 scripts/gate.mjs」),新列两句均已消失且改指 `scripts/gate.mjs`。**新增回归锁 `test/gate-chain-attribution.test.cjs`(6 用例,已入 `npm test`)**:夹具在**整仓临时副本 + `git init`** 里造(不碰真实工作区,S-8 行尾判据需要 git 元数据),覆盖 ①正常态仍绿 ②I4 夹具归因指向入口而非 package.json ③旧读数形状**负控**(形状判据能抓住旧文案,否则本判据空转)④入口不在位 ⑤删环负控仍红 ⑥认不出入口不猜路径。**已实测**:`npm test` **263/263/0(17 文件,原 257 + 新增 6)**;`npm run gate` **5 环 0 FAIL · exit 0**(arch-claims/cards 两环 SKIP 为**本工作树无 `docs/internal/` 本地治理档**所致,改动前后同值,非本次引入);`arch-claims --json/--md/--check` 三模式均 exit 0。**未实测(明确边界)**:CI(ubuntu)上的新读数——本文件的夹具假定 `git` 可用(不可用则 6 条 `skip` 而非假绿)。— [PE-F01](flow: 装配与加载链) - **链级三态退出码:未判全(SKIP)与全过(PASS)不再同码(T3 / N3 · 用户裁定 A · 2026-09-27)**:`scripts/gate.mjs` 原为 `process.exitCode = results.some(r => r.status === "FAIL") ? 1 : 0` —— `verdict=NOT_FULLY_JUDGED` 与 `ALL_PASS` **同为 0**:文案已区分(`⏭ 未判全` vs `✓ 全过`),机器面唯一区分位是 `--json`,而全仓无消费者 ⇒ **CI 只看退出码时「有环没判」与「全判通过」完全同形**。现按用户裁定实现三态:**0 = 全过 / 1 = 有环 FAIL / 2 = 未判全**(失败优先;空环表与任何非 PASS 状态一律落 2,不落 0——分母为 0 不许判绿)。语义**唯一出处** = 新增 `scripts/lib/gate-exit.mjs`(纯函数 + 常量,写方/消费方/单测读同一份,不自带第二套判断);`--json` 的 summary 增 `unjudged / unjudgedSkip / baseline / accepted / unexpected / exit / exitText`(`exit` 与进程退出码同值)。**CI 侧按裁定显式接纳构造性缺位**(不采纳「缺位即红」——那会让 CI 恒红、读数照样不可用):`run: npm run gate -- --accept-unjudged=arch-claims,cards`,仅当「未判集合 ⊆ 基线」时链级 2 降 0 并打印接纳行(`⏭→✓ 链级未判全,但落在消费者声明的接纳基线内…`);**基线外任何新缺位仍是 2 ⇒ 本步红**。基线**只认环 id**(理由文本会随措辞漂移)、**不得写进 package.json 的 gate 脚本**(否则本地裸跑被静默降级——已由 GATEEXIT-08 锁死),补丁面在 CI 文件里。**已实测(四腿,真数据非夹具)**:① 全过腿 = 真 `git clone` + 真治理面(`docs/devref`/`docs/internal` + `PE_GOV_ROOT=/mnt/d/FF`)⇒ `PASS 5 · FAIL 0 · SKIP 0`、**exit 0**;② 未判全腿 = 本 worktree(= CI 形状,两环扫描面仓外)⇒ `PASS 3 · SKIP 2`、**exit 2**(改前该形状为 exit 0);③ FAIL 腿 = 故障注入 `GATE_RING_TIMEOUT_MS=1` ⇒ 五环全 FAIL、**exit 1**(显式给基线也不降级);④ 接纳腿 = `--accept-unjudged=arch-claims,cards` ⇒ **exit 0** 且 `accepted=true`;少给一环或给空基线 ⇒ **仍 exit 2** 并逐个报出 `unexpected`。新增 `test/gate-exit.test.cjs` **12 项**(码值锁 / verdictOf 真值表 / applyBaseline 真值表 / 参数解析 / 三次真跑端到端 / FAIL 注入 / 基线位置纪律 / CI 一致性 / 措辞纪律 / 穷举不变量 / 不可接纳面);**负控已证伪**:CI 未同步时 GATEEXIT-09 **红**(# fail 1),同步后全绿。**独立复核席(产出者≠复核者)实测出两条真缺陷,已修并各留一条可复跑负控**:**(D1) 去重契约零覆盖 + 返回形状漏键**——原穷举用的集合全无重复元素,`Array.from(new Set())` 无事可做 ⇒ 删掉实现里的去重仍全绿;修法=把"去重"从实现细节**升为契约形状**(报出的 `unjudged/baseline/blocking/unexpected` 四数组各自不得带重复 + 输入含重复元素时判定结果必须与去重后逐项相同),并加**返回形状锁**(穷举所有分支的键集合必须唯一——该锁随后抓出**接纳分支漏 `blocking` 键**这一真漏键,下游遍历该字段会崩)。**(D2) 静默死环可被基线吸收(假绿)**——`SKIP` 与"环根本没给读数"原先同码:把门环换成 0 字节 exit 0 的 no-op,加基线后链级仍报 **0**(实测旧行为),即"换空脚本"就能吃掉一条门禁;修法=引入第四态 **`UNKNOWN`**(零输出且 exit 0,**不等于**"扫描面缺位")并定义**不可接纳面 `blocking`**:非 PASS 且非 SKIP 的环**优先... (line truncated to 2000 chars) - **P4 漂移归因措辞订正:判据面未变时不再叙述成「已过期」(`scripts/arch-claims.mjs` + `CHANGELOG`;零对外行为变更)**:上一笔(同为本任务)把漂移判定三态化后,**同环境读数漂移**这一支的**措辞**与事实相反——实测(独立复跑):档内指纹 == 本形态实算指纹(`sha256:a664b8bd8ad77f94`)、表体逐行齐,归因却打印「判据面**已过期**」、收尾再补一句「已归因:判据面变了,不是换环境」。判据面一个字未改却被叙述成演化,人读会被引向错误的修复方向(b2/I3「措辞与事实相反」同族;与假绿同属「读数不可解释」,只是方向相反)。**改法**:抽出纯函数 `driftAttribution(drift, localFp, localN, snapEnv)`(**判据面「变没变」决定措辞,两分类不得互借**)——面变 ⇒「判据面**已过期**」+「判据面变了」;面未变、只剩同环境读数漂移 ⇒「判据面**未变**」+「不是判据演化」,收尾行改称「投影读数漂移」而非「投影已过期」;正文与收尾**共用同一份归因**(此前两处各写各的,正是说反的来源)。新增机检 **B234-7**(纯函数变异体 + **接线腿**:断言函数存在**且真被调到** ≥3 处,防「函数在但没人用」):面未变→措辞须为「未变」且不含「已过期」;面变(指纹变 / 表体缺行)→须为「已过期」且不含「未变」。**同批订正**:CHANGELOG 上一笔引用的指纹 `sha256:5da0f3f0ec5c29ef` **不可复现**(实测本形态 `sha256:6b449587ce725a5a`;该值是判据面的函数,断言增删即变)⇒ 改为**只留可复跑口径、不留冻结值**(冻结值会随下一次判据增删再次变成假事实)。**已实测**(八态矩阵,结论行取词):无档×2 → exit 0「文档投影未判」;同环境×2 → exit 0「结构判据一致」;换环境×2 → exit 0「读数快照陈旧」;**同环境读数漂移 → exit 1 归因「未变」**;**判据面演化 → exit 1 归因「已过期」**(不经管道取码,排除了 `$?` 被前段管道掩盖的假读数)。真实本机态(完整 `docs/internal` + `--write`)两形态 gate:**gov-reachable `GATE_EXIT=0`(arch-claims=PASS)/ gov-unreachable `GATE_EXIT=0`(arch-claims=SKIP)**,读数均可解释。`npm test` **257/257/0**;`build-host --check`(3.4.0)/ `build-client --check` / `sync-prompts --check` / `rpc-manifest --check` / `dead-code-gate` **五处零漂移**。**边界(未动)**:「方案② P4 档进版本控制」涉用户级文档发布边界,**未拍板不施工**——`docs/internal/` 仍 gitignore(`git ls-files docs/internal` = 0),本次改动**不触碰** `.gitignore`、`docs/` 与任何源码/产物。— [PE-F01](flow: 装配与加载链) - **P4 投影漂移判定三态化:修「换环境即假红」(`scripts/arch-claims.mjs`;零对外行为变更)**:`arch-claims --check` 原按「治理档标记区与本次实算**逐字相等**」判漂移,而投影里「期望/实测/判定」列**是环境的函数**——实测同一脚本同一棵树下 `--json` 逐列比对:`id/kind/detail/claim/tier/target` **两形态全同**,只有 `actual`×2(A194-4 · S-2)、`pass/skip`×2 因「治理日志(`PE_GOV_ROOT`/`PE_NAV_EVENTS`)可不可读」而不同。⇒ 投影必然只对**生成它的那个环境**成立,另一形态必报「文档漂移」:实测 2026-09-27 同一份治理根态投影,`PE_GOV_ROOT=/mnt/d/FF` 跑 **exit 0**、不带则 **exit 1**,gate 整链跟着 FAIL。**这是假红**(判据表一个字未变),与假绿同属「读数不可解释」。**改法(三态)**:判据面与环境派生列**分开判**——① 判据面**指纹**(新,`sha256:` 取 id/kind/detail/claim/tier/target + `RUN.length`/`DEFECTS.length`;**只取跨环境同值的量**,实测两形态同值;⚠ 该值是判据面的函数(断言增删即变),**故只留可复跑口径、不留冻结值**——本形态当前值用 `node scripts/arch-claims.mjs --md | grep -o 'sha256:[0-9a-f]*'` 现场取)与**表体逐行绑定**都一致、且档内读数取自**另一种**环境 ⇒ `stale-readings`:提示刷新、**不判漂移**;② 判据面变了(加/删/改断言、表体缺行、指纹被替换)⇒ `drift`,**exit 1**;③ 判据面一致但**同一种**环境读数却变了 ⇒ 仍 `drift` 判红(不放宽,否则把「读数漂移」整类静默掉)。归因**点名具体失效腿**(指纹/条数/表体缺行/读数漂移),不再笼统写「指纹 A vs B」。**同批收口**:`B234-3` 的缺档守卫改为**语义判定**(用分类器实跑缺档/缺标记两个变异体,不再锁 `return null` 字面量——锁字面量会把本次归因改造误判成「守卫被删」);新增 **`B234-5`**(判据面不含环境派生列 + 计数跨环境同值 + 自底向上重算 + 归因两分 + 指纹行参与判定)与 **`B234-6`**(**判别力自证**:9 个内存变异体——换环境→`stale-readings`、同环境读数漂移/指纹变/条数过期/表体缺行→`drift`、指纹被替换→`fpConsistent=false`、缺档/缺标记各 1);投影新增一行 `> fingerprint(…|读数快照 env=|…)`,使**投影自述其读数取自哪种环境**;`--check` 收尾行在有 SKIP 时显式声明「未判全…不记 PASS」(措辞诚实腿)。**修复过程自身踩到并记录的三处**:① 指纹计数里曾放 `passed/skips`(随环境抖)⇒ 归因会说反,已剔除并写入 `B234-5` 的机检腿;② `--write` 写**含**指纹行的投影而比对侧**剥**指纹行 ⇒ 两侧不对称、两种环境都红,改为**两侧同一 `stripFp`**;③ 加指纹行后条数定式失配(`读数快照 env=…` 插在条数与判据面之间)⇒ 判据面未过期也被判过期。**已实测**:九场景逐项(`--check`,含 gate)——治理根态投影读于**两形态**均 **exit 0**、工件形态投影读于**两形态**均 **exit 0**;判据面演化(改 `S-8` detail 一字)**两形态均 exit 1** 且归因为「指纹不一致」;同环境读数漂移 **exit 1** 归因「读数漂移(同为 env=…)」;表体删一行 **exit 1** 归因「表体缺行 1/39(如 A146-1)」;档内指纹篡改 **exit 1**;无治理档(CI/干净 clone 形态)**exit 0** 且打印「不记 PASS」;带**完整** `docs/internal`(10 档)复跑:治理根态 **exit 0**、gate **exit 0**(`环状态:dead-code=PASS rpc=PASS prompts=PASS arch-claims=PASS cards=SKIP`);`npm test` **257/257/0(1... (line truncated to 2000 chars) - **门禁环统一结构化状态出口(P5·2026-09-26;终结 `gate.mjs` 文本启发式)**:根因——`gate.mjs` 的**环级状态判定**一直是文本启发式(扫环输出、用正则猜 PASS/FAIL/SKIP),红队修复波里**连改三次**仍未治好:I2(对整段做宽松子串匹配 → 各环输出天然含自己的判据名与期望行,误判)→ N2(只在末 4 行窗口找 → 结论行后多打几行日志就滑出窗口,**SKIP 被读成 PASS**)→ N4(措辞自含反义子串,`grep 全过` 把否定句读成通过)。病根不在正则写得对不对,而在**环没有机读状态出口**——判据方只能从给人看的措辞里反推机器状态,环一换措辞就可能把「没判上」记成「判过了」。**改法**:① 新增 `scripts/lib/ring-state.mjs`,把契约与解析器收敛为**产出侧/消费侧共用的唯一事实源**——五环统一输出 `RING-STATE `(**恰一行,走 stderr**,stdout 留给 `--json` 机读数据面);② 五环(`dead-code-gate`/`rpc-manifest`/`sync-prompts`/`arch-claims`/`card-arch-consistency`)在各自出口**与退出码同源**地打契约行,并装 `uncaughtException` 安全网(崩溃也留状态行,使「崩溃」与「判红」可区分——二者过去在 RING 行同形);③ `gate.mjs` **删除全部文本匹配**(`resolveStatus`/`locateConclusion`/`scanLines`/`SELF_REPORTS`/`GENERIC_CONCLUSIONS` 五段共 75 行全部移除——启发式块从基线 L63 到 L137 整段删除,剥注释后零正则),只调 `deriveState()` 读结构;④ 退化位**穷举且 fail-closed**:D0 多条契约行 / D1 id 不符 / D2 状态与退出码矛盾 / D3 有输出但无契约行 → **FAIL**(未接线不猜、不默认 PASS),D4 零输出+exit 0 → SKIP「无读数」,D5 零输出+非零 → FAIL;⑤ `installStdoutHygiene()` 守卫 rpc/cards 的 `--json` stdout 纯净性(契约行落到数据面即被抹除,数据面仍可 parse)。**「改前误判 / 改后正确」对照读数(同一措辞、只换裁决器)**:用 `git show 4361c34:scripts/gate.mjs`(契约改造前的基线)取出旧裁决函数 + 在**写入层**改写各环结论措辞(不改任何环文件)后对拍——单变量实测:`arch-claims` 真值 SKIP,**旧 gate 读成 PASS / 新 gate 读成 SKIP**(同一措辞、只换裁决器);把措辞改写的环改成「**连契约行一并抹掉**」后,旧 gate 读 `arch-claims=PASS · cards=PASS`(两条真值都是 SKIP,**双误判**),新 gate 读 `FAIL · FAIL`(fail-closed)。对拍有效性坐实:基线 gate 全文 `RING-STATE` 出现 **0 次**(它根本不认契约行,故其判定与契约行在场与否无关,只随措辞漂移)。单元级对拍:**旧裁决错 2/7、新裁决错 0/7**(口径以可复跑工具 `scripts/ring-state-compare.mjs` 的实跑为准;更早记录里的「3/10」是夹具重构前的旧场景集,已作废);另有**契约行缺失 5 例**:新语义一律正确拒绝判过(fail-closed),其中 4 例旧裁决读成 **PASS** = 逐条误判证据。**既有负控复验**:`GATE_RING_TIMEOUT_MS=1 npm run gate` → 5 条 RING 全出、`PASS 0 · FAIL 5`、exit 1(**不短路**);默认链 5 条 RING 行齐全;契约行缺失负控 → `FAIL`(fail-closed)。**新增回归测试** `test/ring-state.test.cjs`(12 用例,锁定四条验收:五环契约行齐备且 stdout 纯净 / gate.mjs 零文本匹配(正则腿 + 非正则腿)/ 换措辞不误判含 N2·N4 形态 / 行首锚定是真锚(缩进仿冒行必拒)/ 恒红不短路与五环可见),已入 `npm test`。**未改**:任何环的判定语义、退出码契约(SKIP 环仍 exit 0)、门禁... (line truncated to 2000 chars) - **文档真值对齐(治理档/架构档 ⟷ v3.4.0 现码;零代码改动)**:项目梳理实测发现治理层落后于代码——① **架构档三份全部停在 2026-09-12/13**(早于 P1a 载体单一化、P1b 死层清零、P4 门禁机检化与 v3.4.0),其中 L1 总览与 L2 装配链**仍把已退役的孤儿产物 `plugin-client.js` 画成「仍进发布包」**、并称 `src/host` 有「死层 18 件」(实为 **7 件 / 33 行**),版本仍写 3.3.3(实为 **3.4.0**)——它们按设计是「agent 开工前必读的开发主地图」,属**会误导后续开发的假事实**;② **`AGENTS.md` release-notes 计数**写「共 5 个」而实测 **6 个**(3.4.0 未计入);③ **`docs/compatibility-matrix.md` 的版本矩阵**把 `3.3.x` 标为「当前」,而该行描述的行为(插件内重启退役、`update/install`)正是 **3.4.0** 引入的——已订正为「3.3.x – 3.4.0(当前)」;④ **`AGENTS.md` §6.1 的 `node`/`npm` 命令在本机 agent shell 下不可执行**——实测 `where.exe node` 无输出、User/Machine PATH 均无 `C:\Users\lk\.dsh-win\prefix`,且失败呈**静默形态**(`npm run gate` 只留下空 `GATE_EXIT=`,看似跑过实为一条未跑),已在 §6.1 固化为「唯一可复跑形态」(绝对路径 `$NODE`/`$NPM`)并同步 `docs/devref/README.md` 踩坑节。**已实测**:全量 `npm test` **257/257/0(16 文件)**;四门禁 `汇总:通过 33 · 冲突 0 · SKIP 0 · 在册未达 3 · 在册缺陷 0`;`build-host --check`(版本 3.4.0)/ `build-client --check` / `sync-prompts --check` **三处零漂移**;`npm pack --dry-run --json` = **entryCount 19 / unpacked 955,498 B / packed 287,908 B**(不含 `plugin-client.js`);`gh run list` **近 6 次 CI 全 success**;`gh issue list --state all` **10/10 CLOSED**;治理侧 STALE **0**。**架构档重提炼实测(三份全部过机检门)**:L1 总览 33,617→**61,491 B**、PE-F01 装配链 13,780→**24,881 B**、PE-F04 语音链 19,226→**33,927 B**;机检门 `arch-check.mjs` 读数 **mermaid ×3 全绿(⛔=0)**、**anchors 100%(PE-F01 99/99 · PE-F04 87/87)**,新增锚点清单 `_anchors-PE-F01.json` / `_anchors-PE-F04.json`(按 VT-F01 先例留存,可复跑)。逐条语义等价交叉验证:PE-F04 的 C/D 型 25 条重锚「旧文件该行 == 新文件目标行」**25/25 相等**,E 型 2 条经复核为「旧档写的是 `ddc6360^` 号」的口径差(实测 EQUAL,非漂移)。**同批修复共享工具缺陷(`arch-check.mjs`,属治理根 + 技能目录,不进本仓发布物)**:① **退出码契约失效**——头注(:14)声明「0 全通过 / 1 有门未过」,但**全文无 `process.exit`**(`grep` 0 命中),`fail()` 只自增并打印 ⛔,于是**打印 `⛔ 边表条数(0) ≠ 实扫条数(39)` 时仍 exit 0**;按退出码判绿会把失败读成通过(本仓「假绿」家族新成员:**门报失败、进程报成功**)。修复=在文件末尾补 `if (FAILED) process.exit(1)`(**让实现回归它已声明的契约,非改契约**)。② **空扫假绿**——`scan` 的 `--ext` 与目录扩展名不匹配、或 `l1/edges` 的 `--src=` 指向无 import 文件时,会「0 文件 / 0 边」却 **0 对 0 判 ✓ 且 exit 0**(实测 `scan lib` 不带 `--ext=cjs`);修复=两处各... (line truncated to 2000 chars) - **架构档自我证伪订正(L1 总览,含一条我自己引入的假断言)**:本轮重提炼产出的 L1 档初稿写有「`PE-F02` 的索引锚点 `src/host/config-service.js` 与 `PE-F06` 的 `update-service.js` 已失效、应改锚」+「治理模型里若仍留有 18 条死层落点属**假 STALE 源**、应 `nav_node retire`」——**两条均为未核实的推测,经二次复核推翻**。实测证据:① `nav_graph mode="task" target=PE-F02/PE-F06` 显示两者现行落点**全部是磁盘真实文件**(PE-F02 = `lib/index.cjs`+`params-tab.js`+`app.js`;PE-F06 = 10 件);② `config-service.js`/`update-service.js` 只出现在**已退役节点 `feature:pe-m2`(`status: retired`)**的 files 列表里,**不是** PE-F02/F06 的锚点;③ 决定性互斥判据——`nav_graph mode="gaps"` 实测 **STALE 落点 = 0**(若它们真是登记落点必然 STALE);④ 该退役节点实测列 **17 件**(非初稿所述 18),且已正确带 `retired` 状态 ⇒ **索引侧无需任何改动**。已在 L1 档 4 处(L189/L198/L278/L280 + B-6 条目)就地订正并写明推翻依据。**已实测**:`arch-check.mjs mermaid` exit 0、PE-F01/PE-F04 anchors 均 exit 0、仓库四门禁 33 通过·冲突 0、`npm test` 82/82。**教训(同类第三例)**:重提炼中「顺手报出的治理侧问题」若不逐条实测,就会把**推测当事实**写进「开发主地图」——本次靠 `nav_graph` 实测 + 「STALE=0 与失效锚点互斥」这一**机检可判**的交叉约束才抓出来。 - **同批扩查:L2 PE-F04 档同类误判一并作废(4 处)**:按上条教训对同批产出做「同类错误猎捕」,发现 PE-F04 语音链档存在**同一方法学错误**——初稿以**裸单文件路径**查 `nav_graph mode="task"`(该姿势对不带项目前缀的路径一律返回 `No mapping`),据此误判「`state.js`/`voice-insert.js`/`lib/asr.cjs` 等在索引外、应补登记」「`pure.js` 被列为 PE-F04 锚定、应移出」。**实测全部推翻**:① `nav_graph mode="task" target=PE-F04` 现行落点 **9 件全部在册**(`lib/{asr,asr-worker,asr-models,asr-deploy}.cjs` + `src/client/voice/{mic-button,recorder,voice-section,state,voice-insert}.js`);② 精确解析 `feature:pe-f04` 的 files 数组 = **9 项且不含 `pure.js`**;③ 全路径实测 `pure.js` 归属 **PE-F03(提示词增强)**。⇒ **索引侧无需任何改动**(初稿的「索引待修正」结论整体作废)。已就地在页眉锚定自检、L132、L134/136、条目 ⑦、边界声明共 **5 处**订正并写明推翻依据。**已实测**:PE-F04 `anchors` **87/87(100%)**、`mermaid` **14 节点/14 边 0 项失败**;仓库门禁与单测不受影响。**索引侧动作**:本轮全程**只读**,未执行任何 `nav_node`(三条「待修正」经实测均不存在,无动作可做)。 - **文档真值一致性三册落地(新增 gate 第 5 环;方案见 `docs/internal/优化方案-2026-09-25-文档真值一致性三册.md`)**:前序 7 处假断言中**有 4 处指向同一根因——「知识卡与架构档互不引用」**(`docs/devref/shoucang/…索引锚点待修正.md` 早在 2026-09-13 已标 `closed` 并写明「已全部补登、`pure.js` 已摘除」,但三份架构档仍写「索引待修正」,三个子代理又都没读那张卡而重蹈其 2026-09-10 的误判)。人工逐条对账已发三次,故**把一致性变成机检**(与 ADR-234 同源)。**册 A**:新增 `scripts/card-arch-consistency.mjs`(判据 A-1 卡表⟷卡正文 / A-2 档⟷卡须邻近实测依据 / A-3 空扫护栏 + 三条判别力自证),纳入 `npm run gate` **第 5 环**;**卡的分母 = `INDEX.md` 卡表数据行数(4),非文件数(7/5/3 均为错值)**;扫描面**限定本项目档**(避免对 project-nav/shoucang 等其他项目出误伤)。**册 B**:治理根 `_*` 实为 **22 件**(初稿误记 16——**清单列 17 个文件名而标题写 16,漏计 `_finalize1.cjs`**,已订正方案档),17 件 2026-09-13 批次一次性探针**全部 `Move-Item` 归档**至 `.internal/archive/pe-20260913/_probes/`(**原名保留、17/17 sha256 全等、删除数 = 0**,台账 `_MOVED.json`),保留 `_anchors-*.json`×3 与 `_PLAN-*.md`×2 不动。**册 C**:`docs/devref` 4 张卡标题规范化(`[closed]`/`[superseded]`/`[accepted]`)、补卡→档反向指针 9 处、并实测出「**`cards/*.md` 是册壳非卡**」(`reference.md` 载 2 张,`how-to.md`/`decision.md` 各 0)——这条口径已转交册 A 修正其分母。**已实测**:`npm test` **257/257/0(16 文件)**;`npm run gate` **原记录「五环全过(33 + 6 断言 · 冲突 0 · SKIP 0)」已作废**——2026-09-26 红队审查复算:当时实测 **exit 1**(第 4 环 `arch-claims --check` 报「文档漂移」),且 gate 链为 `&&` 串行 ⇒ **第 5 环从未被执行**(日志里连一行都没有)。**跨条目互斥**(不在本条目内,是相邻另一条 bullet):本文件 **L20** 另写「四门禁 **33 通过·冲突 0**、`npm test` 82/82」(L18 亦写「四门禁仍 **33 通过·冲突 0**」)——「五环全过」与「四门禁 33 通过」两项「已实测」不能同真;以可复算者为准(原记录保留在此以存可追溯性,不作删除);三产物 sha256 与并发前基线**逐位相同**(`plugin-host.js d69eb788…`/`lib/client.cjs da090ff4…`/`lib/index.cjs 9e98881d…`)⇒ **高并发全程未污染产物**;`git diff package.json` **恰 1 行**(仅追加 gate 第 5 环)。**边界(本批明确不动)**:不改任何源码/脚本/测试/生成物;**未改本机 User PATH**(属用户全局共享件,超出本仓范围,仅记录事实与绕行办法);`.roundtable/`(会议工作目录)与 `.internal/`(5 件手工 E2E 探针,2026-08-20/24,与 `test/a6-drill.cjs` 同类先例、零引用但在用工具)**均按原样保留**——前者按既有约定不入仓,后者是验证工具而非垃圾。— [PE-F01](flow: 装配与加载链) ## [3.4.0] - 2026-09-19 > ⚠️ **BREAKING(对外契约变更,单列声明)**:本版**移除插件内重启能力**——RPC `update/portRestart`(端口/服务重启)与 `update/makeShortcut`(桌面快捷方式)连同其客户端重启 UI 一并下线,新增 `update/install`(安装已下载到 staging 的新版本并返回 `restartNeeded:true`)。核对依据:这两个 RPC 在 `v3.3.3` 的 tag 内**确实在册**(`git show v3.3.3:lib/index.cjs` 命中 2 条 `harness.handle`),故属本版引入的破坏性变更,非历史叙述。**升级影响**:一键更新后不再能由插件自己重启 DSH,改为提示**手动重启**;用过「重启按钮 / 桌面快捷方式」的用户会看到入口消失。决策依据 ADR-201。