fix(deps): @xmldom/xmldom 版本域 override 0.8.15/0.9.12 + fast-uri>=3.1.6,prod audit 4→2 / 全量 8→2 (#2154) - #2240
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
…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>
3655431 to
4d65dc0
Compare
记录更新:rebase 后的 HEAD 与 CI 实测(补正文里的 HEAD 字段)正文写的是 push 当时的 HEAD
CI(checks workflow,run
正文"未验证项"里的第 1、2 条(vitest/coverage/tsc/docker、真实 |
…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 由主机侧统一收口,不在此自动关单)。
…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`。
为什么
CI job
Vuln scan (pnpm audit prod+full)(.github/workflows/checks.yml:2325,path-filtered 到app/**)在 master 上显示 skipped,属潜伏红:任何碰app/**的 PR 都会踩。实测 #2238 已被它打红:(run 33658185803 / job 100341979789)
按仓库例外政策(
scripts/verify/vulnerability-exceptions.json抬头:仅允许登记上游无修复版本的漏洞;有修复版本的漏洞必须通过 overrides 升级解决,禁止登记),本 PR 全部走 override 升级,未新增任何例外登记。涉及的 advisory(均为 2026-09-02 新发布)
>=0.7.0 <=0.8.14>=0.9.0 <=0.9.11>=3.0.0 <3.1.6>=3.1.2 <3.1.6>=3.0.0 <3.1.6>=3.1.3 <3.1.6为什么 fast-uri 也在这个 PR 里:CI 的 prod 步先
exit 1,同 job 的全量步从未执行,所以这 4 枚 high 在 #2238 的红灯里完全不可见(本次是全量 audit 才扫出来)。只修 xmldom 会把红从 prod 步搬到 full 步,门禁仍然红,#2238 仍然被卡。它们同样"有修复版本 → 禁止登记例外 → 必须 override 升级",形态与 xmldom 完全一致,写集也不变,故同批修掉。依赖来源(
app/pnpm-lock.yaml实测 + registry 核对上游声明范围):@expo/plist@0.7.0(mobile-rn>expo>@expo/cli)^0.8.8mammoth@1.12.0^0.8.6plist@3.1.1(…>xcode>simple-plist)^0.9.10fast-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.yaml(fix(frontend): move pnpm overrides to workspace config #1713 迁入),不在app/package.json——后者的pnpm字段至今只有hoistedDependencies,没有overrides。postcss-selector-parser: ">=7.1.3 <8",追加在 overrides 块末尾)→ 本 PR 追加"@xmldom/xmldom@0.8"/"@xmldom/xmldom@0.9"于同一位置。"brace-expansion@1": ">=1.1.16 <2"→">=1.1.18 <2")→ 本 PRfast-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 自动跟进。fix(deps): 中文摘要 + 可核对的前后计数(先例…全量 audit 5→2),正文逐条列 GHSA/CVE、严重度、路径、SSOT 位置、实测数字。改动
app/pnpm-workspace.yaml(overrides SSOT,3 行):app/pnpm-lock.yaml:30 行变动,只动 3 个包(@xmldom/xmldom0.8.13→0.8.15、0.9.10→0.9.12、fast-uri3.1.5→3.1.7),无其他包版本漂移。写集只有这两个文件;没有改任何
package.json(先例同样没改)。验证:audit 前后计数
命令(直连
registry.npmjs.org——本机默认华为云镜像不实现 audit POST,会返 405;这也是verify-vulnerability-gates.sh里那条诊断提示的由来):pnpm audit --prodpnpm audit(全量)metadata 计数:prod
moderate 2 + high 2→moderate 0 + high 2;全量moderate 2 + high 6→moderate 0 + high 2。依赖总数未变(prod 1095 / 全量 1644)。修复后两个范围的 GHSA 集合均为
{GHSA-w3rx-r6r6-pgpr, GHSA-5p2g-fcmc-qvqq}——GHSA-6gmq-8vp8-gcm6与 4 枚 fast-uri GHSA 都已消失。验证:lock 里只剩 patched 版本
0.8.13/0.9.10/fast-uri@3.1.5在 lockfile 中已零出现(grep 全库仅剩一处无关命中minimatch: 3.1.5)。供应链一致性:三个新版本的 lock
integrity与registry.npmjs.org的dist.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.yaml、app/pnpm-workspace.yaml)。文件内容仍是两条image-size登记,tracking#1663,patched=<0.0.0(上游最新 2.0.2 即受影响,无修复版本):且它们仍被放行:修复后 verifier 对两个 scope 都输出
::warning::… found registered no-fix advisories (tracked): GHSA-5p2g-fcmc-qvqq GHSA-w3rx-r6r6-pgpr并exit 0——这正是设计行为(不可修项降级为 tracked warning),本 PR 一行都没动。门禁表
HEAD =
36554318(fix/deps-xmldom-advisory),base =77c75e44(origin/master,push 前git fetch origin && git rebase origin/master已确认无新增 commit)。bash scripts/verify/verify-vulnerability-gates.sh pnpm-audit prod < audit.jsonexit 1:found vulnerable package(s) not in exception registry: GHSA-6gmq-8vp8-gcm6exit 0:warning … registered no-fix advisories (tracked): GHSA-5p2g-fcmc-qvqq GHSA-w3rx-r6r6-pgprpnpm-audit fullexit 1:GHSA-5jgf-p345-68v8 GHSA-6gmq-8vp8-gcm6 GHSA-f65p-4m7j-42xc GHSA-fph4-wmhf-6fwf GHSA-jqff-g426-hqxpexit 0(同上 tracked warning)bash scripts/verify/tests/verify-vulnerability-gates.Tests.shexit 0)cd app && pnpm install --frozen-lockfile --lockfile-onlyexit 0(Scope: all 6 workspace projects / Done in 849ms)bash scripts/verify/verify-commit-messages.sh origin/master HEADcommit message check ok (1 commits)cd app && pnpm install --lockfile-onlyexit 0,无 ERR;peer 警告为既有(见下)peer 警告说明(既有,非本 PR 引入):
pnpm install --lockfile-only输出 3 条 unmet peer ——eslint-plugin-import@2.32.0要eslint ^2…^9(仓库用 10.4.0)、@lobehub/ui>@emoji-mart/react@1.1.1要react ^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 无关,且:@expo/dom-webview@56.0.5+@expo/log-box@56.0.14这对组合在 master 的 lockfile 里就已如此(第 1472 / 1506 行),本 PR 的 lock diff 零行涉及 expo。@xmldom/xmldom与fast-uri都是零依赖包,结构上不可能引入 unmet peer。临时 worktree 已
git worktree remove --force删除;主 worktree 用--lockfile-only,未生成任何node_modules(find .worktrees/deps-xmldom -name node_modules为空,目录 32M = 纯源码 checkout),磁盘无多余副本。未验证项(交 CI)
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 转换。兼容性风险评估
低风险:
lts(0.8.15)与latest(0.9.12)dist-tag;^0.8.8/^0.8.6/^0.9.10),不是强制跨 minor 提升;fast-uri3.1.5 → 3.1.7 同为 3.x patch,且只在 dev/lint 链(stylelint>table>ajv),不随产物发布;若 CI 上出现
@expo/plist/mammoth与 0.8.15 的明确不兼容信号,回退方案是把"@xmldom/xmldom@0.8"从 overrides 里摘掉、只保留 0.9 域(prod 门禁的 xmldom 命中会回来一条),或等上游@expo/plist/mammoth自己 bump——不建议改走例外登记(政策禁止:有修复版本)。