Skip to content

docs: 对外文档诚实批(lane E / #2154 切片 C)——6 条报告逐条复核后修 5 条,1 条改判,3 处归属拒绝猜号 - #2237

Merged
DeliciousBuding merged 7 commits into
masterfrom
docs/external-doc-honesty
Sep 2, 2026
Merged

docs: 对外文档诚实批(lane E / #2154 切片 C)——6 条报告逐条复核后修 5 条,1 条改判,3 处归属拒绝猜号#2237
DeliciousBuding merged 7 commits into
masterfrom
docs/external-doc-honesty

Conversation

@DeliciousBuding

@DeliciousBuding DeliciousBuding commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

这条 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 = 8f2ade25,base = dc7df53d(origin/master)。7 个 commit,每个 commit 一次门禁。

push 后 master 又前进了一次,本分支故意没有再 rebase(供主机侧合并时裁决):push 完成后 origin/masterdc7df53d 前进到 77c75e44(Lane B:edge EventLog/Bus seq 顺序不变量,只动 edge-server/internal/events/ 下 5 个 .go)。实测与本分支 9 个文件零重叠comm -12 输出为空),git merge-tree 0 个冲突标记gh api pulls/2237 返回 mergeable=true / mergeable_state=behind。不再 rebase 的理由:(1) squash merge 会落到 master 当前 tip,线性历史要求仍满足,behind 不阻塞;(2) rebase 会改掉 HEAD sha 8f2ade25,而该 sha 已作为证据写进本 PR 正文与 #2154 登记评论,且会让上面那次已全绿的 CI run 失效、需要重跑重证 —— 零收益、有诚实成本。若主机侧偏好 up-to-date 分支,rebase 后请重跑 verify-doc-ssot.py + wc -l AGENTS.md(预期仍 284)并同步更新两处 sha 引用。


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

1. CONTRIBUTING.md:36make test 跑前端 vitest —— ✅ 报告成立

文档原话

make test          # Go 单元测试(L0,-short)+ 前端 vitest
make fe-test       # 前端 vitest

(自相矛盾:紧邻行已把前端 vitest 单独归给 fe-test

代码事实

  • Makefile:51test: test-edge test-hub
  • Makefile:53-54test-edge: cd edge-server && go test ./... -short -count=1 -timeout 60s
  • Makefile:61-62test-hub: cd hub-server && go test ./... -short -count=1 -timeout 60s
  • 前端 vitest 是独立目标 Makefile:154-155fe-test: cd app && pnpm -r test
  • test 的依赖闭包里没有任何前端目标.PHONYMakefile:11-12 亦把 testfe-test 分列)
  • 对照另两处:docs/developer-quickstart.md:122正确口径(「make test | test-edge + test-hub,均为 go test -short」),:123 把前端归给 fe-testAGENTS.md:137 的入口列把两个目标并列但未区分谁跑什么,读起来像 make test 也含 vitest。

改后原话

完整命令速查见 docs/developer-quickstart.md §测试速查(SSOT)。
每个 `make` 目标实际跑什么以根 `Makefile` 为准,本文件只列目标名不复述其内容(防漂移)。常用入口:

make test          # L0 后端单元(Makefile `test` 目标闭包,不含前端)
make fe-test       # L0 前端单元(Makefile `fe-test` 目标)

AGENTS.md:137 入口列同步改为 `make test`(Go)/ `make fe-test`(前端)
docs/developer-quickstart.md:122-123 本来就是事实,未改(它已声明「本地入口以根 Makefile 为准」)。
四处口径统一方式 = 指明目标名 + 指回 Makefile/quickstart 作 SSOT,不复述目标内容,因此 Makefile 日后改闭包不会再让文档说谎。


2. CHANGELOG.md:5-7「暂无未发布变更」—— ✅ 报告成立,且 SSOT 判断成立

文档原话

## [Unreleased]

暂无未发布变更。

代码/CI 事实

  • cliff.toml 存在且是 git-cliff 模板(conventional_commits=true,commit_parsers 分 10 组,slice(end=20) 控长)
  • .github/workflows/release.yml:496-518Generate release notes with git-cliff"$GIT_CLIFF" --latest -o release-notes.md
  • .github/workflows/release.yml:534-538softprops/action-gh-release@v3 + body_path: release-notes.md
  • 逐版本用户可见变更的真实 SSOT 确实是 git-cliff 生成的 GitHub Release 正文,报告判断成立。
  • 原文为假的量化证据:git rev-list --count v0.6.1..HEAD = 462(185 fix / 61 test / 52 feat / 49 refactor / 40 docs / 39 chore / 23 ci / 10 perf),其中含 wire 行为变更(如 HEAD dc7df53d 前一条 d1dc97f 的 pageSize clamp 语义)。
  • 额外支撑「不要手写 Unreleased」:scripts/verify/verify-doc-ssot.py:241 注释原话 "CHANGELOG.md is immutable historical prose"(该文件因此被排除出活跃文档扫描),且 :264CHANGELOG.md 定了 90 行预算 —— 手写清单既必然过期又会挤爆预算。

改后原话(按要求手写会立刻过期的列表,改为声明 SSOT + 生成方式 + 指向 release 页面)

## [Unreleased]

本节**不维护手写清单**,也不代表「没有未发布变更」。SSOT 与生成方式:

- **逐版本用户可见变更的 SSOT 是 GitHub Release 正文**:由 git-cliff 按 Conventional Commits
  从 tag 区间自动生成(模板 `cliff.toml`,生成与发布步骤在 `.github/workflows/release.yml` 的
  `Generate release notes with git-cliff` / `Create GitHub Release`)。已发布列表见
  [GitHub Releases](https://github.com/TokenDanceLab/AgentHub/releases)。
- **本文件是不可变历史**:只保留已发布版本的 longform 破坏性变更/升级注意 …… 手写 Unreleased
  列表既必然落后于 master,又会挤占该预算,所以明确不做。
- 打新 tag 发布时,按需在此追加一节 longform;commit 级明细去 Release 正文看,不复制进本文件。

文件头一句同步从「所有值得发布的用户可见变化……记录在这里」改为「已发布版本的 longform 记录……commit 级明细由 git-cliff 生成到 Release 正文」。
CHANGELOG.md 80 行(预算 90)。AGENTS.md §12 加了指回同一 SSOT 的 owner 行(见第 7 节)。

⚠️ 附带发现(本 PR 不改,已登记 #2154:tag v0.6.1(6c7c64c) 不是当前 master 的祖先 —— git merge-base --is-ancestor v0.6.1 HEAD 返回否、git merge-base v0.6.1 HEAD 为空;master 历史在 372a211(2026-08-14)重新生根,全仓仅 462 个 commit。后果:release.yml:42 的 tag-guard(git merge-base --is-ancestor "$GITHUB_REF_NAME" origin/master)对该旧 tag 会判失败;git-cliff --latest 对下一个 tag 找不到可达的前序 tag,会把 462 个 commit 全量塞进 release 正文(cliff.tomlslice(end=20) 只能限制每组条数,不能限制组数)。这是发布链路问题,不是文档问题,纯文档 lane 不动它。


3. SECURITY.md:23 安全门禁工具名指错 —— ✅ 报告成立(且原文另有一处更严重的不诚实)

文档原话

- 已修复漏洞会在 release notes(`BREAKING CHANGE` / security 分组)中披露。
- 依赖漏洞由 CI 持续扫描:`govulncheck`(Go)、`pnpm audit --prod`(JS)、`cargo audit`(Rust),均 fail-closed。

CI 事实.github/workflows/checks.yml

原文说法 实测
JS = pnpm audit --prod vuln-scan-js 两条都跑:2359-2360 pnpm audit --prod + :2372-2373 全量 pnpm audit#2154 F-d 新增,workflow 注释原话:--prod 对 dev/lint/build 链通告「完全不可见」,brace-expansion 两枚 high 曾在 --prod 全绿下钉在 lockfile 里)
Rust = cargo audit vuln-scan-rust:2445 的门禁是 scripts/verify/verify-rust-advisories.sh:2437-2441cargo binstall cargo-audit)。该脚本头注释原话:裸 cargo audit 把 unsound 类当 informational 警告保持绿灯,「the exact cross-ecosystem false-green this gate closes」——原文指错的正是被这个门禁堵掉的假绿工具
「均 fail-closed」 判定其实统一由 scripts/verify/verify-vulnerability-gates.sh#1534)做:工具故障/输出不可解析/存在漏洞都判红,唯一豁免通道是 scripts/verify/vulnerability-exceptions.json。原文没提这个单点判定器
「CI 持续扫描」 三个 job 均 needs: changes 路径过滤:vuln-scan-js:2327-2329(frontend) / vuln-scan-go:2382-2384(go) / vuln-scan-rust:2414-2416(desktop),workflow_dispatch 时无条件跑 ⇒ 不是每次 push 全量扫描
(原文未提) Go 静态安全扫描是 gosec:161-169(go-edge) / :363-370(go-hub),输出经 scripts/verify/verify-gosec-gates.sh fail-closed 判定

追加复核出的第二处不诚实(报告未提,我一并修了):原文承诺漏洞在 release notes 的「security 分组」披露 —— 该分组结构上永远为空cliff.toml 的 parser { message = "^security", group = "🔒 Security / 安全加固" } 要求 security 提交类型,但 scripts/verify/verify-commit-messages.sh:8 的白名单是 TYPES="init|feat|fix|docs|refactor|chore|test|perf|ci|revert"不含 security;实测当前 462 个 commit 中 ^security(\(|:) 计数 = 0

改后原话(节选)

- 已修复漏洞在 release notes 中披露,但**没有独立的 security 分组可用**:release notes 由 git-cliff
  按 Conventional Commits 类型分组(`cliff.toml`),而提交类型白名单是 feat|fix|docs|refactor|chore|
  test|perf|ci|revert(`scripts/verify/verify-commit-messages.sh`,不含 `security`),所以安全修复
  实际落在 `fix`/`feat` 等分组里;只有 `!:` 或正文含 `BREAKING CHANGE` 的提交会进破坏性变更分组。
- 依赖漏洞由 `.github/workflows/checks.yml` 的三个 `vuln-scan-*` job 扫描,判定统一交给
  `scripts/verify/verify-vulnerability-gates.sh`(fail-closed ……):
  - **Go**(`vuln-scan-go`):`govulncheck`,按 hub-server / edge-server 矩阵各跑一次。
  - **JS**(`vuln-scan-js`):`pnpm audit --prod` **和** 全量 `pnpm audit` 两条都跑,互不替代 ……
  - **Rust**(`vuln-scan-rust`):门禁是 `scripts/verify/verify-rust-advisories.sh`(内部调用
    cargo-audit),**不是裸 `cargo audit`** ……同 job 的 `cargo clippy` 是 continue-on-error advisory,不阻断。
- 上述三个 job 都经 `changes` job 的路径过滤触发 ……因此不是「每次 push 全量扫描」。
- Go 静态安全扫描另由 `gosec` 承担(`go-edge` / `go-hub` job ……),与依赖漏洞扫描是两件事。

未改的相邻声明也已复核为真:SECURITY.md:32scripts/release/verify-release-gate.py 读本表 —— 实测 verify-release-gate.py:281 risk_path = os.path.join(repo_root, "SECURITY.md"):183-184 捕获 rotate required / verification required。✅

顺带活体验证(支持上面的改法):push 时 GitHub 报「default branch 2 high vulnerabilities」。查 gh api .../dependabot/alerts = alert #52/#53,均为 npm/image-sizefixed=none,上游无修复版本),正好一一对应 scripts/verify/vulnerability-exceptions.json 登记的 GHSA-5p2g-fcmc-qvqq / GHSA-w3rx-r6r6-pgpr(tracking #1663)。即门禁按设计放行「上游无修复版本」的已登记例外,不是假绿;这也实证了我写进 SECURITY.md 的「唯一豁免通道是例外表」。


4. README.md / README_EN.md —— ⚠️ 部分成立(禁用词那条报告说法不准确,但假声明本身成立)

(a) 结构表漏 pkg/app/workbench —— ✅ 成立

事实:go.work 的第三个 use 项是 ./pkgpkg/go.mod 是独立 module,含 errcode/jwtutil/logmask/safego/testkit/debug/otelids/outboundmetrics/reqlog 9 个包(pkg/README.md 自述「hub-serveredge-server 共享的 Go 包」)。app/workbench/package.json = @agenthub/workbench,其 README 与 AGENTS.md:67 均声明「依赖方向 workbench → shared 单向」,并由 scripts/verify/verify-frontend-package-boundary.py(checks.yml:2044 接线,负向自测 :2047)机器强制。两者都真实存在却未在结构表登记。

改后(中英各补两行,同步):

| `app/workbench` | 端级工作台壳(`@agenthub/workbench`,依赖方向 workbench → shared 单向) |
| `pkg` | Hub/Edge 共享的 Go 包(errcode、jwtutil、logmask、testkit 等;独立 module,经根 `go.work` 联编) |
| `app/workbench` | Platform workbench shell (`@agenthub/workbench`; one-way dependency workbench → shared) |
| `pkg` | Go packages shared by Hub/Edge (errcode, jwtutil, logmask, testkit, ...; separate module joined via the root `go.work`) |

(b)「路线图/roadmap 被门禁主动禁止却仍存在」—— ❌ 报告说法不成立,但底下的假声明成立

实测 verify-doc-ssot.py 的禁用面只有::100 根级文件 ROADMAP.md 不得存在(理由原文 "roadmap state lives on GitHub issues, not a repo file")、:109 陈旧路径 docs/roadmap:200 正则 ROADMAP\.md没有任何规则禁「路线图」或「roadmap」这个词,所以「门禁主动禁止却仍存在」是错的(门禁跑得过就是证据)。

但 README 的声明本身是假的:ls docs/ = README.md api-reference.md architecture architecture.md archives component-acceptance component-acceptance.md decisions.md developer-quickstart.md governance history.md images reference —— 无任何 roadmap 文件;全 docs/ 只有 docs/history.mddocs/architecture/10-macro-engineering-design.md 两处提到该词,都不是路线图载体。

改后(中英同步,把假声明换成门禁理由本身陈述的真事实):

| `docs` | 架构、治理与设计文档(进度与路线图在 GitHub issues,不在 `docs/`) |
| `docs` | Architecture, governance, and design docs (progress and roadmap live in GitHub issues, not `docs/`) |

(c) README_EN.md:65 应为 corepack pnpm dev —— ✅ 成立

事实:docs/developer-quickstart.md:77/83/91/99 一律 corepack pnpm ...README.md:64-65(中文)已是 corepack pnpm install / corepack pnpm devREADME_EN.md:64 也是 corepack pnpm install唯独 :65pnpm dev —— 同一代码块内 step4/step5 自相矛盾,且未 corepack enable 时 step5 会 command-not-found。
(CI 侧不用 corepack:workflow 走 pnpm/action-setup@v6,所以判据取本地启动 SSOT = quickstart。)

改后:

corepack pnpm dev                           # 5. Start Desktop Vite (:5173); web uses corepack pnpm dev:web

parity 门禁复核verify-doc-ssot.py:check_readme_parity() 只校验两对成熟度标记 —— ("Mobile 装配中","Mobile in assembly")("Desktop/Web 主线","Desktop/Web are the mainline"),要求同现或同缺。这 4 个标记在 README.md:31 / README_EN.md:31 均在,本 PR 未触碰;改动前后 DOC-README-PARITY 均 PASS。中英两份的结构表/快速开始改动是逐行对称做的。


5. verifier-map.md + docs/architecture/README.md 双向差集 —— ✅ 成立(补 12 行 + 修 2 处不诚实 + 补 1 行索引)

方法ls scripts/verify/ scripts/release/*.py|*.sh)× grep .github/workflows/ 双向差集,再用 python 解析 checks.yml 的 job 缩进定位每条命中的 job ID + 行号,只登记「真有 workflow 调用」的。

补登记的 12 条(每行都指向真实存在的脚本 + 真实 job,verify-doc-ssot.py 逐条校验,校验数 53→66 脚本路径 / 45→58 CI 文件)

新增行 脚本 接线位置(实测行号)
依赖漏洞扫描 fail-closed 判定 scripts/verify/verify-vulnerability-gates.sh checks.yml vuln-scan-js:2360,2373 + vuln-scan-go:2406;自测 validate:1981
Rust advisory 门禁 scripts/verify/verify-rust-advisories.sh vuln-scan-rust:2445;自测 :2449
质量债双向棘轮 scripts/verify/verify-quality-debt-ratchet.py validate:2084;自测 :2087
auth 依赖所有权 scripts/verify/verify-auth-dep-ownership.py validate:2081
migration DDL 幂等棘轮 scripts/verify/verify-migration-idempotency.py validate:2027
i18n dead-key 棘轮 scripts/verify/verify-i18n-deadkeys.py validate:2030
OIDC 授权码 SSOT scripts/verify/verify-oidc-code-ssot.py validate:1971;自测 :1974
Edge orchestrator 依赖方向 scripts/verify/verify-orchestrator-deps.py go-edge:189;自测 :194
夹具连接钉死 scripts/verify/verify-fixture-connection-pinning.py validate:2059;自测 :2062
远程 devserver 隐私/可移植合同 scripts/verify/verify-devserver-contract.py validate:1956
L3 raw artifact 脱敏 scripts/verify/verify-real-e2e-artifacts.py real-e2e-stack:1856
Go 覆盖率 profile 合并 scripts/verify/merge-coverprofiles.py go-edge:113 / go-hub:333

每行的规则描述取自对应脚本的 docstring 原文(例如 verify-i18n-deadkeys.py 的保守死键判据、verify-auth-dep-ownership.py 的「同进程多 App 互相覆盖安全配置」、verify-migration-idempotency.py#2125 follow-up),不是我自己编的。
旁证:docs/architecture.md:112 早已把 verify-orchestrator-deps.py 称为「机器门禁」,而 verifier-map 里查无此行 —— 正是「已接线但未登记」。

顺带修的两处不诚实(差集反向侧发现)

  1. 原第 60 行「真实登录/OIDC e2e 链路 …… | 无 | 无」为过期:实测 scripts/e2e/run-real-e2e-lane.sh 已接线 checks.yml:1846real-e2e-stack,dispatch-only),且该脚本 :53/:371 内部调用 verify-real-e2e-lane-manifest.py 做六字段合同自检。改为如实区分「lane 入口已接线(仅 workflow_dispatch,不阻塞 PR)」与「verify-oidc-flow.py / verify-login-fixture-topology.py / verify-approved-real-preflight.py 仍未被任何 workflow 调用」(这三个的未接线状态由 grep .github/workflows/*.yml 零命中实证)。
  2. 2026-08-29 新增的宏观四行与主表之间有一个空行,而它们没有自己的表头 + |---| 分隔行 ⇒ GitHub 会把这 4 行渲染成普通文本而非表格行(表格被截断)。删掉该空行使其并入主表。这是渲染缺陷,不是内容问题,四行原文一字未动。

docs/architecture/README.md 漏项ls docs/architecture/ 有 13 个 .md,子索引只列 01-11,漏 github-actions-ci-cd-policy.md;而主索引 docs/architecture.md:101 已登记它 —— 单侧缺口。补一行(并注明「无编号,主索引同样登记」)。28 行(预算 40)。

verifier-map.md 87 行(预算 120)。


6. scripts/verify/quality-debt-baseline.jsonissue 归属 —— ⚠️ 报告部分成立:6 条里只敢改 2 条

复核方式gh issue view <n> 逐个读标题 + 正文 + 文件面,不照抄报告。原状:6 条 soft_gate 里 5 条 issue 都是 1573

先确认 1573 对这 5 条可证伪gh issue view 1573 = 「[P1] 建立 Hub lint finding fingerprint ratchet并移除整 job 软放行」,正文原话「当前 Hub golangci-lint 仍有约 69 个存量 finding」+「#1568 继续只负责 complexity exclusion,不混用 owner」⇒ 它是 Hub 专属,且明令禁止混用 owner。

改了的 2 条(归属已确认)

location 改后 确认证据
frontend-desktop: Lint 1573 1575 gh issue view 1575 = 「[P2] 偿还 Desktop ESLint 债并移除 frontend-desktop lint 软门禁」;正文原话「quality-debt baseline 暂时把该条目指向 #1536,但 #1536 是治理机制任务……不能继续作为实际 lint 债的 owner」,完成条件含「quality-debt baseline 删除该 soft gate」⇒ 就是本条债的 owner。(reason 字段原本已写 (#1575),只有 issue 字段是错的)
frontend-web: Lint Web 1573 1581 gh issue view 1581 = 「[P2] 清零 Web ESLint warnings并让 Lint Web 对新增 warning fail-closed」;正文原话「#1575 只负责 Desktop ESLint,不覆盖 Web」。⚠️ 报告此点不成立:报告称真实归属是 1575,实测 1575 自己明确排除 Web,真 owner 是 #1581reason 里的 (#1575) 一并改为 (#1581)

第 1 条 go-edge: Lint = 1840 本来就对(gh issue view 1840 = 「[P2] edge-server lint 棘轮补齐:基线登记 + fail-closed 门禁(对偶 #1812)」),未动。

没改的 3 条(按任务书「若无法确认归属就不要改」,已登记 #2154「归属待确认」)

location 现值 为什么不敢改
frontend-mobile: Lint (mobile rules) 1573 #1575 自述只管 Desktop、#1581 自述只管 Web;gh search issuesmobile eslint / frontend-mobile / Lint (mobile rules) / mobile lint 软 均未找到 mobile lint 软门禁的专属 issue。reason 里的 (#1575) 同样无据,但没有可替换的已确认号
validate: Verify i18n callsites ratchet (#1612) 1573 报告称应为 1612,实测 1612 不是它gh issue view 1612 返回的是 PR「docs(progress): MASTER 同步 — #1535 合并状态与复杂度豁免偿清」,gh pr view 1612 --json files 显示只改了 docs/progress/MASTER.md 一个文件,与 i18n 毫无关系。而仓内 checks.yml:2126 step 名、docs/governance/verifier-map.md:27CHANGELOG.md:44 三处一致#1612 ⇒ 是全仓的 #1612 引用本身可疑,不是 baseline 单点笔误。无法从仓内或 GitHub 定位真号,不猜
vuln-scan-rust: cargo clippy (advisory) 1573 reason 写 (#1578)gh issue view 1578 确实存在且是创建 vuln-scan-rust job 的那个 issue(「补 Rust advisory 门禁」),但其正文与验收条件全程不含 clippygh issue view 1578 --json body | grep -i clippy 零命中),验收项讲的是 glib UB + advisory gate。「24 条 clippy warning 待归零」这笔债没有可确认的 owner issue(gh search issues clippy 只命中 #2154 自身)

这 3 条我故意留着已知错误的 1573 不用未确认号覆盖:把错号换成另一个猜出来的号,只会让下一个人更难发现它是错的。改为在 #2154 显式登记待裁决。

改动安全性复核verify-quality-debt-ratchet.py:232-236 只校验 issue 存在且为正整数、不校验取值;:426-453 的 historical ratchet 只比较 review_by(延期需 extension_reason),不比较 issue/reason ⇒ 本次改动不触发任何棘轮。改完按要求实跑:9 pass / 0 fail,并加跑负向自测 scripts/verify/tests/verify-quality-debt-ratchet.Tests.py15 tests OK


7. AGENTS.md §12 加「CHANGELOG owner」——净减 2 行

加(+2 行:1 行内容 + 1 行 markdown 空行),§12 发布流程:

变更记录 owner:commit 级用户可见变更由 git-cliff 从 tag 区间生成到 GitHub Release 正文
(`cliff.toml` + `.github/workflows/release.yml`),任何文档不手写;`CHANGELOG.md` 只保留
已发布版本的 longform 破坏性变更/升级注意,不维护 Unreleased 清单。

与第 2 条的 CHANGELOG 声明互为指针,两处不会各自漂移。

删(-4 行,全部是「复制 owner 文档内容」因而最易漂移的行,逐条说明为什么已过期/冗余)

  1. §9.5 - 里面有什么:全表三列(规则/验证脚本/CI job)+ 维护规则;\无` 表示暂无机器验证,靠人工自觉,规则本身不因此失效。—— 与docs/governance/verifier-map.md:10 **逐句重复**(原文:「下表列出有机器管的规则与其验证脚本、CI job。\无` 表示暂无机器验证,靠人工自觉,规则本身不因此失效。」)。verifier-map 才是映射 SSOT,AGENTS.md §9.5 自己第一句就写着「本节不复制长表」,却在下面复制了表的列结构与图例。
  2. §9.5 - 权威范围:映射只描述“有没有机器管”,规则本身的权威仍是本文件;两者冲突时以本文件为准,并更新映射表。 —— 同与 verifier-map.md:10 重复(原文:「映射只描述「有没有机器管」;规则本身的权威是 `AGENTS.md`,两者冲突时以 `AGENTS.md` 为准并更新本表。」);且优先级已由 §0 事实源优先级(AGENTS.md 第 2 位 / 其他专题文档第 5 位)确立。删后该权威声明仍在 verifier-map.md 单点存在,信息零丢失。
    3-4. 上述两条删完后 §9.5 只剩单元素 bullet 列表(格式噪声),把该 bullet 并入上一段,省掉 bullet 行与其前面的空行。

保留 §9.5 的 verifier-map 指针行本体 ⇒ check_verifier_map_owner()(要求 AGENTS.md 含 docs/governance/verifier-map.md)仍 PASS。

wc -l AGENTS.md = 284(改前 286,门禁上限 300)。净变化 −2,符合「净中性或净减少」。


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

# 报告说法 实测 处置
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 = origin/master,rebase 后无冲突)

门禁 命令 结果
文档 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 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 testgo build、vitest、coverage、全量 golangci-lint、docker、make *(4 核机 + 并行 lane)。
golangci-lint 幽灵:本 lane 未跑 golangci-lint,未遇到指向已删除 worktree 路径的缓存幽灵 issue。


未验证项(诚实声明)

  1. 没有跑任何 CI已验证(本条从「未验证」升级为「已验证」,PR 开出后回写):GitHub Actions run 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→[P2] 偿还 Desktop ESLint 债并移除 frontend-desktop lint 软门禁 #1575 / web→[P2] 清零 Web ESLint warnings并让 Lint Web 对新增 warning fail-closed #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 ratchetvuln-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.pyscripts/verify/tests/verify-real-e2e-artifacts.Tests.py 存在于磁盘但没有任何 workflow 调用grep .github/workflows/ 零命中)⇒ 两个负向自测是死的。我在 verifier-map 里因此没有把它们写成「负向自测」(只登记了脚本本体),避免制造新的假绿声明;是否接线请裁决。
  6. [P2] 偿还 Desktop ESLint 债并移除 frontend-desktop lint 软门禁 #1575 / [P2] 清零 Web ESLint warnings并让 Lint Web 对新增 warning fail-closed #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 用了同一个临时文件名。后果与处置:

Closes 无(本 PR 是 #2154 的切片 C,#2154 由主机侧统一收口,不在此自动关单)。

@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: fcf20e11-b4cf-4d25-a6bc-6291c296050c

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.

DeliciousBuding and others added 7 commits September 3, 2026 02:17
CONTRIBUTING.md:36 原称 `make test` = Go 单元 + 前端 vitest,与紧邻行
`make fe-test` 自相矛盾。实测 Makefile:59 `test: test-edge test-hub`,
两个子目标均为 `go test ./... -short`(Makefile:62/70),前端 vitest 由
独立 `fe-test` 目标承载。

改为只列目标名 + 声明「目标内容以根 Makefile 为准」,避免文档复述目标
内容再次漂移;AGENTS.md L0 行的入口列同步标注 (Go)/(前端) 消歧。
docs/developer-quickstart.md:122-123 本来就是正确口径,未改。

门禁: python3 scripts/verify/verify-doc-ssot.py PASS

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
复核成立:仓内有 cliff.toml(git-cliff 模板,含 conventional commit 分组)
与 .github/workflows/release.yml:496-538(Generate release notes with
git-cliff → softprops body_path: release-notes.md),即逐版本用户可见变更
的真实 SSOT 是自动生成的 GitHub Release 正文。

实测 v0.6.1 之后 master 有 462 个 commit(185 fix / 52 feat / 49 refactor
/ 10 perf 等),原文「暂无未发布变更」为假。按任务要求不手写会立刻过期
的 Unreleased 列表,改为明确声明 SSOT + 生成方式 + 指向 releases 页面。

附带发现(本 commit 不改,另在 PR/#2154 登记):tag v0.6.1(6c7c64c) 不是
当前 master 的祖先(git merge-base --is-ancestor 返回否,历史在 372a211
2026-08-14 重新生根),release.yml:42 的 tag-guard 对该旧 tag 会判失败。

门禁: verify-doc-ssot.py PASS;CHANGELOG.md 80 行(预算 90)

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
复核成立(.github/workflows/checks.yml):
- vuln-scan-js:2350/2362 同时跑 pnpm audit --prod 与全量 pnpm audit(#2154 F-d),
  原文只写 --prod 漏了 dev-chain 全量审计。
- vuln-scan-rust:2445 的门禁是 scripts/verify/verify-rust-advisories.sh(包装
  cargo-audit 并把 unsound 类纳入判定 + allowlist 到期日),原文写「cargo audit」
  指错了工具:裸 cargo audit 正是该脚本要堵的跨生态假绿。
- 三个 job 均 needs: changes 路径过滤(go/frontend/desktop)+ workflow_dispatch,
  原文「CI 持续扫描」不准确。
- 三者判定统一走 scripts/verify/verify-vulnerability-gates.sh(#1534 fail-closed),
  原文未提。

额外复核发现(原文亦不诚实,一并修):line 22 称已修复漏洞在 release notes 的
「security 分组」披露,但 cliff.toml 的 ^security parser 永远不可能命中——
scripts/verify/verify-commit-messages.sh:8 的类型白名单不含 security,且当前
462 个 commit 中 security 类型计数为 0。改为如实说明落点。

门禁: verify-doc-ssot.py PASS

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
…始补 corepack

三处复核:
1. 结构表漏目录:pkg/(pkg/go.mod 独立 module,根 go.work 第三个 use 项,含
   errcode/jwtutil/logmask/testkit 等 9 包)与 app/workbench(@agenthub/workbench,
   workbench → shared 单向依赖,由 verify-frontend-package-boundary.py 强制)
   均真实存在但未登记。中英各补两行。
2. docs/ 描述称含「路线图」为假:ls docs/ 只有 architecture/governance/reference/
   component-acceptance 等,无任何 roadmap 文件;verify-doc-ssot.py:100 的
   ROADMAP.md 禁令理由即「roadmap state lives on GitHub issues, not a repo file」。
   改为「架构、治理与设计文档(进度与路线图在 GitHub issues,不在 docs/)」。
   注:报告称「路线图/roadmap 被门禁主动禁止」不准确——门禁禁的是 ROADMAP.md
   文件与 docs/roadmap 路径,不是「路线图」这个词;假声明本身成立,故仍修。
3. README_EN.md:65 pnpm dev → corepack pnpm dev:同一代码块 step 4 已用
   corepack pnpm install,step 5 却裸 pnpm;docs/developer-quickstart.md:77/83/91/99
   一律 corepack pnpm。中英现已一致。

门禁: verify-doc-ssot.py PASS(含 DOC-README-PARITY 两个配对标记)

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
…e 子索引补 CI/CD policy

双向差集复核(ls scripts/verify/ + scripts/release/ × grep .github/workflows/):

已接线但 verifier-map 未登记(全部逐条核对 checks.yml 行号):
- verify-vulnerability-gates.sh → vuln-scan-js:2360/2373 + vuln-scan-go:2406(自测 :1981)
- verify-rust-advisories.sh → vuln-scan-rust:2445(自测 :2449)
- verify-quality-debt-ratchet.py → validate:2084(自测 :2087)
- verify-auth-dep-ownership.py → validate:2081
- verify-migration-idempotency.py → validate:2027
- verify-i18n-deadkeys.py → validate:2030
- verify-oidc-code-ssot.py → validate:1971(自测 :1974)
- verify-orchestrator-deps.py → go-edge:189(自测 :194)
- verify-fixture-connection-pinning.py → validate:2059(自测 :2062)
- verify-devserver-contract.py → validate:1956
- verify-real-e2e-artifacts.py → real-e2e-stack:1856
- merge-coverprofiles.py → go-edge:113 / go-hub:333

顺带修正两处不诚实:
1. 原「真实登录/OIDC e2e 链路」行写 无 | 无,实测 lane 入口
   scripts/e2e/run-real-e2e-lane.sh 已接线 checks.yml:1846 real-e2e-stack
   (dispatch-only)。改为如实区分「lane 已接线」与「verify-oidc-flow.py /
   verify-login-fixture-topology.py / verify-approved-real-preflight.py 仍未被
   任何 workflow 调用」。
2. 宏观四行(2026-08-29 新增)与主表之间有空行,markdown 缺表头+分隔行会被
   渲染成普通文本而非表格行。删掉该空行使其并入主表。

docs/architecture/README.md 子索引漏 github-actions-ci-cd-policy.md,而主索引
docs/architecture.md:101 已登记——补上该行消除单侧缺口。

门禁: verify-doc-ssot.py PASS(verifier-map 校验脚本路径 53→66、CI 文件 45→58);
verify-ci-gates.py PASS;verifier-map.md 87 行(预算 120)、
docs/architecture/README.md 28 行(预算 40)

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
…web→#1581)

复核方式:gh issue view 逐个确认存在性与归属,不照抄报告。

改的两条(归属已确认):
- frontend-desktop: Lint  1573 → 1575
  #1575 = [P2] 偿还 Desktop ESLint 债并移除 frontend-desktop lint 软门禁;
  正文明确「quality-debt baseline 暂时把该条目指向 #1536 …不能继续作为实际
  lint 债的 owner」+ 完成条件含「删除该 soft gate」,即本条债的 owner。
- frontend-web: Lint Web  1573 → 1581(reason 内的 #1575 同步改 #1581#1581 = [P2] 清零 Web ESLint warnings并让 Lint Web 对新增 warning fail-closed;
  正文原话「#1575 只负责 Desktop ESLint,不覆盖 Web」。报告称本条应为 1575,
  实测 1575 明确排除 Web,真实 owner 是 #1581 —— 报告此点不成立,已按实测改。

不改的三条(归属无法确认,按任务要求不猜号,另在 #2154 登记「归属待确认」):
- frontend-mobile: Lint (mobile rules):#1575 自述只管 Desktop、#1581 自述只管
  Web,gh search 未找到 mobile lint 软门禁的专属 issue;reason 里的 (#1575) 无据。
- validate: Verify i18n callsites ratchet:repo 内 checks.yml:2126 / verifier-map /
  CHANGELOG 一律写 #1612,但 gh 实测 #1612 是 PR「docs(progress): MASTER 同步」,
  files 只有 docs/progress/MASTER.md,与 i18n 无关 —— 即全仓的 #1612 引用本身可疑。
- vuln-scan-rust: cargo clippy (advisory):reason 写 #1578#1578 存在且创建了
  vuln-scan-rust job,但其正文/验收条件全程无 clippy(只讲 glib UB + advisory
  gate),无法确认它是「24 条 clippy warning 待归零」这笔债的 owner。
以上三条 issue 仍是 1573(= Hub lint fingerprint ratchet,可证伪的错误值),
本 commit 不用未确认号覆盖,留待人工裁决。

注:verify-quality-debt-ratchet.py 只校验 issue 为正整数与字段齐全(:232-236),
historical ratchet 只比较 review_by(:440-442),故改 issue/reason 不触发棘轮。

门禁: python3 scripts/verify/verify-quality-debt-ratchet.py → 9 pass / 0 fail;
git diff --check 干净

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
净变化 286 → 284(-2),门禁上限 300,wc -l AGENTS.md = 284。

加(+2 行):§12 发布流程新增一行「变更记录 owner」,声明 commit 级用户可见
变更由 git-cliff 从 tag 区间生成到 GitHub Release 正文(cliff.toml +
.github/workflows/release.yml),任何文档不手写;CHANGELOG.md 只保留已发布
版本 longform,不维护 Unreleased 清单。与本分支 CHANGELOG.md 的 SSOT 声明
互为指针,避免两处各自漂移。另加 1 行 markdown 空行使其独立成段。

删(-4 行,均为复制 owner 文档内容因而最易漂移的行):
1. §9.5「- 里面有什么:全表三列(规则/验证脚本/CI job)+ 维护规则;`无` 表示
   暂无机器验证,靠人工自觉,规则本身不因此失效。」——与 docs/governance/
   verifier-map.md:10 逐句重复(该文件才是映射 SSOT,本文件按 §0 只留指针)。
2. §9.5「- 权威范围:映射只描述“有没有机器管”,规则本身的权威仍是本文件;两者
   冲突时以本文件为准,并更新映射表。」——同与 verifier-map.md:10 重复,且
   优先级已由 §0 事实源优先级(AGENTS.md 第 2 位 / 其他专题文档第 5 位)确立。
   删后该权威声明仍在 verifier-map.md 单点存在,未丢信息。
3-4. 上述两条删除后 §9.5 只剩单元素 bullet 列表(格式噪声),把该 bullet 并入
   上一段,省掉 bullet 行与其前空行。

保留 §9.5 的 verifier-map 指针行本体,check_verifier_map_owner 仍通过。

门禁: verify-doc-ssot.py PASS(AGENTS.md 反引号路径 95→96 全部存在);
wc -l AGENTS.md = 284 ≤ 300

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
@DeliciousBuding
DeliciousBuding force-pushed the docs/external-doc-honesty branch from ac0b714 to b017f0f Compare September 2, 2026 18:17
@DeliciousBuding

Copy link
Copy Markdown
Collaborator Author

主机侧合并前补记:分支已 rebase,正文两处 sha 引用订正

正文写的是「HEAD = 8f2ade25,base = dc7df53d」与「push 后 master 又前进了一次,本分支故意没有再 rebase(供主机侧合并时裁决)」。实况是:本仓 required_status_checks.strict: true,master 由 dc7df53d 连续前进到 94aef98a#2234 / #2235 / #2236 / #2239 / #2240)后本 PR 转 BEHINDbehind 阻塞合并,所以「不 rebase」这个选项不成立——分支已按纪律 rebase,HEAD 现为 b017f0f4,base 94aef98a(behind 0)。正文那段与门禁表抬头已相应订正后写入 squash commit body,避免把过期事实带进 master 历史。

按正文自己给的条件重跑并复现(rebase 后必须重证的部分):

结果
写集是否因 rebase 变化 零变化git diff --stat 8f2ade25 b017f0f4 -- 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 个文件零重叠
commit 对应 7 个 commit subject 逐条一一对应(733dee2c..8f2ade25bc7aea9e..b017f0f4
scripts/verify/verify-doc-ssot.py doc SSOT ok(verifier-map 66 script paths / 58 CI files;AGENTS.md path check 96 paths;script mirror ok)
wc -l AGENTS.md 284(≤300 行数预算,与正文预期一致)
git diff --check 干净
CI run 33666303378(HEAD 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

正文里所有本地实测结论因写集零变化而原样成立。合并为 squash,标题按仓库约定补 (#2154)(#2237) trailer。

@DeliciousBuding
DeliciousBuding merged commit c87178b into master Sep 2, 2026
39 checks passed
@DeliciousBuding
DeliciousBuding deleted the docs/external-doc-honesty branch September 2, 2026 18:30
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