# AGENTS.md

> 面向开发人员与 AI Agent 的 `dsh-desktop`（DeepSeek Harness 桌面壳）协作指南。

> ⚠️ **能力口径**：每个特性必须区分五种状态——**已接线 / 未接线 / 未实现 / 计划中 / 已归档**（词表见 §7.3）；不在「已接线」之列的**不得对外呈现为可用能力**，判据见 §7。

> 📖 **本文件是「规则手册 + 索引」**：历史复盘与操作细节已外移。
> - 命令速查（含全部 npm script 名）→ `docs/commands.md`
> - 发布步骤 / 版本 / CI / 双上游通道 → `docs/release-runbook.md`
> - 「已修复，勿回归」事故档案 → `docs/incidents/`，逐条见 §4

---

## 1. 项目概述与仓库结构

`dsh-desktop` 是基于 **Tauri 2.0** 与 **Rust** 的 DeepSeek Harness 跨平台桌面外壳，负责内置 Node.js + Harness 运行时的生命周期管理，并提供 GUI 承载与崩溃自愈。

- **`crates/dsh-contracts`**：无 GUI 的通用契约库（常量、错误码、IPC 封套、JSON-RPC 2.0 唯一契约源）。
- **`crates/dsh-host`**：无 GUI 纯 Rust 核心宿主（子进程派生、孤儿防护、就绪探测、Supervisor、崩溃诊断、脱敏诊断包、日志读取、Safe Mode、Profile/Session）。**INV-6：严格 headless。**
- **`crates/dsh-host-cli`**：命令行前端（`start|status|stop|tail|probe|doctor`），**INV-6 的兑现载体**；仓库内使用，**不作发布产物**。
- **`src-tauri`**：Tauri 桌面应用层（窗口/生命周期/IPC/自动更新/安全模式/LAN 手机桥/日志查看器/托盘/反馈）。
- **`build/`**：运行时启动脚本与安全防护注入。
- **`scripts/`**、**`patches/<target>/`**、**`packages/<target>/`**：组装/门禁/打包工具链，与按通道分目录的补丁、vendored 包。
- **`docs/`**：架构设计、契约定义、不变量与技术规范。

> 📖 **完整目录划分见 `docs/incidents/repo-structure.md`。**

---

## 2. 常用命令速查

> 📖 **完整命令速查表见 `docs/commands.md`**——原 §2 内容原样迁入。
>
> ⚠️ **2026-10-03 起命令面收敛（治理 G1/G2，见 §7.3 与 `docs/dev-plan-defect-remediation.md` §11 的 S4-5）**：
> 门禁清单的**唯一产地是 `scripts/gate-manifest.mjs`**（含分档与逐条理由），package.json 只留
> 22 条**人用入口**（此前 62 条里 41 条是门禁各自的入口）。跑门禁用 `npm run gate -- <name>`，
> **旧名 `npm run verify:<name>` 经 `npm run gate -- verify:<name>` 仍然可用**（兼容层；历史文书不改）。
> 条目数与各档步数**刻意不在此写死**——它们随增删漂，而这一格没有任何守卫（同口径见
> `docs/commands.md` 头部）。实时值：`npm run gate -- --list`；`npm run gate -- gates` 会打印汇总行。
> （历史：2026-10-03 落地时 33 条，2026-10-09 已增至 42 条——原句写死的「33 条」就是这么漂掉的。）
>
> 高频：`npm run dev`、`npm run build`、`cargo test -p dsh-contracts -p dsh-host -p dsh-host-cli`（无头门禁，INV-6）、`npm run gate -- claims` / `npm run gate -- plan-facts`、`npm run verify:fast`。

---

## 3. 架构边界与核心不变量

1. **无头核心库隔离（`dsh-contracts`、`dsh-host`）**：严禁依赖 Tauri、UI 框架或窗口系统；核心库测试必须能在无显示器、无预组装资源包的 CI 下独立通过。判据：**它能不能在没有窗口系统的机器上被测试？** 能就放无头 crate，不能才放 `src-tauri`。
2. **契约与常量集中管理（`dsh-contracts`）**：所有硬编码字符串、超时、退避、缓冲区、正则、探测常量**必须**统一定义在 `constants.rs` 并带 `CX-` 编号；**严禁**在业务逻辑里硬编码超时、路径或 URL。JSON-RPC 2.0 消息模型的**唯一定义点**是 `rpc.rs`（`transport.rs` 仅 re-export）；**严禁在任何 crate 内重复定义协议类型**（该冲突已于 2026-09 修复）。⚠️ 该协议**当前无运行时消费者**。
3. **孤儿进程防护与进程管理（INV-3）**：Windows 用 Win32 `JobObject`（`KILL_ON_JOB_CLOSE`）；Linux 用 `PR_SET_PDEATHSIG` + 进程组；macOS 用进程组 + **Node 侧父死看门狗**（`build/parent-death-watchdog.mjs`，**入口与 `mock-harness.mjs` 共同引用**）+ 启动时退出扫描清理。**主程序异常崩溃或退出时子进程不得残留。**
4. **生命周期监督与自愈（Supervisor）**：状态机 `Stopped -> Starting -> Healthy -> Degraded -> Crashed`；心跳探活、自愈重启、重启退避与断路器，防止无限崩溃循环。
5. **插件分级进程隔离与熔断自愈（Tier 0/1/2）**：不可信插件运行于沙箱 Worker / 子进程（JSON-RPC 2.0），连续故障计数与熔断看门狗使插件级崩溃自动隔离并降级进 Safe Mode。**（该实现已归档；当前插件防护只有进程内 `formatFaultDetails`，见 §7.2。）**
6. **日志环形缓冲与编码容错**：stdout/stderr 经 `LogRing` 缓冲并落盘（`harness.log`、`app.log`）；优先 UTF-8、Windows 回退 GBK，并过滤 ANSI 逃逸序列（`sanitize_line`）。

---

## 4. 平台兼容性与避坑指南

- **Rust 工具链**：**唯一产地是 `Cargo.toml` 的 `rust-version`**（当前 `1.90`；Tauri 2.12 起上调，旧值 1.85 源于 `idna_adapter` 的 edition 2024）。README 的「前置条件」与此同步。
- **Tauri 资源校验**：全新 checkout 在 `cargo check`/`cargo test` 前先跑 `node scripts/stub-tauri-resources.mjs`。
- **Node 执行路径**：产物运行内置 `src-tauri/resources/node/` 下的 Node（Windows 带 `.exe`）。
- **页面跳转**：Webview 先加载 `frontend/index.html` 启动屏并监听 `harness://status`；Token 就绪后由 Rust 导航到本地 Harness 页。

> 🗂️ **「已修复，勿回归」事故档案已按主题外移到 `docs/incidents/`**（内容原样保留；新家含完整根因、修法与守卫）：
> - Windows 打包与启动 → `win32-packaging.md`
> - 契约形状与 IPC → `contract-shape.md`
> - 补丁与上游 → `patch-and-upstream.md`
> - 发布与 CI（静默缺陷 / 清理否决结论 / 镜像清单 / 变更基线）→ `release-and-ci.md`
> - 体积与平台变体（optionalDependencies 翻倍 / linuxdeploy 复发）→ `packaging-size-and-platforms.md`
> - 进程与孤儿防护（macOS 父死看门狗）→ `process-and-orphan.md`
> - 桌面壳与托盘（`menu.rs` 两条前提被证伪）→ `desktop-shell-and-tray.md`
> - 本地工具链限制（Clippy ICE / GNU 下测试二进制加载失败）→ `local-toolchain-limits.md`
> - 仓库结构与路线图 → `repo-structure.md`、`roadmap.md`
> - 本文件被精简章节的原文快照（§3 / §4 前言 / §6）→ `agents-md-original-sections.md`

---

## 5. 架构演进与路线图 (P0~P4)

> 📖 **P0~P4 各阶段与归档裁定见 `docs/incidents/roadmap.md`。**（P2/P3 🗄️ 已归档删除。）

---

## 6. 修改敏感模块前必读文档

- `docs/roadmap.md`：**顶层路线图**（定位与边界原则、阶段 H0~H3）；定位与裁决冲突以它为权威。
- `docs/dev-plan-hardening-and-differentiation.md`：近端施工计划（H0）——风险 R1~R9 与批次 H~N。
- `docs/dev-plan-0.2-hardening.md`：产品/分发侧增补（0.2-A~D）；与 H0 的冲突裁决归用户。
- `docs/dev-plan-disconnected-points.md`：上一阶段主计划（批次 A~G 已闭环）——断线点 D1~D11 与裁决记录；「插件禁用语义」证据链在此。
- `docs/dev-plan-cli-distribution.md`：CLI / runtime 可引用产物分期；**动 CLI 发布形态前先读它**（§5 退役评估）。
- `docs/dev-plan-defect-remediation.md`：**缺陷治理专项**（批次 S0~S7，2026-09-30 起）——D1~D12 缺陷清单、可证伪判据、C1~C8 裁决与执行台账；动 S 批次任何条目前先读它。
- `docs/dev-plan-supervisor-shell-wiring.md`：**Supervisor 接入桌面壳（L1→L2 接线）的设计与阻塞项**——集成点、`SupervisorState`↔`HarnessPhase` 映射表、启动/停止/安全模式生命周期、异常与边界清单（E1~E11），以及**接线前必须先在 `dsh-host` 补齐的三个 API 缺口**（`HarnessEnv` 传递 / 安全模式 profile / `extra_envs` 死字段）。**动 `HarnessSupervisor` 或 `Supervisor` 前先读它。**
- `docs/recover-demo.md`：`recover` 子命令的一页式无 GUI 复现步骤（启动 → 注入崩溃 → 退避递增 → 断路器开路）——**改 Supervisor / 退避 / 断路器后必跑**，它同时是 §7.2「Supervisor 自愈与退避」的证据链之一。
- `docs/version-policy.md`：**版本号策略唯一口径**——命名规则、按变更类型的递增条件、**递增权限**（谁有权决定版本号）、发布流程与守卫；**动版本模型或 `release.yml` 前先读它**。
- `docs/adr/`：架构决策记录库（50 篇，编号有空洞属正常——被否决的编号不复用）；新能力先写代码、再按 `docs/adr/README.md` 登记。
- `docs/dsh-desktop-redesign-architecture-and-plan.md` 与 `docs/system_design.md`：系统重构设计与架构 / 缺陷 / 契约细则。
- `docs/archive/model_gateway_design.md`、`docs/archive/plugin_isolation_architecture.md`：**已归档**，仅在追溯设计意图或评估恢复时读。
- `crates/dsh-contracts/src/constants.rs`（契约常量）、`errors.rs`（错误码 + `AppError`）、`ipc.rs`（`IpcEnvelope<T>` + 形状测试）、`rpc.rs`（JSON-RPC 唯一契约源，⚠️ 无运行时消费者）。
- `crates/dsh-host/src/diagnostics_export.rs`（脱敏诊断包；新规则必须补正反用例）、`transport.rs`（IPC 抽象，RPC 类型 re-export）。
- `src-tauri/src/commands.rs`：**IPC 命令面模块文档**——准入纪律、`CommandResult` 两层结构、外层 `Result` 恒为 `Ok` 的理由。动命令前先读。
- `scripts/verify-shell-pages.mjs`：改 `src-tauri/frontend/` 任何页面后必跑；新增检查项按文件头格式登记。

---

## 7. 宣称纪律（Claim Discipline）

**背景**：外部评审（2026-09）指出 README / 文档宣称的能力与实际代码存在落差。逐项核对后有 4 项宣称在代码中**没有运行时路径**：插件分级隔离、多模型工具网关、一键诊断包、LAN 手机桥（批次 A~G 已全部处置）。这不是「文档写早了」的程度问题——`plugin_worker.rs::call_tool` 当时会**返回伪造的成功结果**（`success: true`），即上层无法通过任何观测手段发现插件工具其实根本没被执行。

> 📌 **该文件已删除**（2026-09-10 批次 F 冻结并归档）。上面这段保留为**历史记录**：它记录的是这套纪律为什么存在，不是当前代码状态。查当前状态请看 §7.2。

### 7.1 三条硬规则

1. **未接线的能力必须显式标注**。任何「已实现但无运行时调用方」的模块，必须在其模块文档头部加 `⚠️ 状态：未接线` 横幅，并在本表登记。禁止用「已实现」「已支持」等词描述未接线能力。
2. **禁止伪造成功**。桩实现若被调用，必须返回**可辨识的错误**，不得返回 `Ok` / `success: true` / 空数组等合理默认值。判据：调用方能否从返回值区分「成功」与「未接线」。**2026-09-10 起这条同时约束 IPC 面**：失败必须是封套里的 `success: false` + 稳定 `error.code`，而不是裸 `false`——恢复页四个按钮「点了没反应」的根因就是静默的 `false`（改为 `E7002` 后页面才能如实报错）。
3. **禁止无声降级**。允许降级（如补丁分级失败策略），但必须把降级事实写进产物：`MANIFEST.json` 的 `patches[]` 逐条记录 `applied / skipped / failed`，缺记录即视为未应用。**同一规则适用于诊断包与日志查看器**：脱敏命中次数写进包内 `README.txt`；日志被截断时 `truncated` 必须如实上报。

### 7.2 宣称能力 ↔ 代码证据对照表

| 对外宣称 | 状态 | 代码证据（唯一产地） | 运行时调用方 |
|---------|------|-------------------|------------|
| 内置 Node + Harness 生命周期管理 | ✅ 已接线 | `crates/dsh-host/src/launch.rs` | `src-tauri/src/lib.rs` setup |
| 孤儿进程防护（INV-3） | ✅ 已接线 | `crates/dsh-host/src/process.rs`（Win32 JobObject / POSIX） | `launch.rs` 派生路径 |
| URL / Token 捕获与就绪探测 | ✅ 已接线 | `readiness.rs`、`token.rs` | `state.rs::on_ready` |
| Supervisor 自愈与退避 | ✅ 已接线（**CLI + 桌面壳两条路径都已接入**；2026-10-11 同日先订正为「未接线」、再补齐消费者） | `crates/dsh-host/src/supervisor.rs`（退避/断路器 + 纯函数 `next_backoff` / `decide_restart` / `is_stable_uptime` / `resolve_launch_env`） | **两条**：① `dsh-host-cli recover`（第一个消费者，`commands/recover.rs`）；② **桌面壳 `src-tauri/src/state.rs` 的 `HarnessSupervisor`**——2026-10-11 改为 L2 适配器（[ADR-064](docs/adr/064-shell-adopts-l2-supervisor.md)），公开方法面零改动、`commands.rs`/`menu.rs`/`quit_guard.rs` 不受影响。**证据链**：单元不变式 11 项（`supervisor::tests`）+ 性质测试 9 项（`tests/supervisor_properties.rs`）+ 集成面（`tests/supervisor_backoff.rs` 退避递增与进 `Failed`、`tests/supervisor_stop_orphans.rs` 启动中 stop 无孤儿、`tests/supervisor_env_profile.rs` env/profile 真达子进程）+ 黑盒（`crates/dsh-host-cli/tests/recover_cmd.rs`）+ 一页式复现步骤 `docs/recover-demo.md` + 壳层映射单测（`classify_state`）。⚠️ **接线带来的产品行为变化**：崩溃改为退避重启至多 3 次（`SUPERVISOR_MAX_RESTARTS`），最坏 **3.5 s** 后才进错误页；⚠️ **GUI 路径的本机自动化覆盖仍缺**（GNU 工具链下 `dsh-desktop` 测试二进制无法加载，见 `docs/incidents/local-toolchain-limits.md`），壳层断言由 CI（MSVC）执行 |
| Safe Mode 隔离 Profile | ✅ 已接线（2026-09-10 补完启动链路） | `crates/dsh-host/src/safe_mode.rs`（profile 落盘）+ `args.rs::profile_patch` / `launch.rs::with_safe_mode`（**启动时选中**：`--profile desktop-safe-mode` + `build/dsh-desktop-safe.patch.yml`） | 错误页 `/safe_mode_action` → `commands.rs`、菜单 `harness-safe-mode` → `menu.rs` |
| ↳ Safe Mode 的界面反馈（横幅） | 🕓 **计划中**（刻意后置） | 上游靠 preload 往 Harness 页注入安全模式横幅（`mountSafeModeBanner`，含「卸载插件 / 退出安全模式」两个按钮）；本仓界面**看不出**当前处于安全模式 | 无（**待软件功能稳定后再开发**，非阻塞项） |
| 崩溃归因诊断（**结论**，非压缩包） | ✅ 已接线 | `crates/dsh-host/src/diagnostics.rs` | `dsh-host-cli doctor`、错误页 |
| **更新失败分类（稳定错误码）** | ✅ **已接线（2026-10-03）** | `src-tauri/src/update.rs::UpdateFailure`（`check/download/install × network` + 签名拒绝 + Other 五类；`E1005~E1008` 见 `dsh-contracts/src/errors.rs`）+ `UpdateStatus.code` 字段 + `updates.html` 的 `t.codes[code]` 分派 | 失败时页面按**稳定错误码**选本地化文案，而不是对上游 updater 的英文 `message` 做字符串匹配。判据：`update_failure_codes_stay_in_family`（族号必须是 E1）与 `failure_classification_is_conservative`（"signature…" 不得判成网络；"updater not configured" 归 Other）钉住保守性 |
| **更新 journal（可核对的失败现场）** | ✅ **已接线（2026-10-03）** | `src-tauri/src/update_journal.rs`（`app_data_dir/update-journal/updates.jsonl`，JSONL + 512 KiB 轮转到 `.jsonl.1`；`JournalAction` 固定枚举 + 错误码白名单） | `update.rs::journal()` 在 started / check / check-result / download / downloaded / install / failed / discarded 八点调用。**只写事实**：版本号、错误码、一行摘要；**不写** URL / Token / 系统路径（单测 `record_serialises_one_line_without_urls` 断言行内无 `http`）。写盘失败只降级为 `log::warn`——journal 是观测设施，不得让更新链路因它失败 |
| **打包版 DevTools 入口** | ✅ **已接线（2026-10-03；`tauri` 加 `devtools` feature）** | `window.rs::toggle_devtools`（`is_devtools_open` → `open/close`）+ `menu.rs::MENU_ID_DEVTOOLS_TOGGLE`（`CmdOrCtrl+Alt+I`）+ `Cargo.toml` 的 `tauri = { features = ["tray-icon", "devtools"] }` | 应用菜单 `Harness` 子菜单末项；事件绕过应用状态（Harness 起不来时恰恰最需要它）。判据：不开 `devtools` feature 时 `open_devtools` 是**编译通过但什么都不做**的 API——这正是此前「打包版没有排障入口」的成因，因此改动必须连 feature 一起落 |
| **更新就绪的可见回执** | ✅ **已接线（2026-10-03）** | `update.rs::announce_ready`（下载成功时）→ `tray.rs::set_update_ready_status`（状态行）+ `notifications.rs::notify_update_ready`（`tauri-plugin-notification`） | 只有**一个**触发点：`download()` 成功。为什么需要：关窗即隐藏意味着更新页可能根本不在屏幕上。通知失败只记 `log::debug`——判定的回执仍是 `updates://status` 状态机，看不到通知不会漏掉更新 |
| **Harness 页 origin 跨重启稳定（粘性端口）** | ✅ **已接线（2026-10-03；CX-14）** | `crates/dsh-host/src/launch.rs`（`reuse_sticky_port` / `remember_sticky_port`）+ `crates/dsh-contracts/src/constants.rs::STICKY_PORT_FILE`（`<launch_root>/harness.port`） | 首个尝试优先复用上次**实际就绪**端口（以 stdout 为准）；端口被占用才回退随机预留。**修的是真缺陷**：WebView 的 `localStorage` 按 origin（含端口）隔离，端口每次随机会让页内偏好（可重映射快捷键 `dsh.keybindings.v1`、dshmarket 的 WebDAV/Gist 配置）每次重启全丢。判据：`sticky_port_is_reused_only_while_bindable`（占用时必须不复用、释放后必须复用）+ `sticky_port_roundtrip_and_rejects_invalid_values`（空/非数字/`0`/特权端口一律不复用）。落盘失败只 warn，不让启动失败 |
| **退出前「有没有会话在跑」检查** | ✅ **已接线（2026-10-04；CX-16）** | `crates/dsh-host/src/quit_probe.rs`（`inspect_quit`：握手换 cookie → `POST /api/session/list` → 读 `result.value.items[].running`；2s 超时，**问不出来按有活处理**）+ `src-tauri/src/quit_guard.rs`（`decide`：`Work`/`Unknown` → 模态确认；`NoWork` → 直接放行）+ `state.rs::launch_endpoint`（端点随实例生命周期存入/清空）+ `menu.rs` / `commands.rs app_quit` 两个退出入口 | 与上游桌面端的 `quit-inspection` 等价，但**不借 Node IPC**：本仓 Harness 是普通 `dsh web` 子进程，走的是契约 C5 那条已鉴权 HTTP/RPC（手机桥同一条）。权威判据是 Harness 自己的 `agents.get(id)?.status === 'running'`，壳不猜。2026-10-05 起有**原生模态确认**（`tauri-plugin-dialog`，「退出 / 取消」），此前的「第二次显式退出强制生效」计数器已删除——对话框本身就是显式答复，不再需要那个近似物。判据：`quit_probe` 7 个单测（含可证伪性：`items` 缺 `running` 字段必须归 Unknown 而非"没活"）+ 真 harness 上的 `tests/quit_probe_live.rs`（只断言"不得为 Unknown"，不预设会话数） |
| **随包 primary runtime 载荷（office skills / workspace-dependencies）** | ✅ **已接线（2026-10-07；CX-17）**：判据、组装、门控、MANIFEST、输入拉取、**默认构建接入**全部到位 | Rust 侧 `crates/dsh-host/src/paths.rs::primary_runtime`（返回 `Option<&Path>`；逐项判定与上游 `validatePayloadEntries` 同构）+ `env.rs` 的 `ENV_BUNDLED_PRIMARY_RUNTIME` 注入 + `constants.rs` 的 `PRIMARY_RUNTIME_DIR/ROOT`、`OFFICE_SKILLS_DIR`、`ENV_BUNDLED_PRIMARY_RUNTIME`；Node 侧 `scripts/prepare-runtime.mjs`（默认构建入口，含落盘自检）+ `scripts/fetch-primary-runtime.mjs`（拉取 + sha256 校验 + 解压）+ `scripts/prepare-primary-runtime.mjs`（组装 / `--check` / `--self-test`）+ `runtime-locks/primary-runtime.json`（URL/版本/sha256 唯一产地）+ `prepare-harness.mjs` 的三态判据与 `MANIFEST.json::primaryRuntime` | `npm run dev` / `npm run build` 经 `prepare:runtime` 默认把载荷装进 `src-tauri/resources/runtime/`，打包 glob 随即整个带进安装包；载荷完整时注入 `DSH_BUNDLED_PRIMARY_RUNTIME`，Harness 的 `skill-office` / `tool-workspace-dependencies` 两行（`dsh-sdk-app/cordis.patch.yml`，由该环境变量门控）随之启用。**为什么必须 fail-closed**：那两行一旦启用会立刻 stat 载荷里的解释器/node/pnpm/skills 资产，缺一个就在 Host 启动期抛错——那是「应用起不来」而非「少个功能」。**必备清单**：`python.exe`/`bin/python3`、`node/bin/node[.exe]`、`node/node_modules`、`pnpm/bin/pnpm.mjs`、`office-skills/scripts/check_office.py`、以及三份 `office-*/SKILL.md`。`Lib`/`lib/python*` **不**是硬需求（Windows 可嵌入发行版整个包是扁平的，实测 21.5 MB / 35 文件）。判据：`paths.rs::primary_runtime_requires_every_entry`（逐项抽走必须变 None）+ `env.rs::bundled_primary_runtime_is_injected_only_when_complete` + 脚本 `--self-test`（全齐放行 / 逐项缺失被拒 / 缺席合法 / runtime.json 规则生效）。**已在真产物上验证**：CPython + 上游 office-skills + 本仓真 node 组装进 `resources/runtime/`（107 MB）→ Rust 判完整并注入变量 |
| ↳ 载荷随默认构建进安装包 | ✅ **已接线（2026-10-07）** | `scripts/prepare-runtime.mjs`（一站式编排：`prepare-harness` → `fetch-primary-runtime` → `prepare-primary-runtime` → **落盘自检**）+ `package.json` 的 `dev`/`build` 改调 `npm run prepare:runtime` | `dev` / `build` 默认带载荷，产物即含 office skills。**自检是硬失败**：落盘后跑一次 `--check`，没报「载荷达标」就让构建红——静默少功能的包是最坏结果（§7.1 规则 3）。**退路**：`DSH_SKIP_PRIMARY_RUNTIME=1` 跳过整套载荷准备（跳过时 `.payload-root` 标记仍在、Tauri glob 仍成立，编译不会红）。**标记归属**：assemble 只清 `dependencies/` 而不再整目录 rm，因此 `prepare-harness` 写的 `.payload-root` 不会被连带删掉——两个脚本对同一目录不再有隐式顺序依赖 |
| ↳ Tauri 资源 glob 不容缺席的适配 | ✅ 已接线（2026-10-05，伴随 CX-17 落地） | `tauri.conf.json` 的 `resources/runtime/**/*` + `prepare-harness.mjs` / `stub-tauri-resources.mjs` 各自写 `<resources>/runtime/primary-runtime/.payload-root` | 让 glob 在**不带载荷**的检出上也匹配成功（Tauri 的资源 glob 匹配不到文件就让 build-script 失败）；载荷在时整个目录照带。Rust 的完整性判定不认这个文件——它只认 python / node / pnpm / office-skills |
| ↳ 载荷本体的来源（供应链） | ✅ **已接线（2026-10-07）** | `scripts/fetch-primary-runtime.mjs`（拉取 + sha256 校验 + 解压）+ `runtime-locks/primary-runtime.json`（URL / 版本 / sha256 的唯一产地）+ `scripts/prune-harness-deps.mjs` 的 `KEEP_FILE_NAMES`（`SKILL.md` 不再被当文档删掉） | 三个输入全部有确定来源，且**默认构建会拉**：**解释器** = python-build-standalone 的 CPython 发行版（Release 逐产物公布 sha256，5 个目标各自钉死）；**office skills** = 上游 `@deepseek-ai/dsh-skill-office` npm 包 `assets/` 下的三份 `SKILL.md` + `check_office.py`；**pnpm** = `pnpm@11.7.0` 的 `bin/pnpm.mjs`（npm 包里正是 schema 要求的路径）。**2026-10-07 拆除 8 个 python 库的 wheel 交叉安装**（numpy/pandas/python-docx/python-pptx/openpyxl/Pillow/lxml/XlsxWriter）：它们唯一的读者是上游**工具描述**与 skill 的 SKILL.md，本仓代码零引用，属死路。**为什么之前判成"本仓无法自产 SKILL.md"**：当时只看了**组装后**的 Harness 树，而 `SKILL.md` 早被 `pruneNodeModules` 的"`.md` 都是文档"规则删掉——与 2026-09-11 的 `yaml/dist/doc` 事故同类（按名字猜是不是开发产物猜错了）。修法在 `KEEP_FILE_NAMES` + 自测 15 项（含可证伪性：旧判据必须删掉 SKILL.md）。下载带指数退避重试 + **先校验再进缓存**（不能把来路不明的产物留在盘上供下次直接用）。真产物验证：解释器 sha256 校验 + 解压、office-skills 4 文件、pnpm 就位 |
| 插件故障归因（进程内） | ✅ 已接线 | `build/plugin-safety-guard.mjs::formatFaultDetails` + `::installPluginSafetyGuards` | `build/harness-node-entry.mjs::loadSiblingModule({ specifier: './plugin-safety-guard.mjs' })` → `::reportFaultAttribution`（**锚用符号名而非行号**：原先写的 `:25`/`:154`/`:15,37,44` 在 2026-10-10 被查出**早已指向注释与正则内部**——行号作为锚必然漂，而漂了没有任何东西会报错） |
| **启动阶段耗时埋点 + V8 字节码编译缓存** | ✅ **已接线（2026-10-10；B3）** | `build/harness-node-entry.mjs::phase`（4 个阶段点 `bootstrap`/`projection`/`sweep`/`dsh-entry-import` + 由 stdout 上 `dsh web:` 触发的 `ready`，格式 `[harness-node] phase <名> <段耗时>ms total=<累计>ms`）+ `enableCompileCache()`（目录 `<app_data_dir>/cache/node-compile-cache`，建不出来则退回 Node 默认目录并明说） | 入口自身，`import(dshEntryPath)` **之前**。**判据**：`phase` 行是「启动到显示 dsh web 耗时」唯一的可证伪基线（此前日志无时间戳 ⇒ 无法对照）；`compile cache:` 行必须报**生效目录**而不是 `status`——实测 `status: ENABLED` 对根本写不了的目录同样返回，故不能当成功判据。`DSH_NO_PHASE_READY=1` 关 ready 观察（负控），`NODE_DISABLE_COMPILE_CACHE=1` 由 Node 自己禁用。**不改变行为**：埋点只写日志、写失败自行吞掉（入口在启动关键路径，任何新抛点都等于「装了新版起不来」） |
| **插件分级隔离 Tier 0/1/2** | 🗄️ **已归档（2026-09-10 批次 F：冻结并归档）** | 代码已删除（`plugin_worker.rs`、`plugin-worker-host.mjs`、`PluginWorkerClient`）；设计文档在 `docs/archive/plugin_isolation_architecture.md`。它此前**从未接线**，且不在真实插件挂载路径上 | **无**（且永远不会走这条路：真实挂载在 Harness 进程内的官方 Cordis 体系） |
| ↳ 插件安全（**当前实际生效的那一条**） | ✅ 已接线，**但只在进程内** | `build/plugin-safety-guard.mjs:formatFaultDetails` | `build/harness-node-entry.mjs` 的未捕获异常/拒绝处理器。**同进程的插件崩溃仍可能带走 Harness**——不得表述为「插件崩溃不拖垮主程序」 |
| ↳ **已知缺口：安全模式不隔离插件**（2026-09-22 登记，口径见 [ADR-051](docs/adr/051-archived-capability-known-gap.md)） | 🗄️ **归档能力的已知缺口**（**已裁定接受**：不是欠债、不是计划中——四字段见右侧；登记在归档行下是因为它**只能由恢复 ADR-040 的能力才能修**） | **缺口**：进安全模式**不会**隔离任何第三方插件。安全模式只换 profile 与 patch 层（C10），而插件挂载由 Harness 进程内官方 Cordis 体系按 `profiles/.generations/desired.json` 投影决定，**投影不读 profile**——坏插件在安全模式下照常加载，仍可带走 Harness。**判据**：真实挂载点在官方 loader 上，本仓够不着，硬接即再造假设施（ADR-040）。**恢复前提**：官方 loader 暴露可挂载隔离点（前置条件同 ADR-040 后果段）。**现场级证据**：`[harness-node] generation projection: … bundles=[…]` 列出的挂载清单在安全模式下**不变**（含第三方），`web boot: N entries did not activate` 同理照旧出现 | **无**（**并禁止用特例分支绕开**：在 `projectGenerations()` 里加「安全模式就跳过插件」= 在 `desired.json` 之外另立第二权威，ADR-007 所禁）。完整登记在 `docs/archive/plugin_isolation_architecture.md`「已知缺口」段 |
| **多模型工具网关** | 🗄️ **已归档（2026-09-10 批次 F）** | crate 已从 workspace 删除；设计文档在 `docs/archive/model_gateway_design.md`（文首有归档说明与恢复判据） | **无** |
| **一键脱敏诊断包 `diagnostics.zip`** | ✅ **已接线（2026-09-10 批次 D）** | `crates/dsh-host/src/diagnostics_export.rs`（5 类脱敏规则：launch token / `dsh-auth-*` cookie / 路径用户名段 / API key / 代理口令；每条有正反用例 + 端到端「产物内无原文」断言）；产物落 `app_data_dir/exports/` | 命令 `diagnostics_export` ← 错误页「导出诊断包」按钮、`logs.html` 同功能按钮、菜单「Harness → Export Diagnostics…」（三者都显示产物路径） |
| LAN 手机桥（扫码配对 + cookie 握手） | ✅ 已接线 | `src-tauri/src/mobile_bridge.rs`、`state.rs::sync_mobile_target`（:352）、`menu.rs` 手机子菜单 | 应用菜单 `mobile-pair` / `mobile-stop` |
| 壳层结构化日志 `desktop.log` | ✅ 已接线 | `src-tauri/src/logging.rs::init`（:46） | `src-tauri/src/lib.rs:70` |
| 补丁分级与失败降级 | ✅ 已接线 | `scripts/patch-layers.mjs`（分级表按**包名**索引）、`patches/LAYERS.md`、`prepare-harness.mjs` | 构建期；结果落 `MANIFEST.json:patches[]`（含 `target` 字段标明通道） |
| **双上游运行时通道（next / alpha）** | ✅ 已接线（2026-09-15；在役通道：`next` / `alpha`；休眠通道：无——alpha 曾经 [ADR-056](docs/adr/056-alpha-channel-dormant.md) 休眠，同日由 [ADR-057](docs/adr/057-alpha-channel-restored-and-dual-promotion.md) 复役；休眠机制保留，`status` 字段是唯一产地，本句由 `npm run gate -- doc-facts` 对账） | `scripts/dsh-targets.mjs`（目标总表）+ `patches/<target>/`、`packages/<target>/`、`harness-deps/<target>/` | `prepare:harness -- --dsh-target=<name>`；`release.yml` preflight 从 tag 的预发布通道名推导目标（未知通道**与休眠通道**直接失败）；`smoke.yml` 有 `dsh_target` 输入；`npm run gate -- patches` 逐目标检查、`npm run gate -- drift` 逐通道对照 dist-tag（休眠目标显式跳过）。见 §8.6 |
| ↳ 补丁行号重算（移植到另一条上游线时） | ✅ 已接线 | `scripts/recount-patches.mjs`（重生成，依赖 git）+ `scripts/relocate-patch-hunks.mjs`（只改 `@@` 行、原文保真、无外部进程）**二选一** + `check-patch-applicability` 的 ±20 窗口判据 | 升级/移植工序；`patch-package` 按行号定位且偏移超 ±20 行即失败，只按内容搜索的预检会漏报。**同一份补丁只用一条路径**——见 §8.6 |
| **统一 IPC 封套 `IpcEnvelope<T>` + 错误码总表** | ✅ **已接线（2026-09-10 批次 E）** | `crates/dsh-contracts/src/ipc.rs`（`IpcEnvelope<T>`，`error` 载荷为 `AppError`）+ `src/errors.rs` 的 `codes` 模块（`E1xxx`~`E7xxx`，族号↔类别有测试） | `src-tauri/src/commands.rs` 的 **21 个命令**中 **20 个**返回 `CommandResult<T>`；唯一例外是 `install_mode`（安装形态判定，刻意返回裸三态字符串 `installed` / `portable` / `unknown`，调用方 `updates.html`）。五个页面（error / plugin-recovery / logs / updates / feedback）均解包 `success` |
| ↳ 命令面 `Result` 语义 | ✅ 已接线 | `commands.rs::CommandResult` 文档注释 + 测试 | 外层 `Result` **恒为 `Ok`**（Tauri 编译要求）；语义全在内层封套。**返回 `Err` 会丢掉错误码**，属违规 |
| **自动更新链路** | ✅ 已接线（2026-09-10 批次 B 闭环） | `src-tauri/src/update.rs` + `tauri-plugin-updater`（`lib.rs:58`、`UpdateManager` 构造于 `lib.rs:145`）；`tauri.conf.json` 开启 `bundle.createUpdaterArtifacts`；**安装形态判据的唯一产地是 `crates/dsh-host/src/install.rs::classify`**（卸载器文件 / macOS `.app` 结构，2026-10-17 起替代 `executable_dir` 路径猜测——该 API 在 Windows/macOS 恒返回 `None`，曾导致任何安装形态都被判成便携版、自动轮询从不启动；单测 `install_mode_follows_positive_signals` 按修复前形态可证伪，静态守卫 `gate -- install-mode` 禁止该写法回流） | 菜单 `updates-check` → `window::show_updates_page` + `UpdateManager::check(true)`；`frontend/updates.html` 调 `updates_status` / `updates_check` / `updates_download` / `updates_install` / `updates_skip` 并监听 `updates://status` |
| ↳ 更新源归属与签名密钥 | ✅ 已闭环（2026-09-10；**端点 2026-09-30 通道化**，见 [ADR-053](docs/adr/053-channelized-updater-manifest.md)） | 端点按通道指向滚动 Release（`releases/download/updater-<channel>/latest.json`），**URL 的唯一产地是 `scripts/updater-manifest.mjs`**，由 `tauri build --config` 在构建期注入（`tauri.conf.json` 只留默认值）；配置里的 `pubkey` 与 `~/.tauri/dsh-desktop.key.pub` **逐字节一致** | 私钥经 CI Secret `TAURI_SIGNING_PRIVATE_KEY` 注入（无口令），本地离线备份在 `~/.tauri/backup/`。发布后由 `npm run gate -- update-channel` 断言「端点 version ≥ 该通道最新 tag」 |
| **OS 级代码签名 / 公证** | 🗄️ **刻意不做——能力边界，非欠债、非计划中**（ADR-046 / ADR-044）。词表里 🗄️ 是唯一表示「裁定不做」的状态；本条与「曾实现后删除」的区别是它**从未实现过** | **无代码，且裁定不接线**：不申请 Authenticode / Developer ID / Azure Trusted Signing 证书，不加 `certificateThumbprint` / `macOS.signingIdentity`，不在工作流注入 `APPLE_*` / `WINDOWS_CERTIFICATE*`。判据（防回归）：上述词汇在 `.github/`、`tauri.conf.json`、`scripts/` 内**零命中即正确**（出现命中反而是违规接线）；README 不得正面宣称（`verify:claims` 的 `os-signing-claim`） | **无**——完整性签名的唯一一层是 updater 的 **minisign**（`plugins.updater.pubkey`）。两条链**不可互相替代**，本行不影响 minisign。用户侧代价与解法写在 `SECURITY.md`「明示边界」与两份 README |
| **应用内日志查看器** | ✅ **已接线（2026-09-10 批次 D）** | `frontend/logs.html` + `crates/dsh-host/src/logs_view.rs`（三来源，尾部读取，**截断如实上报 `truncated`**） | 菜单「Harness → View Logs…」→ `window::show_logs_page`；页面调 `logs_read` / `open_logs` / `diagnostics_export` / `harness_open`。「Reveal Log Folder」保留为次入口 |
| **错误页「安全模式」按钮** | ✅ 已接线（2026-09-10 修复调用名；同日补完启动链路） | `src-tauri/frontend/error.html` 调 `safe_mode_action`（`action: "restart"`），失败经 `fail()` 可见上报 | 错误页按钮 → `commands::safe_mode_action` → `HarnessSupervisor::restart_in_safe_mode`（此前调 `restart()`，实际只是**普通重启**——按钮曾是谎话） |
| **恢复页交互** | ✅ **已接线（2026-09-10 批次 C）** | `plugin-recovery.html` 调 `recovery_status` / `recovery_action`（`restart` / `safe-mode` / `show-log` / `quit`）并**检查封套 `success`**；监听 `harness://status` 反映恢复进度 | 错误页「插件恢复…」按钮 → `recovery_open` → `show_recovery_page`。数据来自 `dsh_host::diagnostics`（此前零消费者的那条链） |
| ↳ 插件卸载 / 禁用 | 🕓 **计划中**（依据上游是否提供停用语义） | **无代码，且刻意不实现**：经核验，市场安装的插件没有真正的可逆解除挂载方式——改名会被冷启动投影还原，改 `desired.json` 会触发 `sweepRegistry()` 真删目录（证据链见 `docs/dev-plan-disconnected-points.md` §3 批次 C） | **无**。恢复页只提供非破坏性动作；未知动作返回 `E7002` 而非静默 `false` |
| **`safe-mode.html`** | 🗄️ **已删除（2026-09-10 批次 C）** | 页面 + 资源 + 配置项一并移除；`window::show_safe_mode_page` 同步删除 | **无**。它的每处交互都要求「插件移除」后端（上文已裁定不做），接上只会交出「其余按钮仍读空气」的页面；其独有能力（进安全模式）已由错误页 / 恢复页 / 原生菜单三处覆盖 |
| 手机桥状态可见性 | ✅ 已接线（2026-09-10） | `src-tauri/src/menu.rs` 的 `Phone` 子菜单状态行 + `mobile_bridge::status_label`；`MobileBridge::on_connected_change`（镜像上游 `onConnectedChange`）在配对状态翻转时回调 | 菜单构建时初始化，**手机侧 `POST /pair` 成功**、菜单配对/停止时均经 `refresh_bridge_status` 刷新（`lib.rs` setup 注册监听器） |
| ↳ 页内手机状态指示器（Harness 侧边栏） | ✅ 已接线（2026-09-10） | `harness_ui.rs::INJECT_SCRIPT`（`include_str!` 内嵌 `frontend/harness-ui-inject.js`），挂在 `[data-dsh-sidebar-settings]` 下；状态下发 `push_phone_status` 走 `webview.eval`，**不新增 IPC 命令** | 两个推送点：连接翻转（`on_connected_change`）与页面加载完成（`on_page_load`）。**只做状态指示、不可点击**——配对/停止仍只走原生 `Phone` 菜单，因此它不渲染成按钮 |
| **Harness 页注入机制（preload 等价物）** | ✅ 已接线（2026-09-10；同期更正「无初始化脚本」的误判） | 主窗口 builder 的 `initialization_script`（`lib.rs`）+ `frontend/harness-ui-inject.js`；脚本按 origin 自我早退（本地页与子框架不注入） | 无头行为自测 `npm run gate -- harness-inject`（19 项断言 + 可证伪性检查，已进 CI） |
| **CLI 打包与核验能力（`package-cli.mjs`）** | ✅ 已接线（2026-09-13；**发布通道于 2026-09-24 退役，本能力保留**） | `scripts/package-cli.mjs`（命名 / 边车 / manifest / 回读校验 / 产物执行自检 / 已发布核验），约 40 项可证伪判据；由 `ci.yml` 与 release `preflight` 的 release 档（门禁 `cli-package`）守着 | **本地手动使用**（`npm run package:cli`）。🚫 **不再上传到 Release**：原 `release.yml` 的 `cli` / `cli-publish` job 已删除。依据「本仓之外零消费者 + 产物不含 runtime 不自足」，见 [`docs/dev-plan-cli-distribution.md`](docs/dev-plan-cli-distribution.md) §5。归档的 `scripts/dry-run-cli-publish.mjs` → `docs/archive/`。**crate 与打包脚本不得随之删除**——守卫 `checkCliCrateRetained` 守着这一点 |
| ↳ runtime bundle 独立发布（Phase 2） | 🕓 **计划中**（带触发条件，刻意不做） | **无代码**。门槛与前置改造写在 [`docs/dev-plan-cli-distribution.md`](docs/dev-plan-cli-distribution.md) §4：出现第一个非本仓消费者，或开工 H3-a 运行时更新事务 / H2-a 兼容矩阵时才做；提前单独做就是为「可能有用的未来」建基础设施（同批次 F 归档 `dsh-model-gateway` 的判据） | **无** |
| **系统托盘 + 关窗驻留** | ✅ **已接线（2026-09-18 批次 0.2-B1）** | `src-tauri/src/tray.rs`（图标 / 菜单 / 事件 / 状态行）+ `window.rs::reveal_main_window`（show → unminimize → focus 三步）；图标资产 `icons/tray-32.png` 与 `icons/tray-template.png` 由 `scripts/generate-tray-icons.mjs` 派生；`tauri` 开 `tray-icon` feature（`Cargo.toml`） | `lib.rs` setup 建托盘（失败只记日志、启动继续）；`on_window_event` 的 `CloseRequested` 改为 `prevent_close` + `hide`；菜单事件与应用菜单**共用** `menu::handle_menu_event` 与同一批 id。**无自动门禁**：托盘区在 CI 容器里不存在，验收靠三平台手工（见 §2 与 0.2-B1 记录） |
| ↳ 驻留期的退出语义 | ✅ 已接线 | `lib.rs::shutdown`（先收手机桥、再收 Harness）；`menu.rs` / `commands.rs` 的 Quit 与 `app_quit` 均先 await 它 | 托盘 / 应用菜单 / 错误页三处 Quit。**为什么必须做**：`app.exit()` 不经过 `CloseRequested`，不先停机就只能由 JobObject / PDEATHSIG 强杀子进程 |
| ↳ 托盘后端缺失时的降级 | ✅ 已接线（2026-09-18，**防启动期回归**） | `tray.rs::appindicator_available`（Linux 用 `dlopen` 探 `libayatana-appindicator3.so.1` / `libappindicator3.so.1`）+ `catch_unwind` 兜底 | `create()` 在任何 `dlopen` 失败 / panic 时返回错误而不 panic；`lib.rs` 只记 error 日志，**应用照常启动**，关窗行为自动退回「关窗即退出」。理由：`libappindicator-sys` 用 `Lazy<Library>` + `panic!`，不先探测会把整个应用带崩 |
| **应用内反馈入口** | ✅ **已接线（2026-09-18 批次 0.2-D2）** | `src-tauri/src/feedback.rs`（`Channel` 四值白名单 + `context()`）+ `frontend/feedback.html` + `crates/dsh-contracts/src/constants.rs` 的 **CX-13** 四个渠道 URL + `crates/dsh-host/src/runtime_manifest.rs`（从 `MANIFEST.json` 读通道 / DSH 版本 / 补丁统计） | 命令 `feedback_context`（页面取版本与路径）、`feedback_open`（打开页面，错误页按钮亦走它）、`feedback_channel_open`（白名单内打开浏览器）；菜单 / 托盘的「Send Feedback…」经 `window::show_feedback_page`。**应用自身不上传任何内容**——只读本机信息 + 调 `opener` |
| ↳ 反馈页的「该带什么」自述 | ✅ 已接线 | `feedback.rs::context()`（版本 / 平台 / 通道 / DSH 版本 / 补丁 applied-failed-skipped / 诊断包与日志目录）+ `runtime_manifest::RuntimeManifest::identity_line` | 读不到 `MANIFEST.json` 时**不报错**，改以 `manifest_readable: false` + `manifest_error` 如实上报（§7.1 规则 3）——反馈页恰恰是「安装可能坏了」时最需要打开的页面 |

### 7.3 维护方式

- 新增能力时：先写代码，再在本表补一行——**顺序不可颠倒**。
- 状态词只有五种，含义互不重叠，**不得混用**：
  | 状态 | 含义 |
  |------|------|
  | ✅ 已接线 | 有代码、有运行时调用方，用户可见 |
  | ⚠️ 未接线 | **代码已写但无运行时调用方**——这是「欠债」，必须登记销账批次 |
  | ❌ 未实现 | **代码不存在**——这是「缺口」，不得对外宣称 |
  | 🕓 计划中 | **代码不存在，且刻意不现在做**——这是「决策」，不是欠债。必须写明后置理由（通常是为等前置能力稳定） |
  | 🗄️ 已归档 | **曾实现，现已删除并裁定不做**——这是「结论」。必须写明归档判据；文档保留在 `docs/archive/` 供追溯设计意图 |
  「未接线」与「计划中」的区别是**有没有代码**：前者是写了没接（欠债），后者是还没写（决策）。把计划中说成未接线会误导读者去找不存在的代码；把未接线说成计划中则是在给欠债打掩护。
  「已归档」与「未接线」的区别是**代码还在不在**：归档是欠债已销账（删了，并给出不做的理由），未接线是债还挂着。**归档不是「计划中」**——不要用「以后可能做」来软化一个已经裁定不做的决定。
- 修改未接线模块时：必须同步删除其 `⚠️ 未接线` 横幅、更新本表状态。
- **归档一个模块时**：代码删除、文档移入 `docs/archive/` 并在文首写归档说明（判据 + 恢复前提），本表状态改 🗄️，`README` 对应表述同步收敛。**删除的文件名要写进本表**——否则下一个人只会看到「某个能力不见了」。
- **归档还要登记残留缺口**（ADR-051）：归档判据说明的是「为什么不做」，读者从中**推不出**「不做之后现在会缺什么」。若该能力有用户可感知的残留影响，必须在本表**归档行的 `↳` 子行**（不另起一行冒充新能力）按四字段登记——**缺口**（具体不会发生什么）/**判据**（为什么不修）/**恢复前提**（什么条件重开）/**现场级证据**（用户能看到的日志行或界面现象）。四项缺一不可。**缺口不得用代码补**：在单一权威之外加特例分支绕开缺口，属于 ADR-007 禁止的做法。
- 评审 / 发布前自查：`grep -rn "⚠️ 未接线\|未实现" AGENTS.md README.md docs/` 应只命中**确实未接线/未实现**的条目（归档项不在其中，它们改用 🗄️）。除真实欠债外，以下三类命中是**预期内**的，不要为消除它们而改写文本：
  1. **词表与纪律条文本身**（§7.3 的状态词表、README 的状态标记约定、本条的规则文字）；
  2. **历史盘点记录**——`docs/dev-plan-disconnected-points.md` §1 的 D1~D11 标题记录的是「盘点当时」的状态（该节开头有显式声明）。把它们改成「已修复」会让后来者看不出当初断在哪；
  3. 引用这些术语的规范文档（如 `dsh-upgrade-checklist.md` 的操作说明）。
- **跨语言断言必须可证伪**：新增「X 一定会发生」这类关于页面 / 脚本行为的断言时，按 `scripts/verify-harness-inject.mjs` 的模式配一段**变体回退检查**——把被守护的行为打回旧写法，断言必须变红，否则断言是装饰。同时守卫**不得依赖检出配置**（行尾、路径分隔符）：CRLF 检出下必须与 LF 表现一致。
- **「扫出来再校验」的门禁必须断言数量不为零**（2026-09-22，`E5-空` / ADR-051 附带发现）：凡是先从源码里扫出一组 X、再逐个校验 X 的守卫，必须同时断言**扫出的数量 > 0**。只断言「扫到的都合规」会容忍「一个都没扫到」——那等于门禁替一段**不存在的检查**背书，比漏报更危险（输出还是一行绿色）。触发条件是**写法变更**：入口加了一种新的 import 形态、表格换了一种列结构，扫描器不认了就静默归零。本仓已踩两次——`verify-claims` C3 的表格解析，与 `verify-harness-entry` E5 漏认 `specifier: './x.mjs'` 形态（后者修前在**零个受检模块**下全绿，而它守的正是「三份打包清单漏登记」这条 v0.7.0-alpha.1 真实事故）。**扫出数为 0 时，先怀疑扫描器，再怀疑源码。**
- **计划文档的预算纪律（2026-09-30 起，S2-2）**：新增任何计划类文档，必须同时归档或改写一份旧计划文档——在役 docs 总量不得只增不减；**计划文档无权延期或改写任何 ADR**，要改 ADR 只能走 superseding / 状态修订 ADR（ADR-055 之于 ADR-047、ADR-056 之于 ADR-052 是正确形态）。违反这条即视为文档回归。
- 与 B1 的联动：任何新增 `patch-package` 补丁必须同时登记进 `patches/LAYERS.md` 与 `scripts/patch-layers.mjs`，否则 `prepare-harness.mjs` 会以「未登记」告警并回退默认层。
- **门禁清单只有一个产地（2026-10-03，治理 G1/G2/S4-5）**：每条门禁（脚本 / 参数 / 分档 / **为什么有它**）登记在 `scripts/gate-manifest.mjs`，编排器是 `npm run gate`。**新增门禁只登记总表**——不要在 `ci.yml`、`release.yml` 或本地快档里另抄一行清单：改版前三份手抄清单互相漂移且漂移不报错，直接后果是 `unwrap-hygiene` 的**真检查**、`doc-facts`、`github-config`、`fault-patterns` 四个守卫**写了却不在任何自动流程里**，而文档还在宣称它们盯着代码（§7.1 规则 2 的同类形态）。判据：`npm run gate -- gates`（每个支持 `--self-test` 的脚本要么登记、要么带理由豁免且扫出数 > 0；每个分档步数 > 0；每种模式要么进分档、要么写 `manual` 理由）。旧名 `verify:<name>` 仍是可解析的别名，历史文书因此不必改写。
- **本表只覆盖「契约 / 能力」级宣称**。比它更细一层的问题是「命令写了但没人调用、页面打包了但不可达」——那类断线在 Rust 里不可见（`src-tauri` 是 `rlib`，`pub` 项一律算「可达」，`dead_code` 永不触发），只能靠 `npm run gate -- ipc-surface` 静态比对。该脚本的检查项、允许清单与「为什么必须有它」，写在脚本头部注释里，新增例外必须**在 `ALLOW_*` 里写明理由**。
  - 其中 **E7（菜单项 id ↔ `handle_menu_event` 分支）** 是批次 0.2-B1 新增的：托盘与应用菜单**共用同一批 id**，而「加了菜单项忘了接处理器」的后果是一个点了完全没反应的项——没有编译错误、没有日志、没有既有守卫能看见。`--self-test` 用三组夹具（缺分支 / 守卫式早退 / 注释里的 id）钉住该判定本身，已进 CI 与 release preflight。

---

## 8. 版本与发布（Versioning & Release）

> 📖 **8.1~8.5**（版本、变更日志、CI、发布步骤与 tag 清理）见 `docs/release-runbook.md`。

### 8.6 双上游运行时通道（next / alpha）

自 2026-09-15 起，本仓**同时维护两条上游运行时通道**，各自钉一个 DSH 版本、持有一套
补丁与 vendored 覆盖包：

| 目标 | 上游线（`upstreamDistTag`） | 固定的 DSH | 补丁 / vendored | 桌面后缀（`publishChannel`） | 对应的桌面版本形态 |
|------|--------|-----------|----------------|------------------|------------------|
| `next`（默认） | npm `next` dist-tag | `0.2.0-rc.2` | `patches/next/`（10 个）、`packages/next/`（已清空） | `rc` | `<x.y.z>-rc.<n>[+<w>]` |
| `alpha` | npm `alpha` dist-tag（2026-09-30 复役，[ADR-057](docs/adr/057-alpha-channel-restored-and-dual-promotion.md) 修订 ADR-056） | `0.2.1-alpha.2` | `patches/alpha/`（12 个）、`packages/alpha/`（已清空） | `alpha` | `<x.y.z>-alpha.<n>[+<w>]` |

> 🔴 **桌面版本号 = 合成号**（[ADR-061](docs/adr/061-synthetic-version-model.md)），**不是上游版本号的副本**：
> `<上游精确版本>.<n>[+<w>]`——前三段与上游自己那段预发布标识原样照抄，`<n>` 是本仓序号。
> 上表最后一列给的是**形态**而不是值：写一个具体号，锚点一动它就漂，而那一格**没有任何守卫**
> 盯着（C1 只读第 3 格「固定的 DSH」）。ADR-057 的跨通道单调要求继续有效，其旧数值示例
> （`0.7.3-alpha.1 > 0.7.2-rc.1`）的表达形式已作废。字段归属与「本仓钉的是哪个上游版本」的
> SSOT 见 [`docs/version-policy.md`](docs/version-policy.md) §3.3 / §4。

> 🌉 **换代期必须先发「桥接版」**（[ADR-063](docs/adr/063-bridge-release-for-version-model-cutover.md)）：
> 合成号跟随**上游**版本线（`0.2.x`），线上历史 tag 属**旧模型**（`0.7.x`）⇒ 换代首个合成号
> 排序必然更低，`updater` 默认判据（`release > current`）会让它**零投递**。处置是先在**同一条
> 通道**上发一个版本号更高、内置 `plugins.updater.allowDowngrades` 的桥接版（登记进台账
> `bridges[]`），桥接版**真的送达**之后合成号才收得到。豁免判据三条（同通道 / ≥ 该通道最高
> tag / **该 tag 已发布**）见 `docs/version-policy.md` §7.4；放宽只对装过桥接版的人生效——
> 那笔代价如实登记在 ADR-063 决策 7，不要把它当成常规手段。

> ✅ **next 线于 2026-09-30 从 `0.1.5-rc.3` 跨两个 minor 推进到 `0.2.0-rc.2`**（ADR-057 同批
> 恢复 alpha 在役并推进到 `0.1.7-alpha.2`）：预检 clean 2 / conflict 12，经
> `scripts/merge-migrate-patches.mjs` 三路合并 + 逐补丁语义裁定后，退役 6 个（两线合计：
> `ui-layout` / `ui-workspace` / `ui-agent-preset` 双线 + `ui-model-selection` 仅 next）、
> 语义重做 4 个；预设传递插件 `dsh-desktop-preset-transfer` 整链退役（上游把 preset roots
> 文件模型重铸为注册模型）。双线锚点现已各自对齐上游 dist-tag，`npm run gate -- drift` 不再告警。
> 逐条裁定见 [`patches/LAYERS.md`](patches/LAYERS.md) 的 2026-09-30 记录。

> ✅ **alpha 线于 2026-10-07 从 `0.1.7-alpha.2` 跨 minor 推进到 `0.2.1-alpha.1`**（ADR-060）：
> 预检 clean 4 / conflict 7（11 个补丁），经 `relocate-patch-hunks.mjs` 行号重算（41 个 hunk
> 全部对齐）+ 3 个包语义重做 + 1 个退役（`ui-model-selection`）后 **10 个补丁全部 clean**，
> 两线补丁集在包名层面归一。逐条裁定见 [`patches/LAYERS.md`](patches/LAYERS.md) 的
> 2026-10-07 记录。

> **两条线的补丁数可以不同，这是正常的**：2026-09-30 退役后 next 10 个 / alpha 11 个
> （`ui-model-selection` 当时**仅 next 退役**——上游 0.2.0 自带搜索而 alpha 线尚无）；
> 2026-10-07 alpha 线推进到 `0.2.1-alpha.1` 后该补丁在 alpha 线也退役，**两线各 10 个**。
> 补丁**净减少**是补丁退役机制想要的方向——不要为了「两条线一样多」而把退役的补丁加回去。

> 📖 通道解耦（`upstreamDistTag` vs `publishChannel`；旧名 `channel` 已于 2026-10-09 按计划 2e 改名）、补丁行号重算与两条线的发布记录见 `docs/release-runbook.md` §8.6；字段归属（谁拥有哪个字段）见 `docs/version-policy.md` §3.3，`MANIFEST.json` v3 六块各回答什么问题见同文件 §3.4（计划 2g）。
