Skip to content

fix(deps): @xmldom/xmldom 版本域 override 0.8.15/0.9.12 + fast-uri>=3.1.6,prod audit 4→2 / 全量 8→2 (#2154) - #2240

Merged
DeliciousBuding merged 1 commit into
masterfrom
fix/deps-xmldom-advisory
Sep 2, 2026
Merged

fix(deps): @xmldom/xmldom 版本域 override 0.8.15/0.9.12 + fast-uri>=3.1.6,prod audit 4→2 / 全量 8→2 (#2154)#2240
DeliciousBuding merged 1 commit into
masterfrom
fix/deps-xmldom-advisory

Conversation

@DeliciousBuding

Copy link
Copy Markdown
Collaborator

为什么

CI job Vuln scan (pnpm audit prod+full).github/workflows/checks.yml:2325,path-filtered 到 app/**)在 master 上显示 skipped,属潜伏红:任何碰 app/** 的 PR 都会踩。实测 #2238 已被它打红:

##[error]pnpm audit (scope: prod) found vulnerable package(s) not in exception registry: GHSA-6gmq-8vp8-gcm6 — failing build
##[error]Process completed with exit code 1.

(run 33658185803 / job 100341979789)

按仓库例外政策(scripts/verify/vulnerability-exceptions.json 抬头:仅允许登记上游无修复版本的漏洞;有修复版本的漏洞必须通过 overrides 升级解决,禁止登记),本 PR 全部走 override 升级,未新增任何例外登记

涉及的 advisory(均为 2026-09-02 新发布)

GHSA 严重度 published 受影响范围 patched lockfile 原锁
GHSA-6gmq-8vp8-gcm6 / CVE-2026-83610 @xmldom/xmldom medium 2026-09-02T15:18:20Z >=0.7.0 <=0.8.14 0.8.15 0.8.13
同上 @xmldom/xmldom medium 同上 >=0.9.0 <=0.9.11 0.9.12 0.9.10
GHSA-jqff-g426-hqxp fast-uri high 2026-09-02T15:41:53Z >=3.0.0 <3.1.6 3.1.6 3.1.5
GHSA-fph4-wmhf-6fwf fast-uri high 2026-09-02T15:42:39Z >=3.1.2 <3.1.6 3.1.6 3.1.5
GHSA-f65p-4m7j-42xc fast-uri high 2026-09-02T15:43:30Z >=3.0.0 <3.1.6 3.1.6 3.1.5
GHSA-5jgf-p345-68v8 fast-uri high 2026-09-02T15:44:30Z >=3.1.3 <3.1.6 3.1.6 3.1.5

为什么 fast-uri 也在这个 PR 里:CI 的 prod 步先 exit 1,同 job 的全量步从未执行,所以这 4 枚 high 在 #2238 的红灯里完全不可见(本次是全量 audit 才扫出来)。只修 xmldom 会把红从 prod 步搬到 full 步,门禁仍然红,#2238 仍然被卡。它们同样"有修复版本 → 禁止登记例外 → 必须 override 升级",形态与 xmldom 完全一致,写集也不变,故同批修掉。

依赖来源(app/pnpm-lock.yaml 实测 + registry 核对上游声明范围):

消费者 声明的 @xmldom/xmldom 范围 原锁 现锁 是否落在声明范围内
@expo/plist@0.7.0(mobile-rn>expo>@expo/cli) ^0.8.8 0.8.13 0.8.15
mammoth@1.12.0 ^0.8.6 0.8.13 0.8.15
plist@3.1.1(…>xcode>simple-plist) ^0.9.10 0.9.10 0.9.12

fast-uri 唯一路径:.>stylelint>table>ajv@8.20.0>fast-uri(dev/lint 链,不在 --prod 范围)。

先例对照(#2224 = d7c326c

先核过先例的实际落点,没有自创形态:

  • git show d7c326c --stat → 写集只有 app/pnpm-lock.yaml + app/pnpm-workspace.yaml。overrides 的 SSOT 是 app/pnpm-workspace.yamlfix(frontend): move pnpm overrides to workspace config #1713 迁入),不在 app/package.json——后者的 pnpm 字段至今只有 hoistedDependencies,没有 overrides
  • 先例的两种形态本 PR 都用上了:
    1. 新增一项(先例 postcss-selector-parser: ">=7.1.3 <8",追加在 overrides 块末尾)→ 本 PR 追加 "@xmldom/xmldom@0.8" / "@xmldom/xmldom@0.9" 于同一位置。
    2. 把钉在过期下限的既有 override 提到 patched 下限(先例 "brace-expansion@1": ">=1.1.16 <2"">=1.1.18 <2")→ 本 PR fast-uri: ">=3.1.5 <4"">=3.1.6 <4"
  • 版本域限定语法也照先例:仓库既有 "brace-expansion@1"/"@2"/"@5""js-yaml@4""uuid@7"/"@9" 都是 名字@域 选择器,本 PR 用 "@xmldom/xmldom@0.8" / "@xmldom/xmldom@0.9"刻意按域拆两条,避免把 0.8 的消费者(@expo/plist / mammoth)跨 minor 强推到 0.9。
  • 值形态统一 ">=patched <下一个域"(先例全部是这个写法),不是硬钉单一版本,方便后续 patch 自动跟进。
  • commit 主题走 fix(deps): 中文摘要 + 可核对的前后计数(先例 …全量 audit 5→2),正文逐条列 GHSA/CVE、严重度、路径、SSOT 位置、实测数字。

改动

app/pnpm-workspace.yaml(overrides SSOT,3 行):

 overrides:
   undici: ">=7.28.0 <8"
-  fast-uri: ">=3.1.5 <4"
+  fast-uri: ">=3.1.6 <4"
   "brace-expansion@1": ">=1.1.18 <2"
   …
   postcss-selector-parser: ">=7.1.3 <8"
+  "@xmldom/xmldom@0.8": ">=0.8.15 <0.9"
+  "@xmldom/xmldom@0.9": ">=0.9.12 <0.10"

app/pnpm-lock.yaml:30 行变动,只动 3 个包@xmldom/xmldom 0.8.13→0.8.15、0.9.10→0.9.12、fast-uri 3.1.5→3.1.7),无其他包版本漂移。

写集只有这两个文件;没有改任何 package.json(先例同样没改)。

验证:audit 前后计数

命令(直连 registry.npmjs.org——本机默认华为云镜像不实现 audit POST,会返 405;这也是 verify-vulnerability-gates.sh 里那条诊断提示的由来):

cd app && pnpm audit --prod --json --registry=https://registry.npmjs.org
cd app && pnpm audit       --json --registry=https://registry.npmjs.org
范围 修复前通告数 修复后通告数 修复后剩余
pnpm audit --prod 4(image-size ×2 high + xmldom ×2 moderate) 2 image-size ×2(#1663 已登记例外)
pnpm audit(全量) 8(image-size ×2 + xmldom ×2 + fast-uri ×4 high) 2 image-size ×2(同上)

metadata 计数:prod moderate 2 + high 2moderate 0 + high 2;全量 moderate 2 + high 6moderate 0 + high 2。依赖总数未变(prod 1095 / 全量 1644)。

修复后两个范围的 GHSA 集合均为 {GHSA-w3rx-r6r6-pgpr, GHSA-5p2g-fcmc-qvqq}——GHSA-6gmq-8vp8-gcm6 与 4 枚 fast-uri GHSA 都已消失

注:pnpm audit 自身仍 exit 1(因为还剩 2 条 image-size),这是 CI 用 || true 落盘、以 verifier 退出码为唯一判定的既有设计(见 workflow 注释)。

验证:lock 里只剩 patched 版本

$ grep -n "xmldom" app/pnpm-lock.yaml
24:  '@xmldom/xmldom@0.8': '>=0.8.15 <0.9'
25:  '@xmldom/xmldom@0.9': '>=0.9.12 <0.10'
3255:  '@xmldom/xmldom@0.8.15':
3259:  '@xmldom/xmldom@0.9.12':
9029:      '@xmldom/xmldom': 0.8.15        # @expo/plist@0.7.0
11067:  '@xmldom/xmldom@0.8.15': {}
11069:  '@xmldom/xmldom@0.9.12': {}
13761:      '@xmldom/xmldom': 0.8.15       # mammoth@1.12.0
14816:      '@xmldom/xmldom': 0.9.12       # plist@3.1.1

$ grep -n "fast-uri" app/pnpm-lock.yaml
9:  fast-uri: '>=3.1.6 <4'
4579:  fast-uri@3.1.7:
11126:      fast-uri: 3.1.7                # ajv@8.20.0
12699:  fast-uri@3.1.7: {}

0.8.13 / 0.9.10 / fast-uri@3.1.5 在 lockfile 中已零出现(grep 全库仅剩一处无关命中 minimatch: 3.1.5)。

供应链一致性:三个新版本的 lock integrityregistry.npmjs.orgdist.integrity 逐字节一致(0.8.15 / 0.9.12 / 3.1.7 三处均 MATCH=True)。

验证:image-size 例外未被触碰

scripts/verify/vulnerability-exceptions.json 不在本 PR 的写集里git status --porcelain 只有 app/pnpm-lock.yamlapp/pnpm-workspace.yaml)。文件内容仍是两条 image-size 登记,tracking #1663patched=<0.0.0(上游最新 2.0.2 即受影响,无修复版本):

entries: [('GHSA-w3rx-r6r6-pgpr', 'image-size', '#1663'),
          ('GHSA-5p2g-fcmc-qvqq', 'image-size', '#1663')]

且它们仍被放行:修复后 verifier 对两个 scope 都输出 ::warning::… found registered no-fix advisories (tracked): GHSA-5p2g-fcmc-qvqq GHSA-w3rx-r6r6-pgprexit 0——这正是设计行为(不可修项降级为 tracked warning),本 PR 一行都没动。

门禁表

HEAD = 36554318fix/deps-xmldom-advisory),base = 77c75e44origin/master,push 前 git fetch origin && git rebase origin/master 已确认无新增 commit)。

门禁 / 检查 命令 修复前 修复后
pnpm-audit prod gate bash scripts/verify/verify-vulnerability-gates.sh pnpm-audit prod < audit.json exit 1found vulnerable package(s) not in exception registry: GHSA-6gmq-8vp8-gcm6 exit 0warning … registered no-fix advisories (tracked): GHSA-5p2g-fcmc-qvqq GHSA-w3rx-r6r6-pgpr
pnpm-audit full gate 同上,pnpm-audit full exit 1GHSA-5jgf-p345-68v8 GHSA-6gmq-8vp8-gcm6 GHSA-f65p-4m7j-42xc GHSA-fph4-wmhf-6fwf GHSA-jqff-g426-hqxp exit 0(同上 tracked warning)
gate 自检 bash scripts/verify/tests/verify-vulnerability-gates.Tests.sh Passed: 23 | Failed: 0exit 0
lockfile 一致性 cd app && pnpm install --frozen-lockfile --lockfile-only exit 0Scope: all 6 workspace projects / Done in 849ms
commit message gate bash scripts/verify/verify-commit-messages.sh origin/master HEAD commit message check ok (1 commits)
依赖解析无新冲突 cd app && pnpm install --lockfile-only exit 0,无 ERR;peer 警告为既有(见下)

peer 警告说明(既有,非本 PR 引入)pnpm install --lockfile-only 输出 3 条 unmet peer —— eslint-plugin-import@2.32.0eslint ^2…^9(仓库用 10.4.0)、@lobehub/ui>@emoji-mart/react@1.1.1react ^16.8||^17||^18(仓库用 19.2.8)、expo>@expo/log-box@56.0.14@expo/dom-webview@^56.0.6(锁 56.0.5)。三条都与 xmldom/fast-uri 无关,且:

  • 在 origin/master 的独立临时 worktree 上删掉 lockfile 从零重解,同样输出同类 3 条 peer 警告(eslint-plugin-import / @emoji-mart/react / expo 链),证明是既有状态;
  • @expo/dom-webview@56.0.5 + @expo/log-box@56.0.14 这对组合在 master 的 lockfile 里就已如此(第 1472 / 1506 行),本 PR 的 lock diff 零行涉及 expo。
  • @xmldom/xmldomfast-uri 都是零依赖包,结构上不可能引入 unmet peer。

临时 worktree 已 git worktree remove --force 删除;主 worktree 用 --lockfile-only,未生成任何 node_modulesfind .worktrees/deps-xmldom -name node_modules 为空,目录 32M = 纯源码 checkout),磁盘无多余副本。

未验证项(交 CI)

  • 未跑 vitest / coverage baseline / tsc 全量 / eslint / stylelint / docker build / Tauri 原生构建:本切片不改任何源码,只动依赖清单与 lockfile,按切片约定由 CI 的前端 job 覆盖。
  • 未做真实 pnpm install(非 --lockfile-only)落地安装 + 运行时冒烟:需要拉全量 tarball(本机 / 已用 79%),且 --frozen-lockfile --lockfile-only 已证明 lock↔manifest 一致;CI 的 pnpm install --frozen-lockfile 会真实执行这一步。
  • 未验证 @expo/plist / mammoth / plist 在 0.8.15 / 0.9.12 下的运行时行为(如 .docx/.plist 解析实际输出)。静态面已核:三个上游声明范围都覆盖新版本,engines(node >=10.0.0 / >=14.6)与旧版一致,两个新版本都是零依赖,advisory 只改 requireWellFormed 序列化路径的 EntityReference 校验;但本 PR 没有跑 mobile-rn 的 expo 构建或 mammoth 转换。
  • 未验证 govulncheck / cargo audit(与本 PR 无关,CI 覆盖;fix(frontend): desktop 消息右键菜单死条目收口——pin/unpin/recall 接上已有 mutation、forward/regenerate 按 handler fail-closed、派发器 7 处静默 break 改为一次可感知反馈 (#2154) #2238 上 cargo audit 已 SUCCESS)。

兼容性风险评估

低风险:

  • 两个 xmldom 目标版本都是上游同 minor 的 patch,且是当前的 lts(0.8.15)与 latest(0.9.12)dist-tag;
  • 都落在消费者自己声明的 semver 范围内(^0.8.8 / ^0.8.6 / ^0.9.10),不是强制跨 minor 提升;
  • fast-uri 3.1.5 → 3.1.7 同为 3.x patch,且只在 dev/lint 链(stylelint>table>ajv),不随产物发布;
  • lock diff 30 行、3 个包,无连带漂移。

若 CI 上出现 @expo/plist / mammoth 与 0.8.15 的明确不兼容信号,回退方案是把 "@xmldom/xmldom@0.8" 从 overrides 里摘掉、只保留 0.9 域(prod 门禁的 xmldom 命中会回来一条),或等上游 @expo/plist / mammoth 自己 bump——不建议改走例外登记(政策禁止:有修复版本)。

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 15a5fe43-4a30-4de4-ac21-5b95904c44f0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…6,prod audit 4→2 / 全量 8→2 (#2154)

- GHSA-6gmq-8vp8-gcm6 / CVE-2026-83610(medium,xmldom requireWellFormed 序列化时无效 EntityReference.nodeName 造成 XML 片段注入,published 2026-09-02T15:18Z):影响 >=0.7.0 <=0.8.14 与 >=0.9.0 <=0.9.11,lockfile 实锁 0.8.13(@expo/plist@0.7.0、mammoth@1.12.0)与 0.9.10(plist@3.1.1),两域均低于 patched → 新增两条**版本域限定** override `"@xmldom/xmldom@0.8": ">=0.8.15 <0.9"` 与 `"@xmldom/xmldom@0.9": ">=0.9.12 <0.10"`,各自重解到 0.8.15 / 0.9.12。不跨 minor 强推:0.8.15 满足上游声明 ^0.8.8(@expo/plist)与 ^0.8.6(mammoth),0.9.12 满足 ^0.9.10(plist),三者 engines(node>=10.0.0 / >=14.6)与零依赖面均未变
- 同批扫出 GHSA-5jgf-p345-68v8 / GHSA-f65p-4m7j-42xc / GHSA-fph4-wmhf-6fwf / GHSA-jqff-g426-hqxp(**high** ×4,fast-uri host 混淆 / IPv6 与百分号解码 SSRF,published 2026-09-02T15:41-15:44Z):既有 override `">=3.1.5 <4"` 钉在已过期下限,lockfile 实锁 3.1.5 中 4 枚 high(路径 .>stylelint>table>ajv>fast-uri)→ 提到 `">=3.1.6 <4"`,重解到 3.1.7。CI 的 prod 步先 exit 1,全量步从未跑到,所以这 4 条在 #2238 的红灯里完全不可见——只修 xmldom 会把红从 prod 步搬到 full 步
- overrides SSOT 在 app/pnpm-workspace.yaml(#1713,不在任何 package.json —— app/package.json 的 `pnpm` 字段只有 hoistedDependencies,先核过 #2224 的实际落点);按 d7c326c 同式修改,写集只有这两个文件,未动任何 package.json;lockfile 仅 30 行变动,只动 3 个包(0.8.13→0.8.15 / 0.9.10→0.9.12 / 3.1.5→3.1.7),无其他包版本漂移
- 实测(直连 registry.npmjs.org,本机默认华为云镜像不支持 audit POST):`pnpm audit --prod` 通告数 4 → 2,全量 `pnpm audit` 8 → 2;剩余 2 条即 #1663 已登记的 image-size 不可修项(GHSA-w3rx-r6r6-pgpr / GHSA-5p2g-fcmc-qvqq,patched=<0.0.0),scripts/verify/vulnerability-exceptions.json **未触碰**(按政策有修复版本者禁止登记,故本次全部走 override 升级)
- 门禁判定复现:verify-vulnerability-gates.sh pnpm-audit prod/full 修复前均 exit 1(prod 点名 GHSA-6gmq-8vp8-gcm6;full 点名 GHSA-6gmq-8vp8-gcm6 + 4 枚 fast-uri GHSA),修复后均 exit 0 且降级为 warning(tracked);自检 verify-vulnerability-gates.Tests.sh 23 passed / 0 failed
- `pnpm install --frozen-lockfile --lockfile-only` 一致性通过;三个新版本 lock 里的 integrity 与 registry.npmjs.org `dist.integrity` 逐字节一致;--lockfile-only 未生成 node_modules,无多余副本
- 未跑(本切片不改任何源码,交 CI 前端 job):vitest / coverage / tsc / docker

Co-authored-by: DeliciousBuding <DeliciousBuding@users.noreply.github.com>

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
@DeliciousBuding
DeliciousBuding force-pushed the fix/deps-xmldom-advisory branch from 3655431 to 4d65dc0 Compare September 2, 2026 18:06
@DeliciousBuding

Copy link
Copy Markdown
Collaborator Author

记录更新:rebase 后的 HEAD 与 CI 实测(补正文里的 HEAD 字段)

正文写的是 push 当时的 HEAD 36554318(base 77c75e44)。开 PR 后 master 前进了一个 commit(530b4d99 = #2239,只动 edge-server/internal/events/**零个 app/ 文件),而本仓分支保护是 required_status_checks.strict: true(必须与 master 同步才可合),PR 因此变成 BEHIND。已按 lane 约定再走一轮 git fetch origin && git rebase origin/master + --force-with-lease

当前 HEAD 4d65dc05
当前 base(merge-base) 530b4d99origin/master
rebase 前后 app/ 内容 逐字节一致git diff --stat 36554318..4d65dc05 -- app/ 为空)→ 正文里所有本地实测结论原样成立,无需重跑
写集 仍只有 app/pnpm-lock.yaml + app/pnpm-workspace.yaml

CI(checks workflow,run 33665186096,HEAD 4d65dc05):completed / success,41 个 check 0 failed / 0 error / 0 timed_out。其中:

  • Vuln scan (pnpm audit prod+full) = success ← 本切片的目标门禁,在 CI 上真实转绿(rebase 前的 run 33664262014 @ 36554318 也已 success)
  • Vuln scan (cargo audit) = success,Vuln scan (govulncheck) = skipped(未碰 Go)
  • validate / Frontend (web) / frontend-desktop (1)(2) / 5 个 coverage baseline / Design CSS syntax / Web stubbed-hub E2E / Frontend (mobile light) / Native Windows frontend (web, desktop) / go-edge / go-hub / windows-go = 全部 success
  • frontend-required / ui-required / backend-required / windows-frontend 聚合 = success

mergeStateStatus: CLEANmergeable: MERGEABLE未合并——按 lane 约定留给主机侧排程。

正文"未验证项"里的第 1、2 条(vitest/coverage/tsc/docker、真实 pnpm install --frozen-lockfile)现已被这轮 CI 覆盖并通过,可以划掉;仍未验证的只剩第 3 条:@expo/plist / mammoth / plist 在 0.8.15 / 0.9.12 下的运行时行为(本 PR 没跑 expo 构建或 mammoth 转换;Frontend (mobile) 与 mobile E2E 在本 PR 上是 skipped / light 档,不构成对该路径的运行时覆盖)。

@DeliciousBuding
DeliciousBuding merged commit 94aef98 into master Sep 2, 2026
41 checks passed
@DeliciousBuding
DeliciousBuding deleted the fix/deps-xmldom-advisory branch September 2, 2026 18:16
DeliciousBuding added a commit that referenced this pull request Sep 2, 2026
…2154)(#2237)

## 这条 lane 是什么

实施 lane E(**纯文档**)= #2154 Feynman 文档探索批的切片 C「对外文档诚实批」。
唯一非 `.md` 改动是第 6 条的 `scripts/verify/quality-debt-baseline.json`(任务书明示的唯一例外)。
**未改任何 `.go` / `.ts` / `.tsx` / `.yaml` 契约文件。**

分支已 `git fetch origin && git rebase origin/master`;上游新增的 `dc7df53d`(纯 Go:edge lifecycle safego 去重 + panic observer)与本分支 9 个文件**零重叠,rebase 无冲突**,因此没有触发「两边都保留 + 行数预算重新核算」的解冲突流程。

合并时 HEAD = `b017f0f4`,base = `94aef98a`(origin/master);lane push 时的 HEAD 为 `8f2ade25`(base `dc7df53d`)。7 个 commit,每个 commit 一次门禁。

**订正(主机侧合并前补记,原文此处写的是「故意不再 rebase」,与最终实况不符)**:push 后 `origin/master` 由 `dc7df53d` 连续前进到 `94aef98a`(#2234/#2235/#2236/#2239/#2240),本仓 `required_status_checks.strict: true` 使 PR 转 `BEHIND`,故本分支**已按纪律 rebase 到 `94aef98a`**,HEAD 由 `8f2ade25` 变为 `b017f0f4`。rebase 前后**写集逐字节零变化**:`git diff --stat 8f2ade2 b017f0f -- AGENTS.md CHANGELOG.md CONTRIBUTING.md README.md README_EN.md SECURITY.md docs/ scripts/verify/quality-debt-baseline.json` **输出为空**;两者全量 diff 只含 master 自身的 `app/pnpm-lock.yaml`+`app/pnpm-workspace.yaml`(#2240)与 `edge-server/internal/events/**`(#2234/#2239),与本 lane 9 个文件零重叠;7 个 commit 逐条 subject 一一对应。因此正文里所有本地实测结论原样成立。rebase 后按原文要求**重跑并复现**:`scripts/verify/verify-doc-ssot.py` → `doc SSOT ok`(verifier-map 66 script paths / 58 CI files、AGENTS.md 96 paths)、`wc -l AGENTS.md` → **284**(≤300 预算)、`git diff --check` 干净;CI run `33666303378` 在 `b017f0f4` 上**全绿**(22 successful / 17 skipped / 0 failing / 0 pending,含 go-hub、go-edge、windows-go、backend-required、frontend-required、ui-required、validate、CodeRabbit),`mergeStateStatus: CLEAN`。

---


## 逐条三段式证据(文档原话 → 代码/CI 事实 → 改后原话)

> 7 条逐字引证与改后原话全文见本 PR 正文(GitHub 侧永久保留),squash commit 只收判定结论以免历史膨胀。

| # | 条目 | 复核判定 |
|---|---|---|
| 1 | `CONTRIBUTING.md:36` 称 `make test` 跑前端 vitest | ✅ 成立(四处口径改指明 Makefile 目标名) |
| 2 | `CHANGELOG.md:5-7`「暂无未发布变更」 | ✅ 成立(改为声明 SSOT 与生成方式) |
| 3 | `SECURITY.md:23` 安全门禁工具名指错 | ✅ 成立,且原文另有一处更严重的不诚实 |
| 4 | `README.md` / `README_EN.md` | ⚠️ 部分成立(禁用词那条报告说法不准确,但假声明本身成立) |
| 5 | `verifier-map.md` + `docs/architecture/README.md` 双向差集 | ✅ 成立(补 12 行 + 修 2 处不诚实 + 补 1 行索引) |
| 6 | `quality-debt-baseline.json` 的 `issue` 归属 | ⚠️ 报告部分成立:6 条里只敢改 2 条 |
| 7 | `AGENTS.md` §12 加「CHANGELOG owner」 | ✅ 净减 2 行守住行数预算(284≤300) |

## 复核后判定「不成立 / 已过期」而跳过或改判的条目

| # | 报告说法 | 实测 | 处置 |
|---|---|---|---|
| 4b | 「路线图/roadmap 被门禁主动禁止却仍存在」 | `verify-doc-ssot.py` 只禁**根级文件 `ROADMAP.md`**(`:100`)、**路径 `docs/roadmap`**(`:109`)、**正则 `ROADMAP\.md`**(`:200`);没有任何规则禁「路线图」这个词,门禁本来就跑得绿 | 报告说法**不成立**,已在 PR 正文写明。但底下的**假声明成立**(`docs/` 里确实没有路线图),故仍按事实改,改法换成门禁理由本身陈述的真事实(roadmap 在 GitHub issues) |
| 6 | web lint 债的真实归属是 1575 | `gh issue view 1581` 正文原话「**#1575 只负责 Desktop ESLint,不覆盖 Web**」 | 报告此点**不成立**,改判为 **#1581** |
| 6 | i18n callsite 债的真实归属是 1612 | `#1612` 是 PR「docs(progress): MASTER 同步」,`files` 只有 `docs/progress/MASTER.md`;而仓内三处(checks.yml:2126 / verifier-map:27 / CHANGELOG:44)一致引用 #1612 | 报告此点**不成立**(且暴露更大问题:全仓的 #1612 引用可疑)。**不改**,登记 #2154 待裁决 |
| 6 | 5/6 条都该改 | 只有 2 条能拿到「该 issue 明确以这笔债为标的」的正文证据 | **只改 2 条**,另 3 条按任务书要求不猜号 |
| 1 | 四处口径自相矛盾 | `docs/developer-quickstart.md:122-123` 其实是**正确**的那一处 | quickstart **未改**,只改 CONTRIBUTING(错的那处)+ AGENTS(歧义的那处) |
| 2 | 若不成立才补真实条目 | SSOT 判断**成立** | 按要求**没有**手写 Unreleased 列表 |

---

## 门禁表(原跑于 HEAD `8f2ade25` / base `dc7df53d`;rebase 到 `b017f0f4` / base `94aef98a` 后写集零变化,doc 门禁已重跑复现、CI 已全绿重证)

| 门禁 | 命令 | 结果 |
|---|---|---|
| 文档 SSOT(主门禁) | `python3 scripts/verify/verify-doc-ssot.py` | ✅ `doc SSOT ok`;verifier-map **66** 脚本路径 / **58** CI 文件全部存在;AGENTS.md **96** 个反引号路径全部存在;`DOC-README-PARITY` PASS |
| CI 结构合同 | `python3 scripts/verify/verify-ci-gates.py` | ✅ `ci gate policy ok` |
| 质量债棘轮(动了 baseline) | `python3 scripts/verify/verify-quality-debt-ratchet.py` | ✅ **9 pass / 0 fail** |
| 质量债棘轮负向自测 | `python3 scripts/verify/tests/verify-quality-debt-ratchet.Tests.py` | ✅ **15 tests OK** |
| skill 白名单 | `python3 scripts/verify/verify-project-skills.py` | ✅ rc=0(`skills root absent (.agents removed) — whitelist gate trivially passes`) |
| conventions 方法 SSOT | `python3 scripts/verify/verify-conventions.py` | ✅ `Passed: 1 \| Failed: 0` |
| doc-ssot 负向自测 | `python3 scripts/verify/tests/verify-doc-entrypoints.Tests.py` | ✅ `Ran 1 test … OK`(证明主门禁没被我的改动弄钝) |
| 空白/冲突标记 | `git diff --check origin/master..HEAD` | ✅ clean |
| AGENTS.md 行数 | `wc -l AGENTS.md` | ✅ **284** ≤ 300 |
| **GitHub Actions(本 PR 真实 run)** | run [33658111957](https://github.com/TokenDanceLab/AgentHub/actions/runs/33658111957) | ✅ **22 SUCCESS / 17 SKIPPED / 0 非绿**;7 个 required 聚合全绿:`validate` `go-edge` `go-hub` `windows-go` `windows-frontend` `backend-required` `frontend-required` |
| 其他行数预算 | `wc -l` | ✅ CHANGELOG.md 80/90、CONTRIBUTING.md 58/90、verifier-map.md 87/120、docs/architecture/README.md 28/40 |

**按纪律未跑**:`go test`、`go build`、vitest、coverage、全量 golangci-lint、docker、`make *`(4 核机 + 并行 lane)。
**golangci-lint 幽灵**:本 lane 未跑 golangci-lint,未遇到指向已删除 worktree 路径的缓存幽灵 issue。

---

## 未验证项(诚实声明)

1. ~~**没有跑任何 CI**~~ → **已验证(本条从「未验证」升级为「已验证」,PR 开出后回写)**:GitHub Actions run [33658111957](https://github.com/TokenDanceLab/AgentHub/actions/runs/33658111957) 结果 **22 SUCCESS / 17 SKIPPED / 0 非绿**,7 个 required 聚合(`validate`/`go-edge`/`go-hub`/`windows-go`/`windows-frontend`/`backend-required`/`frontend-required`)全部 SUCCESS。`validate` 是承载 `verify-doc-ssot.py` + `verify-ci-gates.py` + `verify-quality-debt-ratchet.py` + `verify-conventions.py` + `verify-project-skills.py` 的 job,它 SUCCESS ⇒ 本 PR 全部 9 个文件的改动在 CI 上被同一套门禁判绿,不只是我本地判绿。`go-*` 侧也跑了(因为 `scripts/verify/**` 在 `changes` job 的 `go` 路径过滤里,baseline JSON 改动触发了 Go lane),`go-hub` 的 golangci-lint + 覆盖率门禁 SUCCESS ⇒ 未出现缓存幽灵。
   **顺带活体印证第 3 条的改法**:`Vuln scan (pnpm audit prod+full)`、`Vuln scan (govulncheck)`、`Vuln scan (cargo audit)`、全部 `frontend-*`、`Visual QA *`、`Design CSS syntax` 在本 PR 均为 **SKIPPED** —— 正是我写进 `SECURITY.md` 的「三个 vuln-scan job 都经 `changes` job 路径过滤触发,不是每次 push 全量扫描」的实时证据(本 PR 不含 `app/**` 改动)。
2. **markdown 渲染只在本地按 CommonMark 规则推断**,没有在 GitHub 上肉眼看过渲染结果。两处需要 review 时确认:`docs/architecture/README.md` 新增行、`verifier-map.md` 宏观四行并入主表后是否真的渲染成表格。
3. **`gh issue view` 读到的是 issue/PR 的当前标题与正文**,不能证明「该 issue 在软门禁被引入的那一刻就是 owner」。desktop→#1575 / web→#1581 的判定依据是两个 issue 正文**逐字点名了对应的 baseline 条目与 step 名**,这是我能拿到的最强证据,但仍属文档考古而非当事人确认。
4. **tag `v0.6.1` 与 master 历史脱钩**这件事我只做了 `git merge-base --is-ancestor` / `git merge-base` 两个命令的验证,**没有**去查 release.yml 的历史 run 是否真的因此失败过,也没有验证 git-cliff 在无前序 tag 时的实际输出长度。它超出纯文档 lane 范围,只登记不动手。
5. **未改任何产品代码**,因此第 3/5 条里所有关于「门禁 fail-closed」的描述都是**读脚本源码 + workflow YAML 得出**,不是我实跑这些门禁观察到的红/绿。唯一实跑过的是 `verify-doc-ssot.py` / `verify-ci-gates.py` / `verify-quality-debt-ratchet.py` / `verify-conventions.py` / `verify-project-skills.py` 及两个负向自测。

---

## 需要人工裁决 / 后续 issue(已同步登记 #2154)

1. quality-debt baseline 3 条 `issue` 归属待确认:`frontend-mobile: Lint (mobile rules)`、`validate: Verify i18n callsites ratchet`、`vuln-scan-rust: cargo clippy (advisory)`(现值均为可证伪的 1573)。
2. 全仓 `#1612` 引用可疑(checks.yml:2126 / verifier-map:27 / CHANGELOG:44 三处),需定位 i18n callsite ratchet 的真实接线 issue/PR。
3. tag `v0.6.1` 不在 master 祖先链上 ⇒ `release.yml:42` tag-guard 与 git-cliff `--latest` 的前序 tag 解析都受影响,下一次打 tag 前需裁决(重打 tag / 调整 cliff 调用 / 接受全量分组)。
4. `cliff.toml` 的 `^security` commit parser 是**死分支**(提交类型白名单不含 `security`):要么给白名单加 `security`,要么删掉这个 parser 并改用 label/其它机制披露安全修复。本 PR 只把 SECURITY.md 的承诺改成与现状一致,没动 cliff.toml(属产品配置,非纯文档 lane 范围)。
5. `scripts/verify/tests/merge-coverprofiles.Tests.py` 与 `scripts/verify/tests/verify-real-e2e-artifacts.Tests.py` **存在于磁盘但没有任何 workflow 调用**(`grep .github/workflows/` 零命中)⇒ 两个负向自测是死的。我在 verifier-map 里因此**没有**把它们写成「负向自测」(只登记了脚本本体),避免制造新的假绿声明;是否接线请裁决。
6. #1575 / #1581 均已 CLOSED,但对应的两条 `continue-on-error` 软门禁**仍在 checks.yml 里活着**(`verify-quality-debt-ratchet.py` 的 zombie 检查 PASS 即证明这点),且两条的 `review_by` 都是 `2026-10-01`。即「偿还 ESLint 债并移除软门禁」的 issue 关了、软门禁没移除。属治理不一致,非本 lane 范围。

## 流程事故记录(不影响代码,但影响交付物可信度,故如实记)

开出本 PR 后、往 #2154 贴登记评论时,**另一条并行 lane(Lane A,#2154 评论 `5513247382`)在同一分钟覆写了 `/tmp/pr-body.md`** —— 两条 lane 用了同一个临时文件名。后果与处置:

- **PR #2237 正文未受影响**:`gh pr create` 在覆写发生前已执行完毕。事后用 `gh pr view 2237 --json body` 回读实测 21035 字符,首句「## 这条 lane 是什么」、末句「Closes 无(本 PR 是 #2154 的切片 C…)」,且 `grep -c "toast.actionUnavailable"`(对方正文特征串)= 0 ⇒ 内容是我的、完整的。
- **#2154 的首版评论被污染**:拼评论时读到的是对方正文,等于把我的抬头 + Lane A 的正文贴了上去。已从 PR 正文回读重建、用 `gh api -X PATCH .../issues/comments/5513238457` **原地编辑**修正(不新贴第二条制造噪声),并复核修正后正文里对方 lane 的 5 个特征串(`§6.4`/`§6.5`/`desktop forward`/`regenerate 是否另开 lane`/`i18n 资源面 lane`)全部 0 命中、我的 4 个结构节各 1 次。
- **教训(供主机侧收进并行 lane 纪律)**:多 lane 并行时临时文件必须用 lane 唯一路径。本 lane 后续已改用 `/tmp/laneE-doc-honesty-2237/`。这与 `AGENTS.md`「一个 worktree 同时只放一个写 agent」是同一类风险,但发生在 worktree 之外的共享 `/tmp`,现有规则没覆盖到。

Closes 无(本 PR 是 #2154 的切片 C,#2154 由主机侧统一收口,不在此自动关单)。
DeliciousBuding added a commit that referenced this pull request Sep 2, 2026
…rward/regenerate 按 handler fail-closed、派发器 7 处静默 break 改为一次可感知反馈 (#2154) (#2238)

## 一句话

Desktop 右键菜单里 pin/unpin/recall 是**点了没反应**(平台层有 mutation 但没转发进 workbench deps),forward/regenerate 是**渲染了但根本没有 port**;派发器 7 处 `if (!handler) break;` 让这些点击零反馈消失。本 PR:能接的接上(3 个),接不上的按 handler 存在性 fail-closed 不渲染(2 个),并把静默 `break` 全部换成一次可感知反馈。

## 1. 锚点核实结论(主机侧 4 条,逐条复核)

| # | 主机侧结论 | 复核结果 |
|---|---|---|
| 1 | 门禁是 `hubMessageActions: Boolean(deps.sessionId)`,判据是"有没有 sessionId"而非"handler 存不存在" | ✅ 成立,且比描述更严重:`AgentHubWorkbenchHelpers.ts:157` 把 `props.activeConversationId` **直接当 sessionId** 传下去(注释:`#1383 REST message actions: activeConversationId doubles as the session id`)。Desktop 在 Hub IM 会话下 `activeConversationId` = hub session id ⇒ 门禁恒真 ⇒ pin/unpin/recall 照渲染。改前 516-517 行注释宣称"Desktop/demo shells get an honest, shorter menu (#1818)",与事实相反(Desktop 有 session id) |
| 2 | mappers 有"5 处以上" `if (!handler) break;` | ✅ 成立,精确是 **7 处**同形态静默分支(改前行号):624 regenerate(变量名 `regenerateHandler`)/ 654 approval / 667 pin / 680 unpin / 693 forward / 706 recall / 719 react。全部零反馈、零日志 |
| 3 | desktop 平台层 mutation 确实存在,路径应含 `/platform/` | ✅ 路径修正成立:`app/desktop/src/platform/useDesktopWorkbenchModel.ts` 的 `DesktopChatActions` 有 `sendMessage/recallMessage/editMessage/pinMessage/unpinMessage/markRead`(479-487 行接 `useHubRecallMessage/useHubPinMessage/useHubUnpinMessage`,hook 在 `app/desktop/src/api/sessionQueries.ts`)。**但只有这 3 个能用**:desktop api 层没有 forward hook(shared `hubClient.forwardMessage` 存在,desktop 未包)、desktop 全仓 grep 不到 regenerate、shared hubClient 也没有 addReaction |
| 4 | App.tsx grep 不到 `onPinMessage\|onUnpinMessage\|onRecallMessage` | ✅ 成立,**具体缺 3 个转发**:`onPinMessage` / `onUnpinMessage` / `onRecallMessage`。改前 desktop App.tsx 只转发 `onEditMessage`(674-682)与 `onApprovalDecision`。`onForwardMessage` / `onRegenerate` / `onAddMessageReaction` 同样没有,但属"平台层没有对应 mutation",不是漏转发 |

前提全部成立 ⇒ 按"优先接真 mutation + 其余 fail-closed + 派发器不再静默"执行,没有另造修法。

## 2. 改了什么

**`app/workbench/src/workbenchTranscriptChromeActionMappers.ts`**
- 菜单选项 `hubMessageActions?: boolean` → `capabilities?: TranscriptMenuActionCapabilities`(`pin/unpin/recall/forward/regenerate` 五个独立布尔,缺省全 false = fail-closed)。pin 与 unpin **分开**:条目按 `block.pinned` 二选一,只接了一个方向的 shell 不再渲染死的那一半。forward 仍需 `conversations`(选择器是唯一真实转发路径,#1385),recall 仍限 `author.role === 'human'`,regenerate 仍限 agent 文本块。
- 新增 `UNAVAILABLE_ACTION_TOAST_KEY = 'toast.actionUnavailable'` + `announceUnavailableAction()`,替掉全部 7 处静默 `break`:恰好一次 toast、绝不播报成功文案、绝不产生 softHide/pulse/composer 等假副作用。键未落地时回落到 effect 自带的 `failureMessage`(已本地化),因此既不会静默也不会露出裸键。

**`app/workbench/src/workbenchTranscriptChromeHelpers.ts`**
- `contextMenuGroups` 由 handler 存在性算 capabilities:`pin/unpin/recall = Boolean(sessionId) && deps.onXxx !== undefined`(planner 没有 sessionId 造不出 effect,#1818),`forward = deps.onForwardMessage !== undefined`,`regenerate = deps.onRegenerate !== undefined`。
- 修正 `sessionId` 的 doc 注释(原文断言"Absent on Desktop/demo shells",是假的)。

**`app/desktop/src/App.tsx`**
- 转发 `onPinMessage/onUnpinMessage/onRecallMessage` 到 `workbench.chatActions.{pinMessage,unpinMessage,recallMessage}`,沿用既有 `onEditMessage` 的 `hub-message-` 前缀剥离约定(契约见 `AgentHubWorkbenchTypes.ts:167`:handler 收到的是 raw block id,由 parent 剥前缀);`chatActions` 缺失(demo/Hub 未就绪)时传 `undefined`。
- forward/regenerate/reaction **不接**(desktop 无对应 mutation,且 forward hook 要改 `app/desktop/src/api/sessionQueries.ts`,不在写集)⇒ 靠 fail-closed 让条目消失。

**测试**:`workbenchTranscriptChromeActionMappers.test.ts`、`workbenchTranscriptChromeHelpers.test.ts`、新增 `app/desktop/src/__tests__/App.messageActions.test.tsx`;另有 2 个写集外夹具修正(见 §6.2)。

## 3. 不变量 → 测试映射(全部绿)

| 不变量 | 测试 | 断言方式 |
|---|---|---|
| handler 缺失 ⇒ 菜单不出现 pin/unpin/recall/forward/regenerate | mappers `renders handler-backed menu entries only when the capability is declared (#2154)`;helpers `omits handler-backed menu entries when no handler is wired, even with a session id (#2154)` | 有 sessionId、无 handler ⇒ 逐条 `not.toContain`;另覆盖 pin/unpin 半开、recall 作者门、forward 无会话列表 |
| handler 存在 ⇒ 点击真的派发到该 handler | helpers `renders each wired action and dispatches the click to its handler (#2154)`;mappers `dispatches a declared menu entry to its action string (#2154)`;desktop `forwards the Hub pin/unpin/recall ports with the block-id prefix stripped` | 菜单项 `onClick()` → spy handler 被调用(helpers 层断言 `onPinMessage('u1','sess-1')` 等 5 个 port;desktop 层断言 `chatActions.pinMessage('m1','sess-1')`,即前缀已剥) |
| 任何"无 handler"分支必须产生一次可感知反馈,不允许静默 break | mappers `announces every unwired action exactly once instead of dropping it silently (#2154)`(7 个 effect 逐个)+ `announces Hub REST side effects when handlers are not wired` + `announces approval effects when no decision handler is wired` + `prefers the dedicated unwired-action copy…` + `falls back to the effect failure copy when the dispatcher gets no translate function` | `toHaveBeenCalledTimes(1)`、不是成功文案、且 softHide/pulse/dispatchComposer 均未被调用 |
| desktop 接不上的 port 保持 undefined(菜单因此不渲染) | desktop `leaves the ports Desktop cannot back undefined so the menu hides them`、`withholds every message port when Hub chat actions are unavailable` | props 断言 `onForwardMessage/onRegenerate/onAddMessageReaction === undefined`;chatActions 缺失时 4 个全 undefined |

## 4. 红 → 绿证据

**红(实现改之前,tree = `bdbf810` + 红测试,提交为 `b7b2c785`)**

```
# pnpm --filter @agenthub/workbench exec vitest run \
#   src/workbenchTranscriptChromeActionMappers.test.ts src/workbenchTranscriptChromeHelpers.test.ts
 ❯ src/workbenchTranscriptChromeHelpers.test.ts (27 tests | 1 failed)
     × omits handler-backed menu entries when no handler is wired, even with a session id (#2154)
 ❯ src/workbenchTranscriptChromeActionMappers.test.ts (32 tests | 6 failed)
     × announces Hub REST side effects when handlers are not wired (#2154)
     × announces approval effects when no decision handler is wired (#1821, #2154)
     × renders handler-backed menu entries only when the capability is declared (#2154)
     × dispatches a declared menu entry to its action string (#2154)
     × announces every unwired action exactly once instead of dropping it silently (#2154)
     × prefers the dedicated unwired-action copy once the locale bundle resolves it (#2154)
 Test Files  2 failed (2)      Tests  7 failed | 52 passed (59)
```
典型红断言:`expected [ 'context.copy', …(8) ] to not include 'context.regenerate'`(有 sessionId 无 handler 时条目照样渲染);`pin: expected "vi.fn()" to be called 1 times, but got 0 times`(派发器静默)。

```
# pnpm --filter agenthub-desktop exec vitest run src/__tests__/App.messageActions.test.tsx
 × forwards the Hub pin/unpin/recall ports with the block-id prefix stripped
   AssertionError: onPinMessage must reach the workbench deps: expected undefined to be type of 'function'
 Test Files  1 failed (1)      Tests  1 failed | 2 passed (3)
```

**实现落地后又抓出 2 处"旧断言就是那条假事实"的连带红**(均在写集外,见 §6.2):

```
useWorkbenchTranscriptChrome.test.ts  × builds context menu groups shaped for agent and user blocks
  AssertionError: expected false to be true   (只给 sessionId、不给 handler 就断言 regenerate 条目存在)
__tests__/transcript.test.tsx         × opens the design card context menu and multi-select toolbar…
  expected […] to have a length of 6 but got 5 (无 forward port 的 shell 仍断言"转发"条目存在)
```

**绿(合并时 HEAD `321992ca`,base `c87178b3`;下表原跑于 `094ba8ae` / `d1dc97fd`,第二次 rebase 后已在 `321992ca` 上全部重跑复现,见 §7)**

```
workbench: 4 files / 92 tests passed  (mappers + helpers + useWorkbenchTranscriptChrome + __tests__/transcript)
desktop  : 1 file  / 3 tests passed   (App.messageActions)
web      : 1 file  / 10 tests passed  (src/App.test.tsx,回归面:web 也吃这套门禁)
```

## 5. 门禁表

两轮:rebase 前(HEAD `1a78581`,base `bdbf810`)与 rebase 后(HEAD `094ba8ae`,base `d1dc97fd`,已 push)。rebase 只带入 hub-server Go 改动(`git diff --stat bdbf810..d1dc97f` 全是 `hub-server/**`),FE 树 byte-identical;下表全部为 **rebase 后 HEAD `094ba8ae`** 实跑结果。**注:合并前本分支又 rebase 了一次(→ `321992ca`,base `c87178b3`),下表所有本机可跑项均已在新 HEAD 重跑并逐项复现,见 §7。**

| 门禁 | 命令 | 结果 | HEAD |
|---|---|---|---|
| workbench 单包单测 | `pnpm --filter @agenthub/workbench exec vitest run src/workbenchTranscriptChromeActionMappers.test.ts src/workbenchTranscriptChromeHelpers.test.ts src/useWorkbenchTranscriptChrome.test.ts src/__tests__/transcript.test.tsx` | 4 files / 92 passed | `094ba8ae` |
| desktop 单包单测 | `pnpm --filter agenthub-desktop exec vitest run src/__tests__/App.messageActions.test.tsx` | 1 file / 3 passed | `094ba8ae` |
| web 回归面单测 | `pnpm --filter agenthub-web exec vitest run src/App.test.tsx` | 1 file / 10 passed | `094ba8ae` |
| 文档 SSOT | `python3 scripts/verify/verify-doc-ssot.py` | `doc SSOT ok` | `094ba8ae` |
| 空白/冲突标记 | `git diff --check origin/master...HEAD` | clean | `094ba8ae` |
| i18n 硬编码棘轮 | `python3 scripts/verify/verify-i18n-callsites.py` | PASS(74 files / 597 行 ≤ baseline 78/608) | `094ba8ae` |
| 前端包边界 | `python3 scripts/verify/verify-frontend-package-boundary.py` | PASS | `094ba8ae` |
| workbench 类型 | `pnpm --filter @agenthub/workbench exec tsc --noEmit` | 0 error | `094ba8ae` |
| desktop 类型(app) | `pnpm --filter agenthub-desktop exec tsc --noEmit -p tsconfig.app.json` | 0 error | `094ba8ae` |
| desktop 类型(含测试) | `pnpm --filter agenthub-desktop exec tsc --noEmit -p tsconfig.json` | 0 error | `094ba8ae` |
| eslint(仅改动文件) | `pnpm exec eslint <8 个改动文件>` | 2 problems,**均 pre-existing**(见 §6.9) | `094ba8ae` |

按指令**未跑**:全量 vitest、coverage、`pnpm -r build`、全量 `tsc`(CI 是权威)。命令坑记录:desktop 包名是 `agenthub-desktop` 不是 `@agenthub/desktop`;`pnpm --filter X vitest run` 会 `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT`,必须 `exec vitest run`。

## 6. 证据等级 / 未验证项 / 可能错的地方

**证据等级**
- **L1(jsdom 单测,真实断言)**:菜单条目按 handler 存在性渲染、点击派发到 spy handler、7 个无 handler 分支各产生恰好一次 toast 且无假副作用、desktop App 把 3 个 port 转发到 `chatActions` 且剥掉 `hub-message-` 前缀。
- **L2(静态)**:workbench/desktop(含测试)tsc 0 error、eslint 无新增问题、4 个 verify 脚本 PASS。
- **L3(真实端到端)=无**:没起 Tauri/真实 Hub,没有真人点过菜单,没有真实 REST 往返证据。

**未验证 / 可能错**
1. **缺 i18n 键(只登记未改)**:`toast.actionUnavailable`(zh 建议"该操作在当前端未接入",en "This action is not wired in this client")。资源面 `app/shared/src/chatview/i18n/resources.ts` 不在写集 ⇒ 未加。当前行为:键缺失时回落到该 effect 的 `failureMessage`(如"置顶失败,请重试")——**不静默、不假成功,但"请重试"语义不准**(该端永远不会成功)。键一落地自动切换到专用文案,无需再改代码。
2. **写集外改了 2 个测试文件(各 1 处,已独立成 commit,可直接 drop)**:`94972dec` `useWorkbenchTranscriptChrome.test.ts`(夹具补 `onRegenerate/onRecallMessage` 两行,断言一字未改)、`79ecae30` `__tests__/transcript.test.tsx`(菜单条目数 6→5 + "转发"改断言不存在)。理由:这两处旧断言正是本 PR 要消灭的假事实("有 sessionId 就有 handler 条目"/"有 conversations 就有转发条目"),不改则 CI 必红。若主机侧判定越界,请 drop 这两个 commit 并由写集内 lane 重做。
3. **`AgentHubWorkbenchTypes.ts:165-170` 的 doc 注释现在是假的**(仍写"Desktop/demo shells omit them and pin/unpin/recall/react stay hidden (#1818)")。该文件不在写集 ⇒ 未改,登记为后续 1 行注释修正。
4. **desktop forward 未接**:真接需要 `app/desktop/src/api/sessionQueries.ts` 新增 `useHubForwardMessage`(shared `hubClient.forwardMessage` 已有)+ `DesktopChatActions` 扩字段 + App.tsx 转发。api 层不在写集 ⇒ 未做,改为不渲染条目。
5. **desktop regenerate 未接(且我故意没接)**:web 的做法是 App.tsx 直接 `createHubClient(...).regenerateAgentTask(messageId)`,desktop 技术上可照抄,但**我没有验证 desktop 的 Hub 任务语义下 regenerateAgentTask 是否正确**(desktop 另有 DesktopHubTaskBridge/agent task 路径),所以选择 fail-closed 不渲染而不是接一个语义未证的 port。
6. **coverage 未跑**:新增生产分支(`announceUnavailableAction` 的 t 有/无两路、`capabilities ?? {}` 默认值、5 个 capability 计算)都有对应用例,但包级阈值是否被拉低只有 CI 能判。
7. **只跑了 6 个测试文件**,不是全量。其余 FE 测试里是否还有别处断言"有 conversations 就渲染转发",我用 label grep(`context.forward|context.pinMessage|context.unpin|context.recall|context.regenerate|转发|置顶|撤回|重新生成`)扫过 app/{workbench,web,desktop,shared} 与 e2e,认为没有第二处,但 grep 不是权威。
8. **可见 UX 变化(需产品确认,不是 bug)**:desktop 的"重新生成""转发"条目消失;web 在 `chatActions` 缺失(Hub 未就绪/demo)时"转发/置顶/撤回"也消失。这是 fail-closed 的直接后果——消失的正是原先点了没反应的条目。
9. **eslint 2 个 pre-existing 问题(非本 PR 引入,已用 origin/master blob 探针证明)**:`workbenchTranscriptChromeActionMappers.ts:5` `'AppError' is defined but never used`(error)、`desktop/src/App.tsx:260` `useMemo missing dependency: 'tIm'`(warning)。探针做法:`git show origin/master:<file> >` 临时同目录文件再 eslint,结果与改动后一致;临时文件已删,`git status` 干净。未顺手修(与本 lane 无关,且可能有棘轮基线归属)。
10. **多 lane 环境说明**:本机在 `.worktrees/fe-ctx-menu` 单写者作业;worktree 的 `app/node_modules` 是软链到主 checkout、各包 `node_modules` 是 `cp -a` 复制(内部 `@agenthub/*` 为相对符号链接,已核实指向 worktree 自己的 workbench/shared,跨包测试确实跑的是本分支代码)。未动其他 worktree,未合并任何分支。

## 7. 剩余 blocker(需主机侧决策,非技术阻塞)

1. §6.2 两个写集外 test commit:接受 or drop 重做。
2. `toast.actionUnavailable` 键由谁落(i18n 资源面 lane)。
3. desktop forward / regenerate 是否另开 lane(§6.4、§6.5)。
4. §6.3 的 1 行注释修正归谁。

## 7. 主机侧合并前订正与复跑(第二次 rebase)

正文写的是 HEAD `094ba8ae` / base `d1dc97fd`。实况:master 又前进了两个 commit(`530b4d99`→`94aef98a`→`c87178b3`,即 #2239 / #2240 / #2237),本仓 `required_status_checks.strict: true` 使 PR 转 `BEHIND` 而**阻塞合并**,故已 rebase 到 `c87178b3`,HEAD 现为 **`321992ca`**。

**写集零变化**:`git diff --stat 094ba8a 321992c -- app/desktop app/workbench` 输出为**空**;两者全量 diff 只含 master 自身的 `app/pnpm-lock.yaml`+`app/pnpm-workspace.yaml`(#2240)、`edge-server/internal/events/**`(#2234/#2239)、`docs/**`+`AGENTS.md`+`scripts/verify/quality-debt-baseline.json`(#2237),与本 lane 8 个文件零重叠;6 个 commit subject 逐条一一对应。

**在新 HEAD `321992ca` 上重跑的门禁(逐项复现正文数值)**:

| 门禁 | 结果(`321992ca` 实跑) | 与正文声明 |
|---|---|---|
| workbench 4 个测试文件 | 4 files / **92 passed** | 一致 |
| desktop `App.messageActions` | 1 file / **3 passed** | 一致 |
| web 回归面 `src/App.test.tsx` | 1 file / **10 passed** | 一致 |
| `verify-doc-ssot.py` | `doc SSOT ok`(66 script paths / 58 CI files、96 AGENTS paths) | 一致 |
| `git diff --check origin/master...HEAD` | clean | 一致 |
| `verify-i18n-callsites.py` | PASS:current **74 files / 597** ≤ baseline 78/608 | 一致 |
| `verify-frontend-package-boundary.py` | PASS(376 shared + 421 workbench,0 违规) | 一致 |
| workbench `tsc --noEmit` | **0 error** | 一致 |
| desktop `tsc -p tsconfig.app.json` | **0 error** | 一致 |
| desktop `tsc -p tsconfig.json`(含测试) | **0 error** | 一致 |
| eslint(8 个改动文件) | **2 problems(1 error + 1 warning)** | 一致 |

**并把正文§6.9 那句「2 problems 均 pre-existing」独立验证过**(不只采信):master 版 `workbenchTranscriptChromeActionMappers.ts` 第 5 行同样有 `import { AppError } from '@shared/errors';`,且 `AppError` 在 master 版与本 PR 版的出现次数**都是 1**(即只有 import 行、无使用点)→ 该 error 非本 PR 引入;`desktop/src/App.tsx` 的 `tIm` useMemo 缺依赖在 master 的 **259 行**即已存在 → 同样 pre-existing。

**源码级复核修复本体**:`hubMessageActions` 在生产代码中**已彻底消失**(全仓只剩 `workbenchTranscriptChromeHelpers.test.ts:1095` 一条注释在记录旧行为);`workbenchTranscriptChromeHelpers.ts:515-531` 改为逐 action 的 `capabilities{pin,unpin,recall,forward,regenerate}`,其中 pin/unpin/recall = `Boolean(deps.sessionId) && deps.onXxx !== undefined`,forward/regenerate 只看 handler 存在性;`if (!handler) break` 形态在 mappers 中**归零**,`announceUnavailableAction` 定义于 `:556` 并恰好有 **7 个调用点**(`:625/:660/:693/:709/:725/:741/:757`),与报告所述 7 处静默分支一一对应;`desktop/src/App.tsx:693/703/713` 确实把 `onPinMessage/onUnpinMessage/onRecallMessage` 接进 deps。

**CI run(HEAD `321992ca`)**:**全绿** —— 25 successful / 16 skipped / **0 failing / 0 pending**,其中此前唯一的红 `Vuln scan (pnpm audit prod+full)` 现为 **pass(33s)**:它当初失败的原因是 base 早于 #2240(`app/pnpm-lock.yaml` 仍锁 xmldom 0.8.13/0.9.10 + fast-uri 3.1.5),rebase 带入 #2240 的 override 后自动解除,**本 PR 未为过门禁改任何依赖或例外登记**。`mergeStateStatus: CLEAN`。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant