diff --git a/r19-iteration-review/00-gap-review-2026-07-17.md b/r19-iteration-review/00-gap-review-2026-07-17.md new file mode 100644 index 00000000..9400bbde --- /dev/null +++ b/r19-iteration-review/00-gap-review-2026-07-17.md @@ -0,0 +1,225 @@ +# R19 卡点复审报告(2026-07-17,基线 main 33f52b16) + +## ① 执行摘要 + +本轮对 WorkHub 三端(桌面工作台 / web SPA / API+原生壳)做「前端有设计后端没接 / 后端有设计前端没体现 / 应该有但没有」三视角的卡点复审:64 条原始发现经跨维度去重、逐条对抗核实(驳回 10 条捏造/夸大),再经负责人终审与外部交叉审查(codex)撤销 3 条误报(R19-14/R19-17/R19-38)、修正 2 条范围(R19-4/R19-37,见文末记录),最终确认 **44 条真发现,无 high,23 medium,21 low**。注意:「无 high」仅指本轮产品缺口维度的结论,不构成项目整体判断——同基线的 codex 系统审查另确认 2 项 P0 安全缺陷与 12 项 P1(安全/并发/崩溃恢复维度,本轮未设),整体结论为 HOLD,合流见 01-merged-fix-plan 与 02-ssot。主线结论:R15-R18 的后端能力大量落地,但**接线缺口集中在两处**——(1)一批治理/协作类后端端点(OKR 创建、预算策略编辑、AI 改动回滚、团队邀请、指派/认领、项目归档)**在任何前端都无入口**,后端算了没人用;(2)R15 主动性管线(追DDL/关怀/找人卡)**投递成功却没有任何端能提示**,桌宠、rail 红点、OS 通知三个呈现端都只吃 `/stream/me` 的 `notification.created`,而主动会话通道刻意不产通知,导致核心的「主动把事顶到用户面前」在最常见场景(有个人空间但没盯着看)下静默落空。此外原生壳缺 `RunEvent::Reopen`、locale 运行时更新、通知点击深链等 macOS 标准行为,以及数据层有 9+ 张建了从不读写的孤儿表。无 high 意味着没有主流程当场撞死或产错数据,但 medium 群里多条是「用户能看见能力却够不到」「同一事实两套自相矛盾的真相」,属会被反复撞到的残缺与误导。 + +| 类别 | medium | low | 小计 | +|---|---|---|---| +| 前端有设计后端没接(fe-no-backend) | 1 | 1 | 2 | +| 后端有设计前端没体现(be-no-frontend) | 4 | 7 | 11 | +| 应该有但没有(missing) | 7 | 6 | 13 | +| 数据流与一致性(dataflow) | 8 | 6 | 14 | +| UX 打磨(ux) | 3 | 1 | 4 | +| **合计** | **23** | **21** | **44** | + +--- + +## ② 追踪清单 + +### 2.1 前端有设计后端没接 + +| 编号 | 标题 | severity | 证据 file:line | 现状与缺口 | +|---|---|---|---|---| +| R19-5 | 学到的自动通过策略三端不可撤销 | medium | packages/ui/src/gold-path/route-components.ts:4256 / apps/web/src/browser.ts:1103 | web 撤销按钮 `data-requires-desktop` 直接 preventDefault 指向桌面,桌面 grep 无任何渲染/撤销 permission_policies 的代码,DELETE /api/permissions/:id 成三端够不到的治理死胡同。 | +| R19-33 | 设置里「Cuu 主动性」三档是死控件 | low | apps/desktop-webview/src/spotlight/views/settings.ts:149 / apps/api/src/services/proactive-intents.ts:161 | chip 可点且写库、文案承诺「安静=很少主动开口」,但全库无任何行为消费 `cuu_proactivity`,选安静照样收全量主动打扰,调了等于没调。 | + +### 2.2 后端有设计前端没体现 + +| 编号 | 标题 | severity | 证据 file:line | 现状与缺口 | +|---|---|---|---|---| +| R19-1 | Objectives(OKR)创建+关联端点前端不可达 | medium | apps/api/src/routes/objectives.ts:52,81 / route-components.ts:3571 | POST /objectives 与 /:id/link 带完整校验,但 SDK 无方法、三端零调用者,objective 只被只读展示,用户看得见目标却无从创建或挂链。 | +| R19-2 | AI 预算策略查看+编辑无 UI | medium | apps/api/src/routes/cost.ts:82,89 / packages/api-client/src/client.ts:573 | GET/PUT /cost/policies(admin)做原子策略+审计写,SDK costPolicies/updateCostPolicy 零非测试调用者,管理员无 UI 可设/改上限、告警比、耗尽行为。 | +| R19-3 | 「撤销 AI 文件改动」(revert)无按钮 | medium | apps/api/src/routes/audit.ts:106 / packages/ui/src/replay/render.ts:1 | POST /agent-runs/:id/revert 完整实现文件回滚+审计对账(本地门控),但无 SDK 方法、全端无调用者,replay 只读渲快照,用户能看改动却无法回滚。 | +| R19-4 | 团队邀请生命周期半接线(接受/撤销/桌面缺失)——范围已由负责人修正 | medium | apps/api/src/routes/auth.ts:576 / packages/db/src/repositories/invites.ts:31 / apps/web/src/browser.ts:2849 | 修正:web 创建+待接受清单已在(R18-H1)。真实缺口=受邀者无接受落地页(POST /invites/accept 零前端入口)、revoke 函数无路由、桌面无邀请入口;另 codex P1-05 证实创建后 token 被列表刷新立即抹掉。 | +| R19-26 | 设置 VM budgets(8 项上限)无渲染 | low | apps/api/src/pages/settings.ts:62 / route-components.ts:4217 | 后端填 8 项 token/成本上限进 SettingsPageVM.budgets,web/桌面设置组件均不渲,是死负载。 | +| R19-27 | 工作项跨 run 审计时间线端点从不渲染 | low | apps/api/src/routes/audit.ts:80 / apps/api/src/routes/pages.ts:666 | GET /workitems/:id/audit 聚合全 run 快照+审计+manifest_facts,但无 SDK、无调用者,客户端只取 per-run replay。 | +| R19-28 | 合并融合 choose/preview 步骤从不被调用 | low | apps/api/src/routes/proposals.ts:1062 / packages/api-client/src/client.ts:497 | /merge-proposals/:id/choose 支持选/预览候选,两端只直接 apply,SDK chooseMergeProposalCandidate 零调用,无法预览备选融合结果。 | +| R19-29 | 三个冗余死读端点无前端消费 | low | apps/api/src/routes/agent-runs.ts:362 / ai-worklog.ts:25 / proposals.ts:1044 | handoff / ai-worklog·today / list-work-item-proposals 数据已由页面/live VM 交付,专用端点+SDK 桩无调用者,死冗余。 | +| R19-30 | 增量 trace 游标(?after=)前端从不传 | low | apps/api/src/routes/agent-runs.ts:331 / packages/ui/src/agent-run/render.ts:125 | trace 端点支持游标增量,前端一律从 VM 渲或订 SSE,分页参数未被消费。 | +| R19-31 | 军团后台主动性透明窗口桌面独占且单薄 | low | apps/desktop-webview/src/workbench/army/render.ts:440 / route-components.ts:5346 | 主动性动态只渲 kind/stage/状态、丢弃已取的 workItemId,无正文/跳转/动作;/api/army/background 有端点但 web 无渲染。 | +| R19-32 | web 找人卡渲成标题张冠李戴的只读摘要 | low | route-components.ts:5176 / apps/api/src/services/proactive-find-owner-card-delivery.ts:74 | mirrorActionCardHtml 对所有 action_card 硬套「Cuu 从讨论里拎出 N 件事」标题+无按钮 chip,找人 decide 卡在 web 无 claim/reassign/defer。 | + +### 2.3 应该有但没有 + +| 编号 | 标题 | severity | 证据 file:line | 现状与缺口 | +|---|---|---|---|---| +| R19-15 | web 端无进入个人空间/私聊的导航 | medium | apps/web/src/routes.ts:187 / packages/ui/src/gold-path/product-shell.ts:270 | 个人空间仅桌面 rail 有入口,web 外壳零个人空间/DM/会话导航,只有需已知 id 的只读镜像,web 用户够不到投给自己的主动消息。 | +| R19-16 | 隐藏的主窗无法点 Dock 图标恢复 | medium | client-tauri/src-tauri/src/main.rs:1515,1424 / browser.ts:1235 | 主窗 decorations:false + 关闭即 hide,`.run(generate_context!())` 无 RunEvent 回调,`Reopen` 未处理,点 Dock 图标无反应,只能靠托盘/深链/通知恢复。 | +| R19-18 | 无人发起的「指派/认领工作项」动作 | medium | packages/db/src/repositories/work-items.ts:310 / apps/api/src/routes/workitems.ts:167 | work_item_assignments 生产零 INSERT、claimedByUserId 唯一写口只服务找人卡,POST /workitems 不收 assignee,归属只能靠系统主动发找人卡被动落地。 | +| R19-19 | 项目无关闭/归档/删除 | medium | apps/api/src/routes/projects.ts:35 / packages/db/src/repositories/projects.ts:92 | archived/deletedAt 列全程只被 WHERE 过滤、无写 true 路径,路由仅 GET / + POST /bootstrap,项目收尾无落点、列表无限堆积。 | +| R19-20 | 文件在线预览只支持纯文本 | medium | apps/api/src/services/drive-pages.ts:230 / apps/api/src/routes/drive.ts:390 | 预览闸门只放行文本族 mime,图片/PDF/Office 三个预览端点一律 415「请下载查看」,以文件为核心的产品无法内联审阅设计图/PDF。 | +| R19-21 | 无工作区级审计/操作日志界面 | medium | packages/db/src/repositories/audit.ts:54 / apps/api/src/routes/audit.ts:80 | 审计仓库只有 forEntity/forWorkItem,路由仅 GET /workitems/:id/audit,写审计带 workspaceId 但无汇总读出口,治理审计只能逐工作项翻。 | +| R19-22 | 工作项无人对人评论/讨论区 | medium | packages/db/src/schema/core.ts:937 / apps/api/src/routes/approvals.ts:200 | 通用 comments 表(work_item_id)全库无任何 insert/select 为死表,人对人评论只覆盖审批与网盘文件,核心协作单元缺最基本讨论串。 | +| R19-41 | 桌面缺自动更新/开机自启/崩溃上报 | low | client-tauri/src-tauri/Cargo.toml:11 / tauri.conf.json:62 | 无 updater/autostart/log 插件、无 updater 端点、无 panic hook,错误 eprintln 到 stderr 在 .app 里不可见,常驻主动/SSE 循环无法随 OS 自启。 | +| R19-42 | 外壳声明拥有 local_file_sync 却无实现 | low | client-tauri/src-tauri/src/lib.rs:31 / docs/workhub/00-overview/glossary-dejargon.md:85 | RUST_SHELL_OWNS 列出 local_file_sync,但 spec_watch.rs 不存在、src 无任何 watcher/sync 代码,契约与文档陈旧误导。 | +| R19-43 | 全产品无数据导出(CSV/报表) | low | apps/api/src/routes/cost.ts:139 / apps/api/src/pages/settings.ts:62 | 成本/用量/审计/工作项均无导出端点,预算只读展示,管理员做核算只能截屏抄数。 | +| R19-44 | 通知偏好只有「按类型静音」一个旋钮 | low | apps/api/src/routes/notifications.ts:26 | 偏好 schema 仅 muted_notification_types,无渠道/免打扰时段/按项目粒度,R15 主动推送上线后颗粒度过粗。 | +| R19-45 | 用户只能停用不能恢复 | low | apps/api/src/routes/auth.ts:796 | 只有 deactivate 端点、无 reactivate/restore,停用为不可逆终态,误操作/回归无法撤销。 | +| R19-46 | 项目无成员/访问控制 | low | apps/desktop-webview/src/workbench/settings/render.ts:319 / packages/db/src/schema/core.ts:326 | 无 project_members 表,访问控制只到工作区级+协同参与者级,做不出保密/受限项目、无法从单项目移除某人(属设计取舍但真实缺口)。 | + +### 2.4 数据流与一致性 + +| 编号 | 标题 | severity | 证据 file:line | 现状与缺口 | +|---|---|---|---|---| +| R19-6 | 成本页 by_workitem/trend/by_team 后端填了 web 不渲 | medium | apps/desktop-webview/src/spotlight/views/dashboards.ts:480 / route-components.ts:4107 | 桌面显式把用户导向网页成本页看逐事项花费,web 成本组件无 by_workitem 卡、trend 只渲裸计数、by_team admin 聚合无处渲,跨端承诺断裂+死负载。 | +| R19-7 | 人工协作工作台三张表全孤儿 | medium | packages/db/src/schema/core.ts:646,665,681 | work_item_workspaces/_items/progress_updates 建模齐全带 FK+索引,repositories/contracts/apps 全库零读写、连 relations 都无,被设计从未接线。 | +| R19-8 | 主动 Cuu 消息投递后无任何端提示 | medium | apps/api/src/services/proactive-cuu-delivery.ts:110 / apps/desktop-webview/src/desktop-cuu-runtime.ts:61 | 主动消息只广播 conversation 话题、刻意不产 notification、不发 topics.user,桌宠/rail/OS 三端只吃 /stream/me,截止提醒与关怀被静默塞进用户可能几天不开的空间。 | +| R19-9 | 项目树主区/协同未读只本地+1,重连不补缺 | medium | apps/desktop-webview/src/workbench/shell.ts:784,665 / chat/stream.ts:7 | 权威 unread 只在 selectProject 拉 VM 时到手,meStream 无 onReconnected、broker resume_mode:fresh 不回放,同项目内漂过的通知永久少算(仅 DM 有 30s 兜底)。 | +| R19-10 | 读完会话只推读游标不清 notification 未读 | medium | apps/api/src/routes/conversation-read.ts:42 / apps/desktop-webview/src/browser.ts:1382 | advanceReadCursor 不碰 notifications,而 Dock 角标=approvals+counts.unread,读完会话红点归零但 Dock/通知中心仍虚高,两套自相矛盾的未读真相。 | +| R19-11 | 在线状态两套各自轮询来源 | medium | apps/desktop-webview/src/workbench/chat/view.ts:658 / rail.ts:1047 | 聊天条私有 onlineUserIds(自带 30s+重连刷)从不回写 store,rail/资料卡读 store.onlineUserIds,同一人两处圆点最长差 30s。 | +| R19-12 | OS 通知点击不打开目标页 | medium | client-tauri/src-tauri/src/notify.rs:126 / apps/desktop-webview/src/pet-surface.ts:1545 | 通知只 .show() 无 action/click handler,路由在创建时随事件发出、唯一生产绑定漏传 onSystemNotification,点击紧急审批通知无法深链,主动推送→行动环断头。 | +| R19-13 | 原生壳 locale 启动即冻结 | medium | client-tauri/src-tauri/src/main.rs:1432,1494 / browser.ts:1327 | WorkHubLocale 仅 setup 写一次、无 setter 命令,webview 切语言只写 localStorage+reload 且无 fs 能力,切英文后托盘/通知仍中文且重启后不一致。 | +| R19-34 | spec_docs 表从不写入,current_spec_id 恒空 | low | packages/db/src/schema/core.ts:1123,336 | spec_docs 全仓无 insert/select,work_items.current_spec_id 只被读进 DTO、无写点,契约导出字段恒 absent。 | +| R19-35 | background_jobs 表从不写入,三处 job_id 恒 NULL | low | core.ts:280,391,590 | 建模了持久化异步队列但零操作,knowledge_ask_runs/meeting_records/task_plans 的 job_id 外键只能 NULL,异步流程实际靠各自 status 列自管。 | +| R19-36 | knowledge_ask_runs 表从不落库 | low | core.ts:384 | 建了 question/answer_md/citations/trace 四索引,全仓零读写,知识问答走事件即时计算,用户拿不到问答历史/引用溯源。 | +| R19-37 | 三张遗留 schema 死表(范围已修正) | low | core.ts:914,877,937 | deliveries/attachments/comments 生产零读写;原列入的 user_profiles 有真实仓库+GET/PATCH 路由,系误报已移除(codex 反馈)。 | +| R19-39 | step.snapshot 被桌面 run-stream 白名单丢弃 | low | packages/agent/src/loop/loop.ts:907 / desktop-cuu-runtime.ts:1401 | loop 发 stepSnapshot,web 全量订阅能收,桌面具名白名单含 stepToolResult 却漏 stepSnapshot,快照落点桌面实时视图收不到、只能事后 replay。 | +| R19-40 | 全新私聊要等 30s 轮询才进左栏 | low | apps/desktop-webview/src/workbench/shell.ts:707 / rail.ts:205 | me 流对未在 dmList 的会话 bumpDmUnread 是空操作、不补拉列表,normal DM 又不出 OS 通知,首条私聊左栏最长 30s 盲区。 | + +### 2.5 UX 打磨 + +| 编号 | 标题 | severity | 证据 file:line | 现状与缺口 | +|---|---|---|---|---| +| R19-23 | 工作台网盘删除无确认/无回执/无回收站 | medium | apps/desktop-webview/src/workbench/drive/view.ts:158 / drive/render.ts:112 | 删除按钮裸写「删除」一点即删、无确认,列表只渲 folders+files 从不渲 deleted_items,restoreDriveItem 虽 Pick 进客户端却全工作台无调用点,删错文件在工作台内无恢复路径(对照 web「移到回收站可恢复」)。 | +| R19-24 | web 移出成员/退群/工作区移出均无二次确认 | medium | apps/web/src/browser.ts:2765,2963 / chat/render.ts:241 | web 点退出/移出直接发 DELETE、自退不可逆还提示「刷新后无法再看到它」,而桌面同操作有两段式确认,护栏在 web 整体丢失。 | +| R19-25 | 工作台网盘行是不可聚焦 div,键盘无法打开/进子文件夹 | medium | drive/render.ts:96 / files/render.ts:102 | 行为无 role/tabindex 的 div、view.ts 无 keydown,文件夹行常无按钮,键盘用户完全够不到进入子目录(同库 Files 面板用真 button,端内不一致)。 | +| R19-47 | 军团 oldest_blocker 后端给了 href,web 渲成不可点灰字 | low | apps/api/src/pages/agent-army.ts:283 / route-components.ts:3576 | 后端填 href:'/attention',web 只渲 label 成 `
` 丢 href,「有子任务卡住等你」成死文本而非可点入口。 | + +--- + +## ③ 高危与中危逐条展开(无 high,以下 23 条为 medium) + +**R19-1 Objectives(OKR)创建+关联端点前端不可达。** POST /api/objectives(带 key_results 校验)与 POST /:id/link(assertCanMutateWorkItem 门控)在 objectives.ts:52/81 完整实现,但 api-client 无对应 SDK 方法、web/desktop/ui 全量 grep 零调用者。用户在军团计划卡和成本页只能看到 objective 标题+进度%的只读标签(route-components.ts:3571/4034),却永远无法创建一个目标或把工作项挂到目标上——OKR 概念被吊在半空。**修法**:在工作项详情面加「创建目标 / 关联目标」表单,配套新增 SDK 方法;若 OKR 尚未打算发布,则删掉只读标签避免吊胃口。 + +**R19-2 AI 预算策略查看+编辑无 UI。** GET/PUT /api/cost/policies(cost.ts:82/89,admin-only,原子策略+审计写)与 SDK costPolicies/updateCostPolicy(client.ts:573)存在且有 smoke 覆盖,但零非测试调用者。成本页只读渲用量、从不暴露可编辑策略对象,管理员无从设/改预算上限、告警比、耗尽行为。**修法**:在 cost/settings 面加 admin 预算策略编辑器(列表+按 scope 编辑表单),接 costPolicies/updateCostPolicy。 + +**R19-3 「撤销 AI 文件改动」(revert)无按钮。** POST /api/agent-runs/:id/revert(audit.ts:106)完整实现文件快照回滚+审计对账+写 snapshot.reverted 条目,本地客户端门控。但无 SDK revertAgentRun、desktop/web/ui/tauri 全端无调用者,replay 只只读渲快照。用户能看见 AI 改了什么却永远无法在产品内回滚。**修法**:在桌面 replay/快照面加「撤销此改动」(须跑本地客户端)+确认步,接该端点。 + +**R19-4 团队邀请生命周期半接线(负责人修正范围)。** 原发现称「三端 grep invite 全空」有误——web 邀请创建+待接受清单已由 R18-H1 交付(browser.ts:2849 起,仅密码模式启用)。复核后的真实缺口:①受邀者侧无接受落地页,POST /invites/accept(auth.ts:576)零前端入口,token 只能靠手工 curl 兑换;②invites 仓库的 revoke(:31)没有任何路由暴露,管理员撤不掉已发邀请;③桌面端无邀请入口;④外部审查(codex P1-05)证实创建成功后 `showToken` 紧跟 `hydrate()`,列表重渲用 `body.innerHTML` 重建把一次性 token 数百毫秒内抹掉,用户几乎无法复制。**修法**:接受落地页(可深链)+撤销路由与按钮+token 展示区移出列表重渲域(或 hydrate 前置),桌面端复用。 + +**R19-5 学到的自动通过策略三端不可撤销。** web 设置渲每条自动通过策略的「撤销」按钮带 `data-requires-desktop=true`,browser.ts:1103 在此直接 preventDefault 弹「需在桌面端操作」,从不发 DELETE;而 desktop-webview 与 client-tauri 全库 grep 无任何渲染 permission_policies 或调用撤销的代码(仅测试命中),api-client 也无对应方法。用户能创建常驻自动通过规则,却在任何客户端都撤不掉——治理层死胡同。**修法**:在桌面端补策略列表+撤销控件接 DELETE /api/permissions/:id,并给 api-client 加方法;否则 web 的指路文案是在指向不存在的能力。 + +**R19-6 成本页三维度后端填了 web 不渲。** 桌面 spotlight 成本视图渲 by_workitem 前 5 条并打文案「还有 N 个事项的花费,去网页版成本页细看」(dashboards.ts:480),把用户显式导向 web;但 web 成本组件装配段(route-components.ts:4107-4135)与 React props 均无 by_workitem,trend 只渲一个「趋势:N」裸计数,by_team(admin 聚合)在两端都无处渲。用户按提示跳到 /dashboard/cost 找逐事项明细,那里根本没有——断掉的跨端承诺+死负载。**修法**:web 补「按事项花费」卡渲 vm.by_workitem(admin 可见),或把桌面指路改到确实展示该维度的位置。 + +**R19-7 人工协作工作台三张表全孤儿。** work_item_workspaces(phase/progress/status_note/blocked_reason)、_items(todo+sort_order)、_progress_updates(actor/kind/body 活动流)在 core.ts:646/665/681 建模齐全带 FK+索引,但 apps/repositories/contracts 全部零引用、连 relations 都没声明。任何读 schema 做规划的人会以为进度/协作工作台是真的,实际前后端都不存在。**修法**:确认废弃后从 schema+PG 删除,或补 repository/service/UI 真正落地这套协作进度追踪。 + +**R19-8 主动 Cuu 消息投递后无任何端提示。** ddl-chase 对有责任人工作项的 T-1d/逾期两档、以及全部关怀消息,走 conversation_message 通道由 proactive-cuu-delivery 直接落进个人空间主区,并只向该会话话题广播(delivery.ts 头部明说刻意不落 notification、不发 topics.user)。而桌宠 runtime(只订 /stream/me)、rail 红点(只靠 notification.created 本地+1)、OS 通知三端都吃不到会话话题。用户建过个人空间后,「明天就到期了」「已经逾期了」和关怀被静默塞进一个他可能几天不开的空间,桌宠一声不吭;逾期档的 24h 重复叮嘱在会话通道下天然不生效、suppression_key 保证每件只发一次,连补救的重复提醒也没了——追DDL 的核心目的在最常见场景下完全落空。消息已落库、进空间即可见,故非数据丢失,列 medium。**修法**:主动会话投递成功后额外向 topics.user 广播一条轻量「你的空间有新动态」信号(可做成桌宠轻点头/rail 个人空间行红点),或让桌宠/rail 订阅当前用户个人空间会话话题;至少让逾期档保留可感知的重复提醒。 + +**R19-9 项目树主区/协同未读只本地+1,重连不补缺。** 权威 unread_count 只在 selectProject 拉 workbench VM 时到手(shell.ts:784 唯一调用点),停在同项目期间树叶角标只靠 /me 流 notification.created 本地+1,既无周期重拉也不在重连后补缺(meStream 无 onReconnected,broker resume_mode:fresh 不回放)。rail 30s 轮询只 loadDms、只自愈 DM 未读——注释宣称的「会话列表兜底刷新」对主区/协同叶是空头支票。项目 P 里协同会话 B 收消息时你正开着 A → B 叶+1;期间 /me 流抖动重连漏掉若干通知,B 角标永久少算,只有切走再切回才被新 VM 纠正。DM 能自愈、主区/协同不能。**修法**:给 workbench VM 加 30s/重获焦点重拉,或给 meStream 配 onReconnected 走未读聚合补缺口,让主区/协同叶与 DM 同源自愈。 + +**R19-10 读完会话只推读游标不清 notification 未读。** 每条未读会话消息落一条 severity=normal 的 conversation.message 通知(计入 counts.unread),但工作台打开会话读消息只调 PUT /read→advanceReadCursor(推游标+发 read.updated),完全不碰 notifications。而 Dock/托盘角标=approvals+counts.unread(browser.ts:1382)。别人发你几条消息 → Dock+N、通知中心列 N 条;你在工作台读完 → rail 叶红点归零、别人看到你「已读」,但 Dock 仍显示 N、通知中心仍标未读,非得去主窗手动标已读才消。同一批消息两套矛盾的未读真相。**修法**:读游标推进到会话末尾时,服务端按 conversation_id 批量标该用户该会话的通知已读(或从 counts.unread 口径扣除已读会话),让 Dock/通知中心与读游标同源。 + +**R19-11 在线状态两套各自轮询来源。** chat 视图维护私有 onlineUserIds(view.ts:658)由自己的 30s loadPresence 刷+SSE 重连即时补,聊天成员条/DM 头绿点用它;rail 名册/私聊行/资料卡(profile-popover.ts:49)用另一套 store.onlineUserIds 由 rail 独立 30s 写入。两个消费者各打 GET /presence、相位不同,chat 重连后的更鲜活在线态从不回写 store。对方刚上线,DM 里已显在线,左栏那条 DM 行/资料卡还停在离线,最长差 30s(重连后更久)。同一人同一时刻两颗矛盾的在线点。**修法**:统一 presence 单一来源(chat 刷到的也写回 store,或都从 store 读)。 + +**R19-12 OS 通知点击不打开目标页。** Rust SSE worker 用 notifications.builder().show()(notify.rs:126)显示紧急通知,无 action id/click handler;精心算出的目标路由+window_control 只随 sse_worker.rs:235 的 system-notification 事件走,而唯一生产绑定 pet-surface.ts:1545 未传 onSystemNotification(主窗入口根本不绑 runtime),路由被丢到 no-op。点击「Cuu 需要你审批」通知不深链到审批,主窗又隐藏且无 Reopen,什么都不出现,主动推送→行动环断头;且路由只在创建时交付、不在点击时,无法代表用户点击。**修法**:用 tauri-plugin-notification 的 action/permissions 接收点击事件(或把 plan.route 登记为 pending 深链),点击时 show_main_window+导航到 plan.route。 + +**R19-13 原生壳 locale 启动即冻结。** WorkHubLocale mutex 仅 setup 从 config/env 写一次(main.rs:1433),托盘标签+tooltip 据它构建一次,invoke_handler(1494-1514)无 locale setter,current_workhub_locale 永远返回启动值,驱动托盘文字、深链报错、通知 fallback。webview 切语言只写 localStorage+reload(browser.ts:379)且无 fs 能力(capabilities 仅 core:default),到不了壳。切英文后托盘/tooltip/通知仍中文,且两侧持久化方式不同(config JSON vs localStorage),重启后不一致直到手改 config。**修法**:加 set_shell_locale 命令更新 state 并重建/重标托盘菜单+tooltip,webview 切语言时调它(并/或把 locale 写进 workhub-shell-config.json)。 + +**R19-15 web 端无进入个人空间/私聊的导航。** 个人空间(主动消息落点)只有桌面 rail 有创建入口和导航,web 是项目中心式外壳,product-shell/render 完全无个人空间/私聊/会话列表导航,只有需预知会话 id 的 /conversations/:id 只读镜像。桌面建过个人空间、日常用 web 的用户,其追DDL 与关怀被投进 web 无法导航到的空间;会话通道又不产通知(无 targetUrl),web 用户永远看不到。**修法**:web 外壳补「我的空间/主动动态」入口(列出个人空间主区会话、复用 /conversations 镜像)。 + +**R19-16 隐藏的主窗无法点 Dock 图标恢复。** 主窗 decorations:false + 自定义关闭按钮/Cmd+W 都拦成 hide(用户常规隐藏),但 main() 以 `.run(generate_context!())`(main.rs:1515)结束、无 RunEvent 回调,macOS `RunEvent::Reopen`(Dock 图标点击)从不处理。隐藏后点 Dock 图标——最自然的 macOS 恢复方式——毫无反应,只能靠托盘/深链/通知。**修法**:改 `.build(context)?.run(|app, event| …)` 处理 `RunEvent::Reopen` 调 show_main_window(show+set_focus)。 + +**R19-18 无人发起的「指派/认领工作项」动作。** 人类归属只有 work_item_assignments(多角色)与 claimedByUserId(单认领人)两列;前者生产零 INSERT(唯二 insert 在测试),富模型是死列;后者唯一写口 claimOwnerlessWorkItem 只被 R15 找人卡的 decide 调用,action-cards 路由只暴露 /decide 与 /undo,找人卡本身只由主动扫描管线生成,POST /workitems 也不收 assignee。经理落地计划草案时无法「指派给张三」,成员无法从待领池主动认领,只能等系统碰巧发找人卡才谈归属。**修法**:补人发起的 POST /workitems/:id/assign 与 /claim,在工作项详情/看板卡给按钮;或至少允许经理在计划草案 materialize 时为每条选负责人。 + +**R19-19 项目无关闭/归档/删除。** projects 表预留 archived+deletedAt 列,列表/时间线/网盘全在 WHERE 过滤它们(说明设计预期可归档),但生产无任何路径写 true(唯二在测试),路由只有 GET / + POST /bootstrap,仓库写方法只有 bootstrap*/updateInstructions。经理做完项目想归档、或删掉建错的项目,前后端都无从下手,列表只会无限增长(project-home-pages.ts:176 注释自认 archived 分支「暂不可达」)。**修法**:补 POST /projects/:id/archive 与 /delete(软删),项目主页/设置给按钮,列表加已归档分区。 + +**R19-20 文件在线预览只支持纯文本。** 预览闸门 isTextPreview/isDriveTextPreview 只放行 text/*、json/xml/yaml 及 .csv/.md/.txt 等(drive-pages.ts:230),三个预览端点(网盘 drive.ts:390、交付物 workitems.ts:227、提议变更 proposals.ts)同闸门,非文本一律 415「请下载查看」。而系统本身认识 image/png、application/pdf 等 mime(用于下载头),这些文件真实存在于网盘/交付物。以文件为核心、AI 军团产出交付物的产品,经理审阅设计图/PDF、成员看图片交付物都被迫下载再打开。**修法**:至少为 image/* 与 application/pdf 加内联预览,预览端点按 mime 分派 preview_type 而非只 text。 + +**R19-21 无工作区级审计/操作日志界面。** 审计仓库只有 listAuditLogsForEntity 与 listAuditLogsForWorkItem(audit.ts:54/55),无按工作区/用户/时间范围的列表方法,路由只暴露 GET /workitems/:id/audit。而写审计带 workspaceId(谁邀谁、谁改谁角色、谁批哪次合并、谁停用谁都落审计),数据在但无汇总读出口。管理员想纵览治理审计,只能一个工作项一个工作项翻。**修法**:补 listAuditLogsForWorkspace(actor/action/时间范围过滤)+ 管理员审计页(时间倒序展示全工作区关键操作)。 + +**R19-22 工作项无人对人评论/讨论区。** schema 有一张显然为工作项讨论准备的通用 comments 表(work_item_id+author_nickname+body,core.ts:937),但全库无任何 insert/select(comment 读写都在 project_drive_comments 与 approval_comments 两张不同的表)。人对人评论只存在于网盘文件(主要用于触发 AI 起草)与审批。成员领任务后想在任务下问同事一句、留进展备注,除了跟 Cuu 的 AI 会话外没有轻量的任务级讨论串——核心协作单元缺最基本评论区。**修法**:接上 comments 表补 GET/POST /workitems/:id/comments 与工作项详情讨论串,或在契约明确废弃并说明讨论只在会话进行。 + +**R19-23 工作台网盘删除无确认/无回执/无回收站。** 删除按钮渲危险色、文案裸「删除」,点击直接 deleteDriveItem 无确认,成功只静默 load、无「已移到回收站可恢复」回执;列表只筛 folders+files 渲染、从不渲 deleted_items,restoreDriveItem 虽 Pick 进客户端却全工作台无调用点(唯一真实调用在 spotlight)。工作台删错文件既无撤销也找不到恢复。与产品自身矛盾:同栏「版本回滚」反而两段式确认,web 删除按钮直接叫「移到回收站」且提示可恢复,Spotlight 删除 toast 也说「在回收站可恢复」——唯独主力界面工作台把最破坏性的删除做成无确认/无回执/无恢复+误导性「删除」字样。**修法**:工作台删除加确认步+「已移到回收站,可恢复」回执,渲 deleted_items 恢复区接 restoreDriveItem,并改文案与 web 对齐。 + +**R19-24 web 移出成员/退群/工作区移出均无二次确认。** web 会话镜像点「退出」直接 doRemove(selfUserId,true)、点「移出」直接 doRemove(targetId,false),doRemove 二话不说发 DELETE,无确认;自退不可逆还提示「刷新后你将无法再看到它」。/settings 管理员控制台「移出」工作区成员同理点击即 DELETE /api/workspace/members/:id 把人踢出整个工作区,零确认。而桌面对同类操作有两段式确认(chat/render.ts:241「退出这个群?」「把 TA 移出?」)。web 把护栏整体丢了,破坏性操作与桌面不一致。**修法**:web 的退群/移出成员/工作区移出加二次确认对话框,与桌面两段式对齐。 + +**R19-25 工作台网盘行是不可聚焦 div,键盘无法打开/进子文件夹。** 每行渲成 `
` 包成指向 oldest_blocker.href 的链接。 | + +--- + +## ⑥ 负责人终审撤销与修正记录 + +对抗核实后仍有 3 条误报混入:前 2 条为负责人抽查原生壳簇时发现,第 3 条由外部交叉审查(codex)反馈后负责人复核坐实。编号保留不复用,防止对表错位: + +- **R19-14「托盘 tooltip/Dock 角标不反映待决策数」——撤销,能力已存在。** `set_shell_badge`(client-tauri/src-tauri/src/main.rs:537,注册于 :1795)同时设 macOS Dock 角标(`set_badge_count`)并以 `tray_tooltip_with_badge`(tray.rs:126,带测试)更新托盘 tooltip 计数;apps/desktop-webview/src/browser.ts:175 的 refreshApprovalsBadge 30s 轮询在真实调用。R15 批 A6 交付物。 +- **R19-17「Spotlight 式启动器无全局唤起热键」——撤销,能力已存在。** `tauri-plugin-global-shortcut = "2.3.2"`(Cargo.toml:19),main.rs:841 注册 Option+Space 唤起、:1668 挂插件。R15 热键批交付物。 +- **R19-38「三个 SSE 事件类型死枚举」——撤销,非新发现且修法违背既档约束(codex 反馈)。** enums.ts:165 起的注释块正是 R17 G2(#25 零生产者事件核查)所加的 `@deprecated` 标注,明文「枚举值一律保留——删值会破坏 contracts 兼容面(旧事件反序列化、跨版本客户端)」。R19-38 只是重新发现了这份已登记的兼容债务,其「从 eventTypes+toCuuState 删」的建议与既档约束直接冲突。 + +另有 2 条**范围修正**(不撤销): + +- R19-4 原称邀请「三端零接线」,实际 web 创建+清单 UI 已由 R18-H1 交付;修正为「接受落地页/撤销路由/桌面入口缺失 + token 被重渲抹掉(codex P1-05 交叉证实)」,详见 2.2 表与 ③ 展开。 +- R19-37 原列「四张死表」含 user_profiles,实际它有真实仓库(packages/db/src/repositories/user-profiles.ts)与 GET/PATCH 路由(apps/api/src/routes/user-profile.ts),已移除(codex 反馈);deliveries/attachments/comments 三张负责人复核确认生产零读写,维持。 + +同簇其余 5 条(R19-12/13/16/41/42)负责人逐条复核为真:无通知点击深链(pet-surface.ts:1601 的 bindDesktopShellCuuRuntime 生产调用漏传 onSystemNotification + notify.rs 无 click/action handler)、无 locale setter、`.run(generate_context!)` 无 RunEvent::Reopen、Cargo.toml 无 updater/autostart/log、lib.rs:31 声明 local_file_sync 但无实现文件。另抽查 R19-19(projects.ts 无 archive/delete 路由)、R19-22(drive 评论走 projectDriveComments,通用 comments 表全库零读写)均为真。 \ No newline at end of file diff --git a/r19-iteration-review/01-merged-fix-plan-2026-07-17.md b/r19-iteration-review/01-merged-fix-plan-2026-07-17.md new file mode 100644 index 00000000..d4d0c09b --- /dev/null +++ b/r19-iteration-review/01-merged-fix-plan-2026-07-17.md @@ -0,0 +1,70 @@ +# R20 合流修复计划(2026-07-17) + +两份独立审查对同一基线 main@33f52b16 的合流: + +- **R19 卡点复审**(本仓 00-gap-review-2026-07-17.md):9 维度 workflow + 对抗核实 + 负责人终审,45 条(0H/23M/22L),偏产品断链与 UX。 +- **codex 系统代码审查**(reports/WorkHub-系统代码审查-2026-07-17.md):三 lane 逐链深读,结论 HOLD,P0×2 / P1×12 / P2×11 / P3×2,偏安全边界、并发/崩溃窗口与可靠性。 + +**负责人独立核验**:codex 两条 P0 及承重 P1(P1-05/P1-10/P1-11)均亲手打开证据文件复核为真;P2-07/P2-08 与 R19-12/R19-24 互为独立重发现,交叉印证。**采纳 HOLD 结论:Phase 0 与 REL-1/2 收口前不扩大试用。** + +## 一、两报告交叉对表 + +| codex | R19 | 关系 | +|---|---|---| +| P2-07 OS 通知点击不消费 route | R19-12 | 同一发现,双方独立证实(生产绑定漏传 onSystemNotification + notify.rs 无 click handler) | +| P2-08 破坏性操作单击即提交 | R19-24(web)/R19-23(桌面网盘) | 同簇:web 缺二次确认,桌面网盘删除缺确认+回收站入口 | +| P1-05 邀请 token 被重渲抹掉 | R19-4(已修正) | 互补:R19-4 给出接受落地页/撤销路由/桌面入口缺口,codex 抓到已交付 UI 的生命周期 bug | +| P2-06 web 会话镜像无实时流 | R19-15(web 无个人空间导航) | 同簇「web 会话体验断链」,合并施工 | +| P2-05 设备管理无 UI | —(R19 漏) | codex 独有 | +| P1-06 普通成员进不了 Settings | —(R19 漏) | codex 独有 | +| P1-08 /api/users 全局目录当花名册 | —(R19 漏) | codex 独有 | +| P0/P1 安全、并发、崩溃恢复全系 | —(R19 维度未设) | codex 独有 | +| — | R19-8 主动消息三端静默 | R19 独有(主动性观感核心) | +| — | R19-1/2/3 OKR/预算策略/回滚无 UI | R19 独有 | +| — | R19-19/20/21/22 归档/预览/审计页/评论区 | R19 独有 | +| — | R19-7/34-37 数据层孤儿表 | R19 独有(R19-38 死枚举已撤销:R17 G2 既档兼容债务) | +| — | R19-13/16/41/42 原生壳 locale/Reopen/updater/契约 | R19 独有(codex P1-04 第二实例深链丢失为近邻) | + +结论:两报告重叠极小、互补性强。codex 管「地基与安全」,R19 管「可达性与体验」。 + +## 二、修复阶段(依赖顺序) + +### Phase 0 — 安全封口(HOLD 解除门槛,最先做,全部需真实集成测试) + +| 单 | 内容 | 来源 | 文件域 | +|---|---|---|---| +| SEC-1 | 成员移出 fail-closed:删无 membership 默认租户回退;移出=事务化撤销(membership+session/device+presence+SSE);identify 不自动补回被移出成员;补 remove→旧token→403 与 remove→identify→不恢复 测试 | P0-01 | apps/api middleware/auth.ts、services/workspace-members.ts、routes/auth.ts | +| SEC-2 | 身份代际+活跃流终止:Rust token state 加 generation/watch,清空或变更必中止当前 pump;服务端对撤销发布关流信号或心跳期重验 grant;logout 改有状态事务(修 P1-01 吞错);补 A登出→B登录→A事件不可达 测试 | P0-02+P1-01 | client-tauri sse_worker.rs/main.rs、apps/api sse/stream.ts、desktop-webview settings/boot | + +### Phase 1 — 可靠性:不丢任务、不失联(REL) + +| 单 | 内容 | 来源 | +|---|---|---| +| REL-1 | AgentRun 启动 drain:恢复 expired claims 之外同时 drain 既有 queued rows,含多实例 ownership | P1-10 | +| REL-2 | ProactiveIntent created 恢复:lease/attempt/next_attempt_at 或 outbox,created 视为可恢复态 | P1-11 | +| REL-3 | 并发闸:最后管理员 TOCTOU(workspace 级锁)+ 依赖 DAG 环(项目 advisory lock),并发事务测试 | P1-09+P1-12 | +| REL-4 | 桌面 SSE/poll 可恢复状态机:自制 EventSource EOF 重连+fallback polling 不永久停 | P1-03 | +| REL-5 | 桌面 password/hybrid 登录链路(后端已有,客户端零入口) | P1-02 | +| REL-6 | 第二实例深链 create-if-missing(与正常深链共享 handle_deep_link_plan) | P1-04 | + +### Phase 2 — 断链与数据真实性(双报告合流主战场) + +**2A api+SDK 打底**(解锁 2B/2C):workspace-scoped roster API(P1-08)、邀请撤销路由+接受契约(R19-4)、assign/claim(R19-18)、项目归档/删除(R19-19)、工作区审计端点(R19-21)、workitem comments(R19-22)、图片/PDF 预览(R19-20)、读游标清通知(R19-10)、主动投递补 user-topic 信号(R19-8)、rename 领域事件(P2-04);SDK:revertAgentRun/objectives/invites/devices 等(R19-1/2/3、P2-05)。 + +**2B web**:邀请 token 生命周期修复+接受落地页(P1-05+R19-4)、Settings 常驻+capability 统一(P1-06)、成员数改 roster(P1-08)、partial-failure 诚实态(P1-07)、会话镜像窄流订阅+seq 合并(P2-06)、个人空间入口(R19-15)、设备管理页(P2-05)、二次确认三处(P2-08/R19-24)、成本页三维度渲染(R19-6)、OKR/预算策略/审计页/评论区 UI(R19-1/2/21/22)。 + +**2C 桌面 webview**:通知点击深链 webview 半边(P2-07/R19-12)、权限策略撤销 UI(R19-5)、网盘删除确认+回收站(R19-23)+键盘可达(R19-25)、presence 单源(R19-11)、未读重连补缺(R19-9)、桌宠/rail 吃主动信号(R19-8 桌面半边)、AI 改动回滚按钮(R19-3)、新 DM 即时入栏(R19-40)。 + +**2D 原生壳(Rust,与 2C 命令桥协调)**:通知 click→深链原生半边、RunEvent::Reopen(R19-16)、set_shell_locale(R19-13)、updater/autostart/log+panic hook(R19-41)、local_file_sync 契约清理(R19-42)。 + +### Phase 3 — 一致性、工程质量与打磨 + +消息 outbox/replay(P2-01)、offboarding 可重入 job(P2-02)、审计写入不静默吞(P2-03)、语言偏好同步提示(P2-09)、头像弹窗焦点生命周期(P2-10)、Chrome QA 日志(P2-11)、cargo fmt/clippy 清零并进 CI(P3-01)、预算续租可观测(P3-02)、数据层孤儿表处置(R19-7/34-37,user_profiles 已剔除)、死端点/死参数清理(R19-29/30;SSE 事件枚举值按 R17 G2 既档约束一律保留)、R19 低危 UX 清单其余项。 + +### 需产品拍板(不盲建) + +R19 批 H 原样保留:通知偏好粒度扩展(R19-44)、用户 reactivate(R19-45)、项目级成员/访问控制(R19-46)、Cuu 主动性三档接频控(R19-33)、合并融合预览步(R19-28)。另:loop2 双开关翻 on(R18 冒烟已放行,等拍板)。 + +## 三、验收门槛(采纳 codex 第 12 节,全程适用) + +每个修复单至少一个「修复前稳定失败、修复后稳定通过」的根因测试;auth/membership/stream 必须真实身份轮换或真库集成测试;并发问题用并发事务复现;crash/restart 问题用「中间点退出+二次进程恢复」测试;禁止空 catch 吞错;`pnpm verify`+Rust test/fmt/clippy 全绿后才进双端手工验收。 diff --git a/r19-iteration-review/02-ssot-2026-07-17.md b/r19-iteration-review/02-ssot-2026-07-17.md new file mode 100644 index 00000000..43becbe3 --- /dev/null +++ b/r19-iteration-review/02-ssot-2026-07-17.md @@ -0,0 +1,131 @@ +# R20 问题总账 SSOT(2026-07-17) + +**唯一事实源声明**:两份审查(R19 卡点复审 / codex 系统审查,同基线 main@33f52b16)的全部发现以本文件为准;00-gap-review 与 codex 报告为证据底稿,01-merged-fix-plan 为阶段说明。处置状态、级别、归属如与其他文档冲突,以本文件为准。 + +**统计**:codex 27 项 + R19 终稿 44 条 − 双方重复合并 3 组 = **68 个独立问题**。另:R19 内部对抗核实驳回 10 条(见 findings.json rejected)、负责人撤销 3 条(R19-14/17/38)、范围修正 2 条(R19-4/37)。整体结论 **HOLD**(采纳 codex):Phase 0 收口前不扩大试用。 + +标记含义:`双方重复`=独立重发现已合并单;`Codex独有`/`Claude独有`=单方发现且对方复核或负责人核验通过;`待拍板`=需产品决策不盲建;`撤销`=复核不成立。级别:codex 项用 P0-P3,R19 项用 M(edium)/L(ow)。 + +## Phase 0 — 安全封口(最先,HOLD 解除门槛) + +| 来源 | 标题 | 级别 | 标记 | +|---|---|---|---| +| P0-01 | 成员移出不撤访问+昵称 identify 自动补回(fail-open 默认租户回退) | P0 | Codex独有,负责人亲核为真 | +| P0-02 (+P1-01) | 退出/换号后旧账号私有 SSE 继续 pump;logout 吞错伪装成功 | P0 | Codex独有,负责人亲核为真 | + +## Phase 1 — 可靠性(不丢任务/不失联) + +| 来源 | 标题 | 级别 | 标记 | +|---|---|---|---| +| P1-10 | 崩溃前 queued AgentRun 重启后永久卡住(recovery 只 drain requeued>0) | P1 | Codex独有,负责人亲核为真 | +| P1-11 | ProactiveIntent created 崩溃窗口永久去重,无恢复扫描 | P1 | Codex独有,负责人亲核为真 | +| P1-09 | 最后管理员保护 TOCTOU 并发竞态 | P1 | Codex独有 | +| P1-12 | 工作项依赖 DAG 无环保证可被并发击穿 | P1 | Codex独有 | +| P1-03 | 桌面自制 EventSource 断网后永久停更(EOF 不重连+polling 停摆) | P1 | Codex独有 | +| P1-02 | password/hybrid 模式桌面无登录链路 | P1 | Codex独有 | +| P1-04 | 第二实例 workbench 深链丢失(无 create-if-missing) | P1 | Codex独有 | + +## Phase 2A — API/SDK 打底(解锁 2B/2C) + +| 来源 | 标题 | 级别 | 标记 | +|---|---|---|---| +| P1-05 + R19-4 | 邀请生命周期:token 被 hydrate 重渲抹掉+无接受落地页+无撤销路由+桌面无入口 | P1 | 双方重复(互补合并单) | +| P1-08 | /api/users 全局目录被当工作区花名册(跨租户泄露+计数错) | P1 | Codex独有 | +| P2-04 | 会话 rename 无领域事件,跨端不同步 | P2 | Codex独有 | +| R19-8 | 主动 Cuu 消息投递后三端无提示(服务端补 user-topic 信号半边) | M | Claude独有 | +| R19-10 | 读完会话不清 notification 未读,多套未读真相 | M | Claude独有(codex 复核认可) | +| R19-18 | 无人发起的指派/认领工作项动作 | M | Claude独有 | +| R19-19 | 项目无归档/关闭/删除 | M | Claude独有(codex 复核认可) | +| R19-20 | 文件预览只支持纯文本(图片/PDF 415) | M | Claude独有(codex 复核认可) | +| R19-21 | 无工作区级审计端点/界面 | M | Claude独有 | +| R19-22 | 工作项无人对人评论(comments 表死) | M | Claude独有 | +| R19-43 | 无数据导出(CSV) | L | Claude独有 | + +## Phase 2B — web 前端 + +| 来源 | 标题 | 级别 | 标记 | +|---|---|---|---| +| P1-06 | 普通成员正常导航进不了自己的 Settings;capability 模型混用 | P1 | Codex独有 | +| P1-07 | 次要请求失败伪装成真实 0/空态 | P1 | Codex独有 | +| P2-05 | 设备管理 API 完整无任何 UI | P2 | Codex独有 | +| P2-06 | web 会话镜像不订阅专属 SSE 却宣称自动刷新 | P2 | Codex独有(与 R19-15 同簇分单) | +| P2-08 + R19-24 | web 移出成员/退群/工作区移出单击即提交,无二次确认无行锁 | P2 | 双方重复 | +| R19-15 | web 无个人空间/私聊导航入口 | M | Claude独有 | +| R19-1 | OKR 创建+关联端点前端不可达 | M | Claude独有 | +| R19-2 | AI 预算策略查看+编辑无 UI | M | Claude独有 | +| R19-6 | 成本页 by_workitem/trend/by_team 后端填了 web 不渲(跨端指路断裂) | M | Claude独有(codex 复核认可) | +| R19-26 | 设置 budgets 8 项无渲染 | L | Claude独有 | +| R19-32 | web 找人卡标题张冠李戴无动作 | L | Claude独有 | +| R19-47 | oldest_blocker href 渲成死文本 | L | Claude独有 | + +## Phase 2C — 桌面 webview + +| 来源 | 标题 | 级别 | 标记 | +|---|---|---|---| +| P2-07 + R19-12 | OS 通知点击不深链(生产绑定漏传 onSystemNotification+壳层无 click handler) | P2 | 双方重复 | +| R19-5 | 学到的自动通过策略三端不可撤销(桌面补撤销 UI) | M | Claude独有(codex 复核认可) | +| R19-3 | AI 文件改动 revert 端点无按钮 | M | Claude独有 | +| R19-9 | 项目树主区/协同未读只本地+1,重连不补缺 | M | Claude独有 | +| R19-11 | presence 两套轮询源,同人异态 | M | Claude独有(codex 复核认可) | +| R19-23 | 工作台网盘删除无确认/无回执/无回收站入口 | M | Claude独有(codex 复核认可) | +| R19-25 | 网盘行不可聚焦,键盘无法打开/进目录 | M | Claude独有(codex 复核认可) | +| R19-8(桌面半边) | 桌宠/rail 消费主动信号 | M | Claude独有 | +| R19-39 | step.snapshot 被桌面 run-stream 白名单丢弃 | L | Claude独有 | +| R19-40 | 新私聊等 30s 轮询才进左栏 | L | Claude独有 | + +## Phase 2D — 原生壳(Rust) + +| 来源 | 标题 | 级别 | 标记 | +|---|---|---|---| +| R19-16 | 隐藏主窗点 Dock 不恢复(无 RunEvent::Reopen) | M | Claude独有(codex 复核认可) | +| R19-13 | 壳层 locale 启动即冻结,无 setter | M | Claude独有(codex 复核认可) | +| R19-41 | 无自动更新/开机自启/崩溃上报 | L | Claude独有 | +| R19-42 | RUST_SHELL_OWNS 声明 local_file_sync 无实现 | L | Claude独有 | + +## Phase 3 — 一致性/工程质量/打磨 + +| 来源 | 标题 | 级别 | 标记 | +|---|---|---|---| +| P2-01 | 消息 DB commit 与 publish 间无 outbox/replay | P2 | Codex独有 | +| P2-02 | 停用账号清理 best-effort 失败不可重试 | P2 | Codex独有 | +| P2-03 | 成员移出/改角色审计失败被静默吞 | P2 | Codex独有 | +| P2-09 | 引导语言偏好同步失败被吞 | P2 | Codex独有 | +| P2-10 | 头像裁剪弹窗缺键盘焦点生命周期 | P2 | Codex独有 | +| P2-11 | Chrome QA 启动错误缺根因日志 | P2 | Codex独有 | +| P3-01 | cargo fmt/clippy 未进验证链且当前失败 | P3 | Codex独有 | +| P3-02 | 预算续租失败不可观测,0 行当成功 | P3 | Codex独有 | +| R19-7 | 人工协作工作台三张表全孤儿 | M | Claude独有 | +| R19-34 | spec_docs 恒空,current_spec_id 无写点 | L | Claude独有 | +| R19-35 | background_jobs 零操作,三处 job_id 恒 NULL | L | Claude独有 | +| R19-36 | knowledge_ask_runs 从不落库 | L | Claude独有 | +| R19-37 | 三张遗留死表 deliveries/attachments/comments(范围已修正) | L | Claude独有 | +| R19-27 | 工作项跨 run 审计时间线端点不渲染 | L | Claude独有 | +| R19-29 | 三个冗余死读端点 | L | Claude独有 | +| R19-30 | 增量 trace 游标前端不传 | L | Claude独有 | +| R19-31 | 军团后台透明窗口单薄且桌面独占 | L | Claude独有 | + +## 待产品拍板(不盲建) + +| 来源 | 标题 | 级别 | 标记 | +|---|---|---|---| +| R19-44 | 通知偏好只有按类型静音一个旋钮 | L | 待拍板 | +| R19-45 | 用户只能停用不能恢复 | L | 待拍板 | +| R19-46 | 项目无成员/访问控制(设计取舍) | L | 待拍板 | +| R19-33 | Cuu 主动性三档死控件(接频控或标注未生效) | L | 待拍板 | +| R19-28 | 合并融合 choose/preview 步从不调用 | L | 待拍板 | +| — | loop2 双开关翻默认 on(R18 真 key 冒烟已放行;约束:队列闸进程内) | — | 待拍板 | + +## 驳回与撤销总账 + +| 编号 | 结论 | 依据 | +|---|---|---| +| R19-14 托盘角标 | 撤销 | set_shell_badge(main.rs:537)同时更新 Dock 角标+托盘 tooltip 计数,browser.ts:175 真实调用(R15 A6) | +| R19-17 全局热键 | 撤销 | tauri-plugin-global-shortcut 2.3.2 在用,Option+Space 已注册(R15) | +| R19-38 死枚举 | 撤销 | R17 G2 既档 @deprecated 兼容债务(enums.ts:165),枚举值按档必须保留,删除建议违约 | +| R19-4 邀请 | 范围修正 | web 创建+清单 UI 已在(R18-H1);缺口收窄为接受落地页/撤销路由/桌面入口+token 抹除 | +| R19-37 死表 | 范围修正 | user_profiles 有真实仓库+路由,移除;其余三张维持 | +| 核实驳回 10 条 | 不成立 | 见 findings.json `rejected[]`(捏造行号/虚构文件/影响夸大),不再复核 | + +## 验收门槛 + +沿用 01-merged-fix-plan 第三节(即 codex 报告第 12 节):修复前稳定失败的根因测试、真实身份轮换集成测试、并发事务复现、crash 中断+二次进程恢复测试、禁空 catch、fmt/clippy 全绿后才进双端手工验收。 diff --git a/r19-iteration-review/findings.json b/r19-iteration-review/findings.json new file mode 100644 index 00000000..821677e1 --- /dev/null +++ b/r19-iteration-review/findings.json @@ -0,0 +1,1810 @@ +{ + "stats": { + "pipeline_raw": 64, + "pipeline_deduped": 57, + "verifier_confirmed": 47, + "verifier_rejected": 10, + "lead_withdrawn": 3, + "lead_corrected": 2, + "final_confirmed": 44, + "note": "处置状态唯一事实源=02-ssot-2026-07-17.md;confirmed[] 内含 lead_verdict.withdrawn 的条目不计入 final_confirmed" + }, + "confirmed": [ + { + "title": "学到的自动通过/权限策略在任何端都无法撤销或编辑(DELETE /api/permissions/:id 存在但零 UI 接线)", + "category": "fe-no-backend", + "surface": "cross", + "severity": "medium", + "detail": "网页设置页渲染每条自动通过策略的『撤销』按钮,带 data-requires-desktop=\"true\"、DELETE 方法,旁边文案写『撤销需在桌面端操作』。browser.ts 的委派分发在 requires-desktop=true 时直接 preventDefault + 弹 desktopRequiredNotice,从不发请求。服务端 DELETE /api/permissions/:id 确实存在且要求本地(桌面)客户端。问题是:桌面 desktop-webview 里没有任何渲染 permission_policies 或调用撤销的代码(grep 全空),api-client 也没有 revokePolicy/删除策略的方法。于是网页把用户指向桌面,桌面却没有这个能力——用户能创建常驻自动通过规则,却在任何客户端都无法撤销,是治理层的死胡同。", + "suggestion": "在桌面端补策略列表+撤销控件(调 DELETE /api/permissions/:id),并给 api-client 加对应方法;否则网页『撤销需在桌面端操作』的文案是在指向不存在的能力。", + "evidence": [ + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 4256, + "note": "revoke 按钮 data-requires-desktop=\"true\" data-method=\"DELETE\",href=policy.revoke_href" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 4249, + "note": "文案『撤销需在桌面端操作』/『revoking requires the desktop app』" + }, + { + "file": "apps/web/src/browser.ts", + "line": 1103, + "note": "requiresDesktop==='true' → preventDefault + desktopRequiredNotice,从不发 DELETE" + }, + { + "file": "apps/api/src/pages/settings.ts", + "line": 40, + "note": "revoke_href = /api/permissions/${policy.id}" + }, + { + "file": "apps/api/src/routes/permissions.ts", + "line": 91, + "note": "DELETE /:id 存在但 createRequireLocalClientMiddleware 限本地客户端;桌面/ api-client 均无对应 UI/方法(grep 全空)" + }, + { + "file": "apps/api/src/routes/permissions.ts", + "line": 91, + "note": "DELETE /api/permissions/:id (M24) revokes a permission policy including learned allow-grants; local-client + admin gated" + }, + { + "file": "apps/api/src/routes/permissions.ts", + "line": 83, + "note": "PUT /api/permissions creates a policy; POST /ask (line 101) requests an approval" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 4247, + "note": "settings page lists permission_policies read-only (data-r9-settings-policy rows) with no revoke/edit control" + } + ], + "dim": "web-unbacked", + "merged_from": [ + "web-unbacked:自动通过规则『撤销』是三端皆无的死胡同:网页按钮弹『需桌面端』,桌面无撤销 UI,客户端无撤销方法", + "api-orphans:Learned permission policies can be viewed but never revoked/edited from any UI" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "核实无误:web撤销按钮指向桌面(route-components.ts:4256/browser.ts:1103 preventDefault)、DELETE /:id 本地客户端门控(permissions.ts:91),而 desktop-webview 与 client-tauri 全库 grep 均无渲染 permission_policies 或调用撤销的任何代码(仅 test 命中),学到的自动通过规则确成三端不可撤销的治理死胡同,medium 诚实。" + } + }, + { + "title": "网页成本页组件不渲染后端已填充的多个 CostDashboardVM 维度(by_workitem/trend/by_team)", + "category": "dataflow", + "surface": "cross", + "severity": "medium", + "detail": "后端 cost 服务在 admin 口径下真实聚合并填充 CostDashboardVM.by_workitem(逐工作项花费+turns)。桌面 spotlight 成本视图渲染前 5 条 by_workitem,并在超出时打出文案「还有 N 个事项的花费,去网页版成本页细看」,把用户显式指向网页成本页。但网页成本组件 renderCostRouteComponent 通篇没有任何 by_workitem 卡片——它只渲 by_user/by_objective/by_task_plan(军团)/model_breakdown。React 适配器的 props 也不含 by_workitem。用户按桌面提示跳到网页 /dashboard/cost 找逐事项明细,那里根本没有这块,是一条断掉的跨端承诺。", + "suggestion": "在网页成本组件补一张「按事项花费」卡渲染 vm.by_workitem(admin 可见),或把桌面文案的指向改到确实展示该维度的位置。", + "evidence": [ + { + "file": "apps/desktop-webview/src/spotlight/views/dashboards.ts", + "line": 466, + "note": "topItems = vm.by_workitem.slice(0,5) 渲染逐事项行" + }, + { + "file": "apps/desktop-webview/src/spotlight/views/dashboards.ts", + "line": 480, + "note": "超过 5 条时文案『还有 N 个事项的花费,去网页版成本页细看』——把用户指向网页成本页" + }, + { + "file": "apps/api/src/pages/cost.ts", + "line": 177, + "note": "后端 by_workitem: input.isAdmin ? byWorkitem.map(...) 真实填充" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 4107, + "note": "网页成本组件最终装配(4107-4135)只含 metrics/risks/budget/models/army/labor/byUser/byObjective,无 by_workitem" + }, + { + "file": "packages/ui/src/gold-path/route-react-components.ts", + "line": 346, + "note": "cost React props(346-360)不含 by_workitem/by_team,仅计数字段" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 4113, + "note": "网页只渲 `${routeT('cost.trend')}: ${props.trendCount}` 一个裸计数 pill" + }, + { + "file": "packages/ui/src/gold-path/route-react-components.ts", + "line": 357, + "note": "trendCount = vm.trend.length —— props 只带条数,不带序列" + }, + { + "file": "apps/desktop-webview/src/spotlight/views/dashboards.ts", + "line": 444, + "note": "桌面 const trend = vm.trend.slice(-14) → 画柱状图+峰值(457-479)" + }, + { + "file": "apps/api/src/pages/cost.ts", + "line": 162, + "note": "后端 trend: aggregateTrend(uniqueEntries) 填充逐日序列" + }, + { + "file": "apps/api/src/pages/cost.ts", + "line": 171, + "note": "by_team: input.isAdmin ? byTeam.map(...) 真实填充" + }, + { + "file": "packages/contracts/src/pages.ts", + "line": 1024, + "note": "CostDashboardVM.by_team schema 定义" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 4135, + "note": "成本组件按 byUserCard/byObjectiveCard 装配,无 byTeam(全仓 grep by_team 在 ui/desktop 零命中)" + } + ], + "dim": "web-unbacked", + "merged_from": [ + "web-unbacked:成本页 by_workitem 破裂的跨端指路:桌面把用户导向网页成本页看逐事项花费,网页却从不渲染它", + "web-unbacked:成本趋势序列:桌面画成 14 天柱状图,网页只渲一个『趋势: N』的裸数字", + "web-unbacked:成本 by_team 分组由后端(admin)真实聚合,却在网页和桌面都无处渲染——死负载" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "核实无误:桌面 dashboards.ts:480 显式把用户导向网页成本页看逐事项花费,但网页成本组件装配段(route-components.ts:4107-4135)及 React props(route-react-components.ts:346-360)均无 by_workitem,全仓 by_workitem/by_team 仅在测试 fixture 命中、无渲染,而后端 cost.ts:177/171 admin 口径真实填充——跨端承诺断裂且 by_team 为死负载,medium 成立。" + } + }, + { + "title": "团队邀请生命周期(创建/接受/撤销)后端就绪却缺前端接线", + "category": "be-no-frontend", + "surface": "cross", + "severity": "medium", + "evidence": [ + { + "file": "apps/api/src/routes/auth.ts", + "line": 535, + "note": "POST /api/auth/invites mints a workspace invite" + }, + { + "file": "apps/api/src/routes/auth.ts", + "line": 576, + "note": "POST /api/auth/invites/accept redeems an invite token" + }, + { + "file": "packages/contracts/src/auth.ts", + "line": 46, + "note": "inviteCreateRequestSchema / inviteAcceptRequestSchema (line 51) exist in the contract" + }, + { + "file": "packages/db/src/repositories/invites.ts", + "line": 31, + "note": "revoke 接口+实现(89)已存在,注释「管理员撤销邀请(软删)」——是刻意为管理员设计的能力" + }, + { + "file": "apps/api/src/routes/auth.ts", + "line": 594, + "note": "GET /invites 列出待接受邀请供面板追踪,但没有配套的撤销路由" + } + ], + "detail": "Token-based team onboarding has endpoints and contract schemas, but grep for 'invite' across apps/web/src, apps/desktop-webview/src and packages/ui/src returns nothing and no SDK method wraps it. There is no 'invite a teammate' screen and no invite-accept landing flow, so the documented member-onboarding path cannot be exercised from the product on this branch.", + "suggestion": "Add an invite-create action to the member-management/settings surface and an invite-accept screen (deep-linkable from the token), wired to the two endpoints.", + "dim": "api-orphans", + "merged_from": [ + "api-orphans:Team invite create/accept flow is fully backed but has no UI on either client", + "product-missing:管理员能看到待接受邀请、却无法撤销:revoke 函数在仓库里但没有任何路由接线" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "POST /invites(:535)与/accept(:576)真实且生产注入invites仓库、revoke(:83)无路由调用,三端grep invite零命中确无UI,be-no-frontend核心成立;仅证据594行「GET /invites面板追踪」有误(无此路由)削弱次要合并项,不推翻主发现,medium公允。" + }, + "lead_verdict": { + "corrected": true, + "reason": "web 创建+待接受清单已由 R18-H1 交付(browser.ts:2849+,密码模式),原「三端零接线」不实;真实缺口=接受落地页/撤销路由/桌面入口,且 codex P1-05 证实 token 被列表重渲抹掉" + } + }, + { + "title": "webview 已算出的待处理数从未桥接到原生托盘 tooltip 与 Dock 角标(缺 webview→shell 计数命令)", + "category": "missing", + "surface": "desktop", + "severity": "medium", + "detail": "The tray is installed once with a constant tooltip: main.rs:1174 `.tooltip(tray_tooltip(locale))`, and tray_tooltip returns a hard-coded string ('WorkHub - Cuu 已就绪' / 'Cuu is ready', tray.rs:104-109). No `set_tooltip`/`set_title`/`set_icon` call exists anywhere in src-tauri (grep is empty), so the menu-bar presence is static for the whole session. The design doc explicitly specifies the opposite — tasks-reminders-notifications.md:238: '托盘 tooltip/title 同步未读数(防抖 250ms)'. The webview does compute the pending-approvals count and paints an in-app command badge (browser.ts:1342-1345 refreshApprovalsBadge → spotlight.setBadges), but that count never reaches the tray. When the main window is hidden to tray, the user has no menu-bar signal that decisions are waiting.", + "suggestion": "Add a shell command (e.g. set_tray_pending_count) the webview calls from refreshApprovalsBadge, and update the TrayIcon tooltip/title (and optionally icon overlay) on change.", + "evidence": [ + { + "file": "client-tauri/src-tauri/src/tray.rs", + "line": 104, + "note": "tray_tooltip returns a static constant; no runtime update path" + }, + { + "file": "client-tauri/src-tauri/src/main.rs", + "line": 1174, + "note": "tooltip set once at install; TrayIcon never mutated afterward" + }, + { + "file": "docs/workhub/04-modules/tasks-reminders-notifications.md", + "line": 238, + "note": "spec requires tray tooltip/title to sync unread count" + }, + { + "file": "apps/desktop-webview/src/browser.ts", + "line": 1345, + "note": "pending count painted only as in-app badge; no native Dock badge" + }, + { + "file": "client-tauri/src-tauri/src/main.rs", + "line": 1494, + "note": "no set_badge command exists in the shell command surface" + } + ], + "dim": "native-shell", + "merged_from": [ + "native-shell:Tray tooltip/title never reflects pending-decision count (documented feature unimplemented)", + "native-shell:No OS Dock badge for pending decisions when the app is hidden" + ], + "verdict": { + "real": true, + "reason": "证据全部核实:tray tooltip 为静态常量、src-tauri 无任何运行时更新/set_badge 命令、webview 算出的待决策数只落页内角标不桥接原生,且文档 238 行明文规定该同步,属未实现的文档化能力,medium 合理。", + "adjusted_severity": "medium" + }, + "lead_verdict": { + "withdrawn": true, + "reason": "set_shell_badge(main.rs:537)同时更新 Dock 角标与托盘 tooltip 计数(tray_tooltip_with_badge),browser.ts:175 每 30s 真实调用——能力已存在(R15 A6)" + } + }, + { + "title": "工作台网盘删除是破坏性操作却无回收站/恢复入口(restoreDriveItem 已接入客户端但工作台从不调用)", + "category": "ux", + "surface": "desktop", + "severity": "medium", + "detail": "工作台网盘标签页里删除文件是「一点即删、零确认」:删除按钮渲成危险色、文案只有裸「删除」/「Delete」(drive/render.ts:92),点击后直接调 deleteDriveItem(drive/view.ts:158),成功只 .then(load) 静默重拉、没有任何『已移到回收站,可恢复』的回执(drive/view.ts:161),失败也只贴一句『删除失败』。更关键的是:renderDriveListHtml 只从 items 里筛 folders+files 渲染(drive/render.ts:112-115),从不渲染契约里带的 deleted_items;整个 workbench/drive/ 目录里搜不到任何『回收/deleted/recycle/移到』字样,restoreDriveItem 虽被 Pick 进客户端类型却从没在任何工作台模块里被调用(真正调用只在 spotlight/views/drive.ts:434)。也就是说工作台用户删错文件后,在工作台内既得不到撤销、也找不到恢复路径。这与产品自身的其它约定自相矛盾:同一个右栏里『版本回滚』这种覆盖性动作反而做了两段式确认(drive/render.ts 版本区注释明确『回滚这种覆盖性动作不该无确认』),web 端删除按钮直接叫『移到回收站』并在成功后提示『已把「X」移到回收站,可在回收站中恢复』(apps/web/src/browser.ts:1405 附近)+整套回收站列表,Spotlight 桌面端删除 toast 也是『已删除(在回收站可恢复)』。唯独主力界面工作台把最具破坏性的删除做成了无确认、无回执、无恢复入口、还用了误导性的『删除』字样。", + "evidence": [ + { + "file": "apps/desktop-webview/src/workbench/drive/render.ts", + "line": 92, + "note": "删除按钮:wh-wb-act--danger 危险色,文案裸写『删除』/『Delete』(非 web 的『移到回收站』)" + }, + { + "file": "apps/desktop-webview/src/workbench/drive/view.ts", + "line": 158, + "note": "点击『删除』→ 立即 deleteDriveItem,无任何确认;.then(load) 静默重拉、无『可恢复』回执" + }, + { + "file": "apps/desktop-webview/src/workbench/drive/render.ts", + "line": 112, + "note": "渲染函数只筛 folders+files,从不渲染 deleted_items → 工作台无回收站/恢复入口" + }, + { + "file": "apps/web/src/browser.ts", + "line": 1405, + "note": "对照:web 删除后提示『已把「X」移到回收站,可在回收站中恢复』——诚实告知可恢复" + }, + { + "file": "apps/desktop-webview/src/spotlight/views/drive.ts", + "line": 434, + "note": "对照:restoreDriveItem 的唯一真实调用点在 Spotlight(工作台内无);Spotlight 有『最近删除』恢复区" + }, + { + "file": "apps/desktop-webview/src/workbench/drive/view.ts", + "line": 22, + "note": "DriveTabApiClient Pick includes restoreDriveItem, but grep shows no call site anywhere in the workbench (only version rollback exists, which is a different endpoint)" + }, + { + "file": "apps/desktop-webview/src/workbench/drive/view.ts", + "line": 158, + "note": "delete is fully wired (deleteDriveItem); the drive list only shows live items filtered by parent_id — a deleted item just disappears with no recovery path in this surface" + }, + { + "file": "apps/api/src/routes/drive.ts", + "line": 481, + "note": "backend POST /projects/:projectId/items/:itemId/restore exists (drivePages.restoreItem); client.restoreDriveItem (packages/api-client/src/client.ts:596) wraps it and shell.ts:79 explicitly Picks it into the workbench client" + } + ], + "dim": "ux-polish", + "merged_from": [ + "ux-polish:桌面工作台网盘删除文件:无确认、按钮裸写「删除」、删完无回执、且工作台内根本没有回收站/恢复入口", + "desktop-unbacked:Workbench drive can delete files but has no restore/trash affordance, though restoreDriveItem is wired into its client" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "逐条核实全部属实:工作台删除(view.ts:153-168)零确认+仅静默重拉+失败只贴「删除失败」,render.ts只渲folders+files从不渲deleted_items,restoreDriveItem虽Pick进客户端却全工作台无调用点(仅spotlight:434/web:1424调用),而契约有deleted_items、web诚实提示「移到回收站可恢复」、spotlight有恢复区——工作台确实是唯一无确认/无回执/无恢复入口的破坏性删除,非裁除项,medium成立。" + } + }, + { + "title": "设置 VM 的 budgets(8 项上限)被后端填充却无任何界面渲染", + "category": "be-no-frontend", + "surface": "cross", + "severity": "low", + "detail": "settings 页服务把 8 个预算上限字段(run/user/team 的 token 与成本上限)填进 SettingsPageVM.budgets。但网页设置组件 renderSettingsRouteComponent 只渲语言/设备/运行时/LLM/权限策略五块,从不渲 budgets;桌面设置视图也不渲。这些配置上限值算出来后没有任何界面消费,是又一份死负载(用户想核对团队日/月预算上限,在设置页看不到)。", + "suggestion": "在网页设置的『系统诊断』区补一张预算上限只读卡渲染 vm.budgets,或从 VM 移除无人消费的 budgets。", + "evidence": [ + { + "file": "apps/api/src/pages/settings.ts", + "line": 62, + "note": "budgets: { run_tokens/user_daily_tokens/.../team_monthly_cost_cny } 8 项全填充" + }, + { + "file": "packages/contracts/src/pages.ts", + "line": 1113, + "note": "SettingsPageVM.budgets schema(8 字段)" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 4217, + "note": "设置组件(4217-4281)只渲 language/device/runtime/llm/policies,无 budgets" + } + ], + "dim": "web-unbacked", + "merged_from": [ + "web-unbacked:设置 VM 的 budgets(8 项 token/成本上限)被后端填充却无任何界面渲染" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "证据全部核实无误:settings.ts:62-71 填充 8 项预算上限进 SettingsPageVM.budgets,contracts pages.ts:1113 有 schema,但全库 grep vm.budgets 无任何消费方,web 设置组件(route-components.ts)与桌面 spotlight/views/settings.ts 均不渲染,属真实死负载,low 恰当。" + } + }, + { + "title": "军团面板 oldest_blocker 后端给了跳转 href,网页只渲成不可点灰字", + "category": "ux", + "surface": "web", + "severity": "low", + "detail": "agent-army 页服务给每个卡住的军团计划填 oldest_blocker.href='/attention',让用户能一键跳去处理这个『最老的阻塞(等人)』。网页军团组件却只渲 plan.oldest_blocker.label 成一段
灰字,完全忽略 href——一条告诉用户『有子任务卡住等你』的信息,是死文本而非可点入口。桌面同资源的可操作性更强,网页此处的可点性被丢了。", + "suggestion": "把网页 oldest_blocker 那段
包成指向 plan.oldest_blocker.href 的链接(与该卡其它 href 一致走 primaryHrefs)。", + "evidence": [ + { + "file": "apps/api/src/pages/agent-army.ts", + "line": 283, + "note": "oldest_blocker 填 { kind, label, age_seconds, href: '/attention' }" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 3576, + "note": "网页只渲 `
${plan.oldest_blocker.label}
灰字丢了 href(同卡 budget_href/work_item_href 都做成了 ),确为可点入口缺失,但仅 low 打磨级。", + "adjusted_severity": "low" + } + }, + { + "title": "Objectives(OKR) 创建+关联端点在任何前端都不可达", + "category": "be-no-frontend", + "surface": "cross", + "severity": "medium", + "evidence": [ + { + "file": "apps/api/src/routes/objectives.ts", + "line": 52, + "note": "POST /api/objectives creates an objective with key_results; the file's own comment (line 23) says it 'adds the minimal loop' for objective creation/linking" + }, + { + "file": "apps/api/src/routes/objectives.ts", + "line": 81, + "note": "POST /api/objectives/:id/link attaches a work item to an objective, gated by assertCanMutateWorkItem" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 3571, + "note": "agent-plan cards DISPLAY objective title + progress % (data-r9-agent-plan-objective) read-only" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 4034, + "note": "cost page renders by_objective breakdown read-only" + } + ], + "detail": "POST /api/objectives and POST /api/objectives/:id/link exist with full validation and work-item-write authorization, but there is no client-SDK method (packages/api-client/src/client.ts defines none for objectives) and grep across apps/web/src, apps/desktop-webview/src and packages/ui/src finds zero callers. Objective titles/progress are shown as read-only labels sourced from task-plan data, so a user sees objectives referenced but can never create one or link a work item to it from either the web or desktop client.", + "suggestion": "Add an objective-create form and a 'link to objective' action on the work-item detail surface, wired to a new SDK method; or, if OKRs are not yet a shipped feature, drop the read-only objective labels so the concept is not dangled without an entry point.", + "dim": "api-orphans", + "merged_from": [ + "api-orphans:Objectives (OKR) create + link endpoints are entirely unreachable from any frontend" + ], + "verdict": { + "real": true, + "reason": "核对属实:POST /api/objectives 与 /:id/link 在 objectives.ts:52/81 带完整校验和工作项写鉴权,但 api-client/web/desktop/ui 全量 grep 无任何调用者或 SDK 方法,route-components 仅只读展示 objective_title/progress,用户能看见目标却无从创建或挂链,属真实 backend-orphan,medium 合理。", + "adjusted_severity": "medium" + } + }, + { + "title": "AI 预算策略的查看+编辑(cost/policies)无任何 UI 接线,管理员无法设/改上限", + "category": "be-no-frontend", + "surface": "cross", + "severity": "medium", + "evidence": [ + { + "file": "apps/api/src/routes/cost.ts", + "line": 82, + "note": "GET /api/cost/policies lists budget policies (admin-only)" + }, + { + "file": "apps/api/src/routes/cost.ts", + "line": 89, + "note": "PUT /api/cost/policies/:scope/:id edits caps/warning-critical ratios/on_exhausted with atomic audit logging" + }, + { + "file": "packages/api-client/src/client.ts", + "line": 573, + "note": "SDK costPolicies() / updateCostPolicy() (lines 573-575) exist but have 0 non-test callers" + } + ], + "detail": "The whole budget-governance write surface is backend-only. costPolicies, updateCostPolicy and costUsage are declared in the SDK (client.ts:572-575) and covered by API smoke tests, but no web/desktop/ui code invokes them. The cost page (/api/pages/cost) renders usage read-only and never surfaces the editable policy objects, so an admin has no way to set or change an AI budget cap, warning ratio, or exhausted-behavior through the product despite the endpoint doing careful atomic policy+audit writes.", + "suggestion": "Add an admin budget-policy editor (list + per-scope edit form) to the cost/settings surface, wired to costPolicies/updateCostPolicy.", + "dim": "api-orphans", + "merged_from": [ + "api-orphans:AI budget-policy management (view + edit caps) is inaccessible — no UI wires cost/policies or cost/usage" + ], + "verdict": { + "real": true, + "reason": "三处 file:line 均核实属实:GET/PUT /api/cost/policies 为 admin-only 写面,SDK costPolicies/updateCostPolicy 全库零非测试调用者(仅 client.ts 实现+types.ts 声明),cost 页面只读渲染 usage 无策略编辑表单,管理员确无 UI 可设/改预算上限,非裁除项,medium 恰当。", + "adjusted_severity": "medium" + } + }, + { + "title": "『撤销 AI 的文件改动』(snapshot revert) 端点存在但任何界面都无按钮", + "category": "be-no-frontend", + "surface": "cross", + "severity": "medium", + "evidence": [ + { + "file": "apps/api/src/routes/audit.ts", + "line": 106, + "note": "POST /api/agent-runs/:id/revert reverts a file snapshot, marks the audit row undone, and writes a snapshot.reverted audit entry; local-client gated" + }, + { + "file": "packages/ui/src/replay/render.ts", + "line": 1, + "note": "replay view renders snapshots/audit facts but offers no revert action (no revert SDK method exists in packages/api-client)" + } + ], + "detail": "The destructive-change undo capability is fully implemented backend (file revert + audit reconciliation) and local-client gated for the desktop shell, but there is no SDK method (revertAgentRun) and no caller in apps/desktop-webview/src, apps/web/src, packages/ui/src, or client-tauri. Users can view what the AI changed via replay but can never roll it back through the product.", + "suggestion": "Add a 'Revert this change' action on the desktop replay/snapshot surface (it must run on the local client), wired to POST /api/agent-runs/:id/revert with a confirm step.", + "dim": "api-orphans", + "merged_from": [ + "api-orphans:'Undo the AI's file changes' (snapshot revert) has no button anywhere" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "经核对 audit.ts:106 确有本地门控的 POST /agent-runs/:id/revert 完整实现,而 api-client 无 revert 方法、全端(desktop/web/ui/tauri)无任何调用者、replay 只只读渲染快照数,非裁除项,孤儿端点判定成立。" + } + }, + { + "title": "工作项跨 run 汇总审计时间线端点被计算却从未被渲染", + "category": "be-no-frontend", + "surface": "cross", + "severity": "low", + "evidence": [ + { + "file": "apps/api/src/routes/audit.ts", + "line": 80, + "note": "GET /api/audit/workitems/:id/audit returns snapshots + audit_logs + buildReplayManifestFacts() across ALL runs for a work item" + }, + { + "file": "apps/api/src/routes/pages.ts", + "line": 666, + "note": "the work-item detail page (GET /api/pages/workitems/:id) does not embed audit/snapshot/manifest_facts; only per-run replay (/agent-runs/:id/replay) is wired in the clients" + } + ], + "detail": "This endpoint assembles a whole-work-item history (every run's snapshots, audit logs, and derived manifest_facts) but has no SDK method and no frontend caller. The clients only ever fetch per-run replay, so the aggregated cross-run governance trail — the exact thing a reviewer would want on a work item — is calculated on request but shown nowhere.", + "suggestion": "Surface an 'Audit / history' tab on the work-item detail page backed by this endpoint, or fold its manifest_facts into the work-item page VM.", + "dim": "api-orphans", + "merged_from": [ + "api-orphans:Work-item consolidated audit timeline endpoint is computed but never rendered" + ], + "verdict": { + "real": true, + "reason": "audit.ts:80 确返回跨run聚合快照/审计/manifest_facts,而replay渲染器(replay/render.ts)不读evidence_refs、客户端也无任何fetch该端点,唯一href(replay.ts:256)从不被渲染,per-run replay又filter到单run,故此聚合治理轨迹确实算而不现,low属实(note里路径多写了/audit段为无伤大雅的小瑕疵)。", + "adjusted_severity": "low" + } + }, + { + "title": "合并融合候选『选择/预览』步骤从不被调用,客户端直接 apply", + "category": "be-no-frontend", + "surface": "cross", + "severity": "low", + "evidence": [ + { + "file": "apps/api/src/routes/proposals.ts", + "line": 1062, + "note": "POST /api/merge-proposals/:id/choose selects/previews an AI fusion candidate (option_key) before applying" + }, + { + "file": "packages/api-client/src/client.ts", + "line": 497, + "note": "SDK chooseMergeProposalCandidate exists but has 0 UI callers; both web (browser.ts) and desktop only call applyMergeProposalCandidate (/merge-proposals/:id/apply)" + } + ], + "detail": "The merge-conflict flow supports picking/previewing among AI fusion candidates via /choose, but neither client uses it — they call /apply directly with a payload. The 'compare candidates before committing' affordance is backend-only, so users cannot preview alternative fusion outcomes.", + "suggestion": "Either wire a candidate-preview step (chooseMergeProposalCandidate) into the merge UI before apply, or remove the endpoint + SDK stub if apply-direct is the intended UX.", + "dim": "api-orphans", + "merged_from": [ + "api-orphans:Merge fusion-candidate 'choose/preview' step is never invoked (clients jump straight to apply)" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "核实无误:/merge-proposals/:id/choose(proposals.ts:1062)与SDK chooseMergeProposalCandidate(client.ts:497)确实存在,但全端源码(web/desktop browser.ts、spotlight views、desktop-cuu-runtime)仅调用applyMergeProposalCandidate,choose零调用方,属真实的后端孤儿端点/未接线SDK桩,low级诚实。" + } + }, + { + "title": "三个冗余死读端点(agent-run handoff / ai-worklog today / list-work-item-proposals)无前端消费", + "category": "be-no-frontend", + "surface": "api", + "severity": "low", + "evidence": [ + { + "file": "apps/api/src/routes/agent-runs.ts", + "line": 362, + "note": "GET /api/agent-runs/:id/handoff; SDK getAgentRunHandoff (types.ts:289) has 0 callers — handoff is already rendered from the run live-VM in packages/ui/src/agent-run/render.ts:92" + }, + { + "file": "apps/api/src/routes/ai-worklog.ts", + "line": 25, + "note": "GET /api/ai-worklog/today computes today's AI metrics; no SDK method, no UI caller — the same metrics are surfaced via /api/pages/attention" + }, + { + "file": "apps/api/src/routes/proposals.ts", + "line": 1044, + "note": "GET /api/workitems/:id/proposals; SDK listWorkItemProposals (types.ts:306) has 0 callers — proposals are embedded in the work-item page VM instead" + } + ], + "detail": "Three read endpoints (and two SDK stubs) exist whose data is already delivered to the clients through page/live VMs, so no frontend ever calls the dedicated endpoints. They are harmless but dead surface area that implies capabilities the UI reaches a different way.", + "suggestion": "Remove the unused endpoints/SDK stubs, or repoint the clients to them if the goal is a single canonical fetch path.", + "dim": "api-orphans", + "merged_from": [ + "api-orphans:Dead redundant read endpoints: agent-run handoff, ai-worklog/today, and list-work-item-proposals" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "三处 file:line 属实,getAgentRunHandoff/listWorkItemProposals 两个 SDK 方法只在 client.ts/types.ts 声明、无任何非测试调用者,/api/ai-worklog/today 的 getTodayMetrics 只被 pages.ts 的页面 VM 路径复用而非该专用端点,数据确经 live-VM/页面 VM 交付,是真实死冗余读端点,low 严重度诚实。" + } + }, + { + "title": "增量 trace 游标(?after=) 被支持但前端从不传", + "category": "be-no-frontend", + "surface": "api", + "severity": "low", + "evidence": [ + { + "file": "apps/api/src/routes/agent-runs.ts", + "line": 331, + "note": "GET /api/agent-runs/:id/trace parses an 'after' non-negative-integer cursor for incremental step fetch" + }, + { + "file": "packages/ui/src/agent-run/render.ts", + "line": 125, + "note": "clients render the trace from the embedded VM (vm.trace) and rely on the SSE push stream; no caller passes ?after=" + } + ], + "detail": "The trace endpoint supports cursor-based incremental polling via ?after=, but every frontend either renders the full trace from the page/live VM or subscribes to the SSE stream, so the pagination parameter is never exercised. Minor: unused query-param capability.", + "suggestion": "Either drop the cursor param or use it for a lightweight incremental-trace fallback when SSE is unavailable.", + "dim": "api-orphans", + "merged_from": [ + "api-orphans:Incremental trace cursor (?after=) is supported but the frontends never send it" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "核实无误:agent-runs.ts:331 确解析 after 游标并于 337 传入 queue.trace,而前端一律从内嵌 vm.trace(render.ts:112/125)渲染或订 SSE,全库 grep 无任何调用方传 ?after=,确系无前端消费的孤儿参数,low 打磨级恰当。" + } + }, + { + "title": "spec_docs 表从不写入,work_items.current_spec_id 只读恒为空", + "category": "dataflow", + "surface": "api", + "severity": "low", + "detail": "spec_docs 表(schema core.ts:1123,带 project/work_item/content_sha256 索引)在整个仓库里从没有 insert 或 select——唯一的引用是 relations/core.ts:71 声明的 currentSpec 关系,而 drizzle 的关系查询 API(db.query.*/with:{})在生产代码里从未被调用(全局 0 处)。work_items.current_spec_id(FK→spec_docs,core.ts:336)也从没有任何写点:work-items.ts:1138 把它读进 DTO(row.currentSpecId→workItem.current_spec_id)、契约 work-item.ts:55 也导出该字段,但没有一行代码 set 过它。结果:spec_ready 状态暗示存在一份 spec 文档,但该文档从未被持久化,API 响应里 current_spec_id 永远缺席。对比 main_branch_id 在 proposals.ts:1428 有真实写点——current_spec_id 是残缺的另一半。", + "suggestion": "要么把 spec 生成落库(写 spec_docs 并回填 current_spec_id),要么删掉 spec_docs 表、current_spec_id 列与契约字段,避免 API 消费方对着永远为空的字段编程。", + "evidence": [ + { + "file": "packages/db/src/schema/core.ts", + "line": 1123, + "note": "spec_docs 表定义,全仓无 insert/select" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 336, + "note": "work_items.current_spec_id FK 声明,无写点" + }, + { + "file": "apps/api/src/services/work-items.ts", + "line": 1138, + "note": "只读:把 currentSpecId 映射进响应 DTO" + }, + { + "file": "packages/contracts/src/domain/work-item.ts", + "line": 55, + "note": "契约导出 current_spec_id,实际恒为空" + } + ], + "dim": "data-event-orphans", + "merged_from": [ + "data-event-orphans:spec_docs 表从不写入,work_items.current_spec_id 只读不写、恒为空" + ], + "verdict": { + "real": true, + "reason": "grep 证实全仓无 insert(specDocs)/db.query 关系查询/currentSpecId 赋值,work-items.ts:1138 只读、契约导出该 optional 字段——spec_docs 表与 current_spec_id 列确为无写点孤儿;但字段 optional 且恒 absent,不撞死主流程也不产错数据,属残余脚手架,故降为 low。", + "adjusted_severity": "low" + } + }, + { + "title": "人工协作工作台三张表全孤儿(work_item_workspaces/_items/progress_updates)", + "category": "dataflow", + "surface": "api", + "severity": "medium", + "detail": "三张表在 0000 迁移里建好并带完整 FK+索引,建模了一整套'人类队友工作台+进度追踪'能力:work_item_workspaces(phase/progress_percent/status_note/blocked_reason,core.ts:646)、work_item_workspace_items(todo 清单+sort_order,core.ts:665)、work_item_progress_updates(actor/kind/body/phase/progress 活动流,core.ts:681)。但三者在 apps/**、packages/db/src/repositories/**、packages/contracts/** 全部 0 引用——没有任何 insert/select/update,连 relations 里都没声明。这是被设计但从未接线的子系统:任何读 schema 做规划的人都会以为进度/协作工作台是真的,实际后端与前端都不存在。", + "suggestion": "确认是废弃设计后从 schema+PG 删除(减少每次迁移携带的死表),或补上 repository/service/UI 真正落地这套协作进度追踪。", + "evidence": [ + { + "file": "packages/db/src/schema/core.ts", + "line": 646, + "note": "work_item_workspaces 定义,全仓 0 引用" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 665, + "note": "work_item_workspace_items 定义,全仓 0 引用" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 681, + "note": "work_item_progress_updates 定义,全仓 0 引用" + } + ], + "dim": "data-event-orphans", + "merged_from": [ + "data-event-orphans:人工协作工作台三张表全孤儿:work_item_workspaces / _items / progress_updates" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "亲验:三表在 core.ts:646/665/681 建模齐全,全仓仅 schema 定义+export 桶(1687-89)+迁移锁触发器表名清单引用,repositories/contracts/apps 无任何读写与 relations,确系被设计从未接线的孤儿;无用户主流程撞死但明显残缺且误导 schema 读者,medium 成立。" + } + }, + { + "title": "background_jobs 表从不写入,三处 job_id 外键恒为 NULL", + "category": "dataflow", + "surface": "api", + "severity": "low", + "detail": "background_jobs 表(core.ts:280)从没有 insert/select——生产代码里对它零操作(连测试/qa/seed 都不碰)。三张表挂了指向它的 job_id 外键:knowledge_ask_runs.job_id(core.ts:391)、meeting_records.job_id(core.ts:590)、work_item_task_plans.job_id(core.ts:820)。因为 background_jobs 永远是空表,这些 job_id 列只能是 NULL(写非空值会违反 FK 约束),且全仓也确实没有任何 jobId 非空写点。也就是说 schema 建模了一套持久化异步任务队列,但根本没实现——会议转写等异步流程实际靠各自记录上的 status 列('processing'→done)自管,与这张表无关。", + "suggestion": "删除 background_jobs 表及三处 job_id 死列(它们制造了误导性的'有 job 队列'假象),或让异步流程真正落 background_jobs 并回填 job_id。", + "evidence": [ + { + "file": "packages/db/src/schema/core.ts", + "line": 280, + "note": "background_jobs 表定义,无 insert/select" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 391, + "note": "knowledge_ask_runs.job_id FK 恒 NULL" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 590, + "note": "meeting_records.job_id FK 恒 NULL" + } + ], + "dim": "data-event-orphans", + "merged_from": [ + "data-event-orphans:background_jobs 表从不写入,三处 job_id 外键恒为 NULL" + ], + "verdict": { + "real": true, + "reason": "核对属实:background_jobs 表(core.ts:280)全仓无 insert/select,三处 job_id 外键(391/590/820)无任何非空写点(唯一读点 meeting-pages.ts:299 条件读、测试均置 null),确为建模但未实现的孤儿表;无用户影响,low 诚实。", + "adjusted_severity": "low" + } + }, + { + "title": "knowledge_ask_runs 表从不落库,知识问答无历史/审计出口", + "category": "dataflow", + "surface": "api", + "severity": "low", + "detail": "knowledge_ask_runs 表(core.ts:384)建模了知识问答的完整历史:question、answer_md、citations_json、trace_json、status。但全仓对它零 insert/select(连测试都没有)。知识问答实际是即时/瞬态计算的(knowledge.evidence.ready 事件由 agent loop 直接发出,不落这张表),用户问完即走、拿不到任何问答历史或引用溯源,尽管 schema 明确为此建了表和四个索引。这是 worker/服务产出的数据没有任何持久化出口的典型。", + "suggestion": "若要提供问答历史/引用审计,让知识 ask 流程写 knowledge_ask_runs 并加一个列表出口;否则删表。", + "evidence": [ + { + "file": "packages/db/src/schema/core.ts", + "line": 384, + "note": "knowledge_ask_runs 定义(question/answer_md/citations/trace),全仓 0 读写" + } + ], + "dim": "data-event-orphans", + "merged_from": [ + "data-event-orphans:knowledge_ask_runs 表从不落库:知识问答无历史/审计出口" + ], + "verdict": { + "real": true, + "reason": "grep 确认 knowledgeAskRuns 仅在 core.ts:384 定义与 1676 barrel 导出出现,全仓零 insert/select,知识问答走 knowledge.evidence.ready 事件即时计算不落表,孤儿表属实,low 级诚实。", + "adjusted_severity": "low" + } + }, + { + "title": "四张遗留 schema 死表(deliveries/attachments/comments/user_profiles)", + "category": "dataflow", + "surface": "api", + "severity": "low", + "detail": "四张表定义并从 schema 索引导出,但没有任何 repository/service/route 对它们做 insert/select/update:deliveries(core.ts:914)、attachments(core.ts:877)、comments(core.ts:937,注意与在用的 approval_comments/project_drive_comments 是不同的表)、user_profiles(core.ts:238)。它们唯一的'引用'是 relations/core.ts 里的关系声明(profile: one(userProfiles)、deliveries: many(deliveries)、comments、currentSpec 等)——但 drizzle 关系查询 API(db.query.* / with:{})在生产代码全局 0 调用(见 packages/db/src/index.ts 只 export 关系但无人 query),所以这些关系声明是惰性的、运行期从不触发。四张表纯属早期 schema 遗留,随每次迁移携带进 PG。", + "suggestion": "确认废弃后从 schema+PG 清除;或在关系声明处加注释标注'预留/未接线',避免误导。", + "evidence": [ + { + "file": "packages/db/src/schema/core.ts", + "line": 914, + "note": "deliveries 表,仅 relations 声明,无 query" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 877, + "note": "attachments 表,全仓 0 引用" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 937, + "note": "comments 表(非 approval/drive comments),无 query" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 238, + "note": "user_profiles 表,仅 relations 声明" + } + ], + "dim": "data-event-orphans", + "merged_from": [ + "data-event-orphans:四张遗留 schema 死表:deliveries / attachments / comments / user_profiles" + ], + "verdict": { + "real": true, + "reason": "grep 证实 deliveries/attachments/comments/user_profiles 四表除 core.ts 定义与 relations 声明外全仓无任何 repo/service/route 读写,且 db.query.* 关系查询 API 生产代码零调用,关系声明确属惰性遗留,file:line 与描述全部属实,low 级不夸大。", + "adjusted_severity": "low" + }, + "lead_verdict": { + "corrected": true, + "reason": "user_profiles 有真实仓库(user-profiles.ts)+GET/PATCH 路由,从死表清单移除(codex 反馈);deliveries/attachments/comments 复核维持" + } + }, + { + "title": "三个 SSE 事件类型死枚举(usage.recorded 全死,confidence.scored/sync.progress 订而无发)", + "category": "dataflow", + "surface": "cuu", + "severity": "low", + "detail": "事件层存在订阅方无发布方的孤儿。(1) usage.recorded(eventTypes.usageRecorded,enums.ts:187)既不被任何生产代码发布,也不被 toCuuState/toAttentionItem/桌面白名单消费——只出现在 gold-path 夹具(fixtures/gold-path.ts:1133);用量直接写入 usage_records 表,从不发事件。(2) confidence.scored 在 toCuuState.ts:9 被映射为桌宠'thinking'态,但从没有一处 bus.publish 发它——agent-run-confidence.ts:128 的 'confidence.scored' 只是审计日志 action 字符串,不是事件。(3) sync.progress 在 toCuuState.ts:20 被映射为 'syncing_files' 态,但全仓无发布方(仅出现在 openapi 文档枚举与桌面 QA 场景)。结果:桌宠的'思考(来自打分)'与'同步文件'动画永远不会因真实事件触发。", + "suggestion": "要么在打分/同步流程真正发这两个事件(补齐 CuuState 反馈),要么从 eventTypes 与 toCuuState 里删掉这三个死枚举,避免维护者以为它们是活的。", + "evidence": [ + { + "file": "packages/contracts/src/enums.ts", + "line": 187, + "note": "usage.recorded 定义,全仓无发布/消费" + }, + { + "file": "packages/events/src/toCuuState.ts", + "line": 9, + "note": "confidence.scored→thinking,但无发布方" + }, + { + "file": "packages/events/src/toCuuState.ts", + "line": 20, + "note": "sync.progress→syncing_files,但无发布方" + }, + { + "file": "apps/api/src/services/agent-run-confidence.ts", + "line": 128, + "note": "'confidence.scored' 只是审计 action,非事件" + } + ], + "dim": "data-event-orphans", + "merged_from": [ + "data-event-orphans:三个 SSE 事件类型是死枚举:usage.recorded 全死,confidence.scored / sync.progress 订而无发" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "三条证据逐一核实成立:usage.recorded 除文档外无生产发布方也无消费方(cost 文档自认未落),confidence.scored 在 toCuuState:9 映射但 agent-run-confidence:128 只是 audit action 字符串非 bus.publish,sync.progress 在 toCuuState:20 映射但仅 QA 脚本发、无真实发布方,均为订而无发的死枚举,low 诚实。" + }, + "lead_verdict": { + "withdrawn": true, + "reason": "非新发现:enums.ts:165 起为 R17 G2 所加 @deprecated 标注,明文枚举值一律保留(契约兼容面),删除建议违背既档约束(codex 反馈后复核坐实)" + } + }, + { + "title": "step.snapshot 事件被桌面 run-stream 具名白名单丢弃", + "category": "dataflow", + "surface": "desktop", + "severity": "low", + "detail": "agent loop 在 loop.ts:907 发出 step.snapshot(eventTypes.stepSnapshot,检查点/回滚快照标记),runner 会把它连同其他事件一并 publish 到 run 主题与 workitem 主题(agent-runner.ts 的 emit 路径)。web 端订阅全部事件类型(Object.values(eventTypes),browser.ts:154)故能触发刷新;但桌面 CUU run-stream 用具名事件监听白名单 desktopCuuRunStreamEventNames(desktop-cuu-runtime.ts:1397-1409)——该列表含 agent_run.step / step.tool_result / budget.* 等,唯独遗漏 step.snapshot。EventSource 的具名事件不会触发默认 onmessage,所以桌面工作台在实时运行视图里永远收不到 step.snapshot,快照落点对桌面用户不可见(只能事后走 REST replay)。", + "suggestion": "若桌面运行视图希望标示'已保存检查点',把 eventTypes.stepSnapshot 加入 desktopCuuRunStreamEventNames;否则确认此遗漏是有意的并加注释。", + "evidence": [ + { + "file": "packages/agent/src/loop/loop.ts", + "line": 907, + "note": "发出 step.snapshot 事件" + }, + { + "file": "apps/desktop-webview/src/desktop-cuu-runtime.ts", + "line": 1401, + "note": "run-stream 具名白名单含 stepToolResult 却漏 stepSnapshot" + } + ], + "dim": "data-event-orphans", + "merged_from": [ + "data-event-orphans:step.snapshot 事件被桌面 run-stream 白名单丢弃" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "核对属实:loop.ts:907 确发 stepSnapshot,web(browser.ts:154 全量订阅+render.ts:85 渲 snapshot pill)能收,而桌面具名白名单(desktop-cuu-runtime.ts:1397-1409)含 stepToolResult 却独漏 stepSnapshot,EventSource 具名事件不触发 onmessage,桌面实时视图确收不到,只能事后 REST replay 看到检查点标记,属真实低级事件孤儿。" + } + }, + { + "title": "主动 Cuu 会话消息投递后无任何端能提示(桌宠/rail/OS 通知全静默丢失)", + "category": "dataflow", + "surface": "cross", + "severity": "medium", + "evidence": [ + { + "file": "apps/api/src/services/proactive-cuu-delivery.ts", + "line": 110, + "note": "主动Cuu消息只 publish 到 topics.conversation(会话话题);文件头26-29行明确刻意不调 notifyConversationMessage、不落任何 notification(个人空间主区无 participant 行,扇出天然短路)" + }, + { + "file": "apps/desktop-webview/src/desktop-cuu-runtime.ts", + "line": 61, + "note": "桌宠 SSE worker『目前只订阅 /api/push/stream/me,收不到 conversation: 话题的事件』——桌宠永远看不到会话话题上的主动消息" + }, + { + "file": "apps/desktop-webview/src/workbench/rail.ts", + "line": 151, + "note": "rail 未读红点『实时性只靠一条 /api/push/stream/me 订阅:收到某会话新消息的 notification.created 时本地+1』;主动Cuu消息不产 notification,故红点永不+1" + }, + { + "file": "apps/api/src/services/ddl-chase.ts", + "line": 250, + "note": "cuuDeliveryEnabled 时 t1d/overdue 两档对有责任人的工作项改走 conversation_message 通道(个人空间可用即不降级到通知)" + }, + { + "file": "packages/config/src/env.ts", + "line": 148, + "note": "PROACTIVE_CUU_DELIVERY_ENABLED 默认 true——上述会话通道是默认行为,不是实验开关" + }, + { + "file": "apps/desktop-webview/src/workbench/rail.ts", + "line": 314, + "note": "个人空间项目行(未选中时)只渲 open_work_item_count>0 的小圆点,没有未读徽标;main 会话的 unread_count 只在该项目被选中(activeVm)时才随叶子渲出" + } + ], + "detail": "链路:pulse-scheduler 的 ddl-chase(默认开)对『有责任人』工作项的 T-1d/逾期两档,以及 care-scan 的全部关怀消息,都通过 proactive-intents 的 conversation_message 通道,由 proactive-cuu-delivery 直接 createCuuMessage 落进用户个人空间主区,并只向该会话话题广播。这条投递刻意不落 notification(见 delivery 文件头),也不向 topics.user(即 /stream/me)发任何事件。而三处呈现端都只吃 /stream/me 上的 notification.created:桌宠 runtime(desktop-cuu-runtime.ts:61 只订 /stream/me)、rail 未读红点(rail.ts:151 只靠 notification.created 本地+1)。个人空间项目行本身也不渲未读徽标(rail.ts:314),main 会话未读数只在进入该个人空间后才可见。综合结果:用户建过个人空间后,截止提醒(『明天就到期了』/『已经逾期了』)和关怀问候被静默塞进一个他可能几天都不打开的空间,桌宠——Cuu 唯一的常驻『主动开口』载体——一声不吭。追DDL的核心目的(主动把截止日顶到用户面前)在最常见场景(有个人空间、没盯着它看)下完全落空。逾期档的24h重复叮嘱阶梯(nextRemindAt)在会话通道下『天然不生效』(ddl-chase.ts:32-34注释),suppression_key=ddl:{id}:overdue 保证每件逾期只发一次,连补救的重复提醒也没了。关怀默认开(careWeeklyCap=2),但个人空间不可用即 suppressed 不降级,可用则静默投递——realized 到达率极低。缺的是:主动会话通道投递成功后向 topics.user 补一条轻量『你的空间有新动态』信号(或让桌宠/rail 订阅个人空间会话话题),让至少一个端能提示。", + "suggestion": "主动会话消息投递成功后,额外向 topics.user(targetUserId) 广播一条轻量提示事件(不等于系统OS通知,可做成桌宠轻点头/rail个人空间行红点),或让桌宠 runtime 与 rail 订阅当前用户个人空间会话话题;至少让追DDL的逾期档保留可感知的重复提醒。", + "dim": "cuu-proactive", + "merged_from": [ + "cuu-proactive:Cuu 主动会话消息(追DDL的t1d/逾期 + 全部关怀)投出去后没有任何端能提示——桌宠收不到、rail不亮红点、无系统通知,提醒静默丢失" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "证据逐条在 main 核实为真:主动 Cuu 消息仅广播 conversation 话题、不落 notification、不发 topics.user,而桌宠/rail/OS 三端都只吃 /stream/me 的 notification.created(源码注释自认),用户有个人空间但没盯着看时截止提醒与关怀确会静默投递、无任何端提示;但消息已落库、进空间即可见非数据丢失/崩溃,故降为 medium。" + } + }, + { + "title": "设置里『Cuu 主动性』三档是死控件,无任何代码读取,选『安静』照样收全量主动打扰", + "category": "fe-no-backend", + "surface": "desktop", + "severity": "low", + "evidence": [ + { + "file": "apps/desktop-webview/src/spotlight/views/settings.ts", + "line": 149, + "note": "proactivityValues=[quiet,balanced,proactive] 渲成可点 chip,点击 patchAiProfile({cuu_proactivity}) 真持久化;文案(94-100行)承诺 quiet=『很少主动开口,等你来问』" + }, + { + "file": "apps/api/src/services/ai-settings.ts", + "line": 282, + "note": "cuu_proactivity 仅被读出/回写返回,服务层无任何据它分支的行为" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 895, + "note": "cuu_proactivity 列 + check 约束存在,但全库只有 存/取/展示 三类引用" + }, + { + "file": "apps/api/src/services/proactive-intents.ts", + "line": 161, + "note": "主动打扰唯一闸门 recordAndDeliver 的频控只有『每人每日上限+用户级静音』,从不查 cuuProactivity;ddl-chase/care-scan/conversation-observer 同样零引用(全库 grep 无行为消费者)" + } + ], + "detail": "cuu_proactivity(R12 引入)在桌面设置里是一个可交互、会写库的三档选择器,文案明确承诺它控制 Cuu 主动开口的频率(quiet『很少主动开口,等你来问』)。但 R15 的主动性管线(pulse-scheduler → proactive-intents 闸 → ddl-chase/care-scan)完全另起一套频控(每日上限 dailyCapPerUser=10、关怀 opt-out、静默时段),从不读 cuu_proactivity;反应式的 conversation-observer 也不读。结果:用户把 Cuu 设成『安静』——正是在明确表达『别主动烦我』——却仍会照常收到追DDL的Cuu私聊和每周关怀。这是 R15 新建主动性系统时没有把既有的主动性偏好接进去的集成缺口,是一个『调了等于没调』的误导控件。缺的是:让 proactive-intents 闸(或各扫描任务)按 target 用户的 cuu_proactivity 调节频率/阈值(quiet 收紧甚至只留升级类、proactive 放宽)。", + "suggestion": "在 recordAndDeliver 或各 pulse 任务里按 target 用户的 cuu_proactivity 分档:quiet 降到只保留升级/找人这类必要打扰,proactive 放宽每日上限;或先把该控件在桌面标注为暂未生效,避免骗用户。", + "dim": "cuu-proactive", + "merged_from": [ + "cuu-proactive:设置里的『Cuu 主动性』三档(安静/均衡/主动)是死控件——没有任何代码读它,选『安静』照样收全量追DDL和关怀" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "跨所有可核对分支确认 cuu_proactivity 仅存/取/展示、agent/workers/services 无行为消费者,该偏好控件确有\"调了不生效\"的误导性(real 成立);但危害所依赖的 R15 投递管线(recordAndDeliver/ddl-chase/care-scan)在可访问的任何树中都不存在、无法坐实\"选安静照样收全量打扰\",且此偏好是否应节流 R15(已自带静音/每日上限等独立控件)属设计判断,故 medium 夸大,降为 low。" + } + }, + { + "title": "web 会话镜像把『找人』决策卡渲成标题张冠李戴的只读摘要,无 claim/reassign/defer", + "category": "be-no-frontend", + "surface": "web", + "severity": "low", + "evidence": [ + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 5176, + "note": "mirrorActionCardHtml 对所有 action_card 一律套死标题『Cuu 从讨论里拎出 N 件事』,item 只渲成静态 wh-mirror-clarify-opt 文字片,无任何 decide 按钮" + }, + { + "file": "apps/api/src/services/proactive-find-owner-card-delivery.ts", + "line": 74, + "note": "找人卡是一条 decide 条目(assignee=项目负责人,work_item=逾期无主项),桌面按同一VM渲成 claim/reassign/defer 三动作走 POST /decide" + }, + { + "file": "apps/api/src/services/ddl-chase.ts", + "line": 227, + "note": "无责任人的逾期项走 action_card 通道投到项目主区(needs_owner);项目主区可用即不降级到通知" + } + ], + "detail": "追DDL对『逾期且无人认领』的工作项,通过 action_card 通道在项目主区插一张系统 decide 卡(标题『XX 已逾期且无人认领』,三动作 claim/reassign/defer,发给项目负责人定夺)。桌面工作台按 VM 渲成可决策卡。但 web 会话镜像的 mirrorActionCardHtml 对任何 action_card 都硬套标题『Cuu 从讨论里拎出 N 件事 / Cuu pulled N items out of the discussion』,并把条目只渲成不可点的文字 chip——既没有决策按钮,标题也与找人卡语义完全不符(这卡根本不是从讨论里拎出来的)。项目负责人若在 web 上看到这张卡,会以为只是一条 Cuu 从讨论提炼的摘要,无法认领/改派/延期,找人卡的整个目的在 web 端落空。缺的是:web 镜像对 decide 类卡至少给出跳转到桌面/工作项去处理的入口,并按卡实际来源出对的标题。", + "suggestion": "mirrorActionCardHtml 按 item.kind/来源区分标题(找人卡用『逾期无人认领』等真实语义),对 decide 卡提供跳到工作项或提示『去桌面/工作项处理』的可点入口,而非套死『从讨论拎出』的只读摘要。", + "dim": "cuu-proactive", + "merged_from": [ + "cuu-proactive:web 会话镜像把『找人』决策卡渲成只读摘要且标题张冠李戴(『Cuu 从讨论里拎出N件事』),无 claim/reassign/defer 任何动作" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "证据全部核实无误:mirrorActionCardHtml(route-components.ts:5170-5184)对所有 action_card 硬套「从讨论里拎出N件事」标题且渲成无按钮只读chip,而找人卡确是走同款 action_card 通道的 decide 卡(delivery.ts:74/ddl-chase.ts:227),且 listMessages 按成员而非kind鉴权、项目负责人确能在 web 镜像看到该主区卡——标题张冠李戴属真误导;但 web 镜像本就是只读次要面(desktop卡+通知深链/workitems/:id 才是真决策入口)、条目真实标题仍以chip渲出,故降为 low。" + } + }, + { + "title": "web 端无进入个人空间/私聊的导航,web 用户够不到投给自己的主动消息", + "category": "missing", + "surface": "web", + "severity": "medium", + "evidence": [ + { + "file": "apps/desktop-webview/src/workbench/rail.ts", + "line": 361, + "note": "个人空间是桌面 rail 专属分组(renderPersonalSpaceSectionHtml,数据源 GET /api/me/personal-projects)" + }, + { + "file": "apps/web/src/routes.ts", + "line": 187, + "note": "web 有 /conversations/:id 只读镜像路由,但需已知会话 id" + }, + { + "file": "packages/ui/src/gold-path/product-shell.ts", + "line": 270, + "note": "web 外壳/导航(product-shell、render)对 personal/个人空间/DM/会话入口零引用——没有任何链到个人空间主区的入口" + } + ], + "detail": "个人空间(主动Cuu消息的落点)只有桌面 rail 有创建入口和导航;web 是项目中心式外壳,product-shell/render 里完全没有个人空间、私聊或会话列表的导航 chrome,只有一个需要预先知道会话 id 的 /conversations/:id 只读镜像。于是:一个在桌面建过个人空间、日常用 web 的用户,他的追DDL(t1d/逾期走Cuu会话)和关怀消息被投进 web 根本无法导航到的个人空间;又因会话通道刻意不产通知(无 targetUrl 可点),web 用户永远看不到这些主动消息。缺的是:web 侧的个人空间/主动动态入口(哪怕先做成一个『我的空间』只读镜像列表)。", + "suggestion": "web 外壳补一个『我的空间/主动动态』入口(列出个人空间主区会话、复用现有 /conversations 镜像),让 web 用户至少能读到 Cuu 投给自己的主动消息。", + "dim": "cuu-proactive", + "merged_from": [ + "cuu-proactive:web 端没有任何进入个人空间/私聊的导航,web 用户根本够不到投给他的主动 Cuu 消息" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "在 r18/h1-web-members 集成树三条证据行号全精确吻合,且 proactive-cuu-delivery.ts:26-28 刻意不产通知、proactive-intents.ts 会话通道投成后不降级发通知,web 的 product-shell/render/productNavGroups 对 personal/conversation 零引用、无任何 /api/me/personal-projects 消费方,个人空间项目又被 /api/projects 过滤——web 用户确实无路径读到投给自己个人空间的主动 Cuu 消息,缺口真实且非裁除项。" + } + }, + { + "title": "唯一的主动性透明窗口(军团后台)桌面独占且内容单薄,web 有端点无渲染", + "category": "be-no-frontend", + "surface": "desktop", + "severity": "low", + "evidence": [ + { + "file": "apps/desktop-webview/src/workbench/army/render.ts", + "line": 440, + "note": "renderArmyBackgroundProactiveHtml 只渲 kind 标签+stage+delivered/suppressed 状态,不含消息正文/工作项链接/任何动作" + }, + { + "file": "apps/api/src/routes/conversation-army.ts", + "line": 58, + "note": "GET /api/army/background 端点存在(供 listRecentProactiveIntentsForUser)" + }, + { + "file": "packages/ui/src/gold-path/route-components.ts", + "line": 5346, + "note": "web 侧无 army background 渲染(product-shell/route-components 对 armyBackground 零引用),web 用户看不到主动性动态" + } + ], + "detail": "listRecentProactiveIntentsForUser 已把当前用户最近的 delivered+suppressed 主动意图取出,但唯一的呈现是桌面军团后台的『主动性动态』小节,且只渲 kind/stage/状态(delivered/suppressed),既看不到 Cuu 到底说了什么、指向哪件事,也无从据此复看或调节。web 端有 /api/army/background 端点却无任何渲染。结果:用户几乎没有窗口理解 Cuu 主动做了什么、为什么某条被 suppressed(每日上限/静音),更谈不上从这里反馈。这是 finding 1『投递静默』之外的另一层——连事后审计透明度也很薄。缺的是:把主动性动态做成可读正文+可跳工作项/会话+可调节的面板,并让 web 也能看。", + "suggestion": "把主动性动态扩成含摘要文本、跳转与静音动作的面板,并在 web 复用同一端点渲染,给用户一个统一的『Cuu 最近替我操了哪些心』视图。", + "dim": "cuu-proactive", + "merged_from": [ + "cuu-proactive:唯一的主动性透明窗口(军团后台『主动性动态』)是桌面独占且内容单薄,web 有端点无渲染" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "核对属实:desktop army/render.ts 的主动性动态区仅渲 kind/stage/status/时间(VM 甚至丢弃了 repo 已取的 workItemId,无正文无跳转无动作),而 gold-path 全库对 armyBackground/proactive 零引用,/api/army/background 端点存在却无 web 渲染,系真实的端到端透明度缝隙,low 诚实。" + } + }, + { + "title": "项目树主区/协同会话未读只本地+1,同项目内不与服务端对账、重连不补缺(仅 DM 有兜底)", + "category": "dataflow", + "surface": "cross", + "severity": "medium", + "detail": "权威未读 unread_count 只在 selectProject() 拉 workbench VM 时到手(shell.ts:784,唯一的 pages.workbench 调用点)。停在同一个项目期间,主区/协同会话树叶的未读角标只靠 /me 流的 notification.created 做本地 +1(shell.ts:702 bumpConversationUnreadInVm),既没有周期性重拉 VM,也不会在 /me 流重连后补缺口:meStream 只配了 url/getClientToken/onEvent,没有 onReconnected 回调(shell.ts:665-715),而 broker 明确 resume_mode:\"fresh\"、不重放漏事件(stream.ts:7-9)。rail 的 30s 轮询只调 loadDms(rail.ts:1728-1732),只把 DM 行未读拉回权威——rail.ts:149-153 注释却宣称『30s 会话列表兜底刷新』覆盖了会话未读,对主区/协同叶其实是空头支票。复现:项目 P 里协同会话 B 收到消息时你正开着会话 A → B 叶 +1;期间 /me 流抖动重连一次(漏掉 B 的若干 notification.created,或某条因在线抑制根本没发通知),B 的角标就永久少算,只有切到别的项目再切回 P 才会被新 VM 纠正。DM 未读能自愈、主区/协同不能,同一『未读』事实两套新鲜度。", + "evidence": [ + { + "file": "apps/desktop-webview/src/workbench/rail.ts", + "line": 1728, + "note": "30s 轮询只 loadDms(),不重拉 workbench VM" + }, + { + "file": "apps/desktop-webview/src/workbench/rail.ts", + "line": 153, + "note": "注释声称 30s 会话列表兜底刷新会话未读——对主区/协同叶不成立" + }, + { + "file": "apps/desktop-webview/src/workbench/shell.ts", + "line": 784, + "note": "pages.workbench 唯一调用点,仅 selectProject 触发;无周期/重连重拉" + }, + { + "file": "apps/desktop-webview/src/workbench/shell.ts", + "line": 665, + "note": "meStream 无 onReconnected,重连不补缺口" + }, + { + "file": "apps/desktop-webview/src/workbench/chat/stream.ts", + "line": 7, + "note": "broker resume_mode:fresh,不重放漏事件" + } + ], + "suggestion": "给 workbench VM 加 30s/重获焦点的周期性重拉,或给 meStream 配 onReconnected 走 GET attention/未读聚合补缺口,让主区/协同叶与 DM 同源自愈。", + "dim": "dataflow-consistency", + "merged_from": [ + "dataflow-consistency:项目树会话未读角标只本地 +1,同项目内永不与服务端对账(仅 DM 有 30s 兜底)" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "在 r18/h1-web-members 逐行核实:meStream(shell.ts:665-715)确无 onReconnected 而该回调是既有能力(stream.ts:34/226)、pages.workbench 唯一调用点在 selectProject(shell.ts:784)无周期/重连/焦点重拉、rail 30s 轮询只 loadDms 刷 DM 不碰 workbench VM(rail.ts:1019/1728)、broker resume_mode:fresh 不回放(stream.ts:7),主区/协同叶未读只本地+1不与服务端对账、重连不补缺,与 DM 自愈形成真实新鲜度不一致,证据全部属实。" + } + }, + { + "title": "读完会话只推进读游标不清 notification 未读,Dock/通知中心长期虚高", + "category": "dataflow", + "surface": "cross", + "severity": "medium", + "detail": "每收到一条会话消息(未在看时)都会落一条 conversation.message 通知、severity=normal(notifications.ts:507,528),它计入 GET /api/notifications 的 counts.unread。但在工作台打开会话读消息只调 PUT /conversations/:id/read → advanceReadCursor,只推进读游标 + 发 conversation.read.updated,完全不碰 notifications(conversation-read.ts:42-47;conversations.ts:1533-1553)。chat 视图里唯一会 POST /notifications/:id/read 的是 dispatch-ask 追赶条、且要用户点一下(view.ts:1088)。而 Dock/托盘角标 = attention.queue + notifications.counts.unread(browser.ts:1382)。复现:别人给你发 3 条团队/私聊消息(你不在看那条会话)→ Dock 角标 +3、web/主窗 /notifications 列出 3 条未读;你在工作台把会话点开读完 → rail 叶红点归零、别人看到你『已读 N/M』,但 Dock 角标仍显示 3、通知中心仍标未读,非得再跑去主窗通知中心手动标已读才消。同一批消息存在『读游标已读』与『通知未读』两套自相矛盾的未读真相。", + "evidence": [ + { + "file": "apps/api/src/routes/conversation-read.ts", + "line": 42, + "note": "PUT /read 只 advanceReadCursor,无 notifications 联动" + }, + { + "file": "apps/api/src/services/conversations.ts", + "line": 1533, + "note": "advanceReadCursor 只推进游标 + publishReadUpdated,不 markRead 通知" + }, + { + "file": "apps/api/src/services/notifications.ts", + "line": 528, + "note": "conversation.message 通知 severity=normal,计入 counts.unread" + }, + { + "file": "apps/desktop-webview/src/browser.ts", + "line": 1382, + "note": "Dock 角标 = approvals + notifications.counts.unread" + }, + { + "file": "apps/desktop-webview/src/workbench/chat/view.ts", + "line": 1088, + "note": "唯一的 notification mark-read 只针对 dispatch-ask 追赶条且需手动点" + } + ], + "suggestion": "读游标推进到会话末尾时,服务端顺带把该会话该用户的 conversation.message/mention 通知按 conversation_id 批量标已读(或从 counts.unread 口径中扣除已读会话),让 Dock/通知中心与读游标同源。", + "dim": "dataflow-consistency", + "merged_from": [ + "dataflow-consistency:在工作台读完会话只推进读游标,不清 notification 未读——Dock 角标与通知中心长期虚高" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "在 main 逐点核实:advanceReadCursor(conversations.ts:1532)只推进游标+发 read.updated 全不碰通知,无任何订阅 read.updated 或按 conversation_id 批量标已读的对端;conversation.message(notifications.ts:497)severity=normal 计入 counts.unread(:582);Dock 角标=approvals+counts.unread(browser.ts:1382);chat 唯一 mark-read 仅针对追赶条需手点(view.ts:1088)——读完会话确实不清通知未读、Dock/通知中心长期虚高;仅\"+3\"表述因 dedupe 折叠成 1 条略有出入但不损核心,medium 诚实。" + } + }, + { + "title": "在线状态两套各自轮询来源(聊天条 vs rail/资料卡),同一人两处圆点打架", + "category": "dataflow", + "surface": "desktop", + "severity": "medium", + "detail": "chat 视图维护自己的局部 onlineUserIds(view.ts:658),由它自己的 30s 定时器 loadPresence 刷新(view.ts:1568-1577、3772),SSE 重连时还会额外即时补一次(view.ts:2697-2698)——聊天成员条和 DM 头像的绿点用它。rail 名册/私聊行、以及头像资料卡(profile-popover.ts:49 读 state.onlineUserIds)用的是另一套 store.onlineUserIds,由 rail 独立的 30s loadDms→loadPresence 写入(rail.ts:1047-1071)。两个消费者各自打 GET /api/presence、相位不同、且 chat 视图重连后的更鲜活在线态从不回写 store。复现:对方刚上线,你在 DM 里(chat 视图这一侧因重连已刷到『在线』绿点),但左栏那条 DM 行/资料卡(store 侧)还停在离线,最长可差 30s(chat 重连后甚至更久,直到 rail 下一拍)。同一个人、同一时刻,界面上两颗自相矛盾的在线点。", + "evidence": [ + { + "file": "apps/desktop-webview/src/workbench/chat/view.ts", + "line": 658, + "note": "chat 视图私有 onlineUserIds,独立于 store" + }, + { + "file": "apps/desktop-webview/src/workbench/chat/view.ts", + "line": 3772, + "note": "chat 视图自带 30s presence 轮询 + 重连即时刷新" + }, + { + "file": "apps/desktop-webview/src/workbench/rail.ts", + "line": 1047, + "note": "rail 另一套 loadPresence 写 store.onlineUserIds" + }, + { + "file": "apps/desktop-webview/src/workbench/profile-popover.ts", + "line": 49, + "note": "资料卡在线态取 store.onlineUserIds,与聊天成员条不同源" + } + ], + "suggestion": "统一 presence 为单一来源(chat 视图刷到的在线态也写回 store.onlineUserIds,或都从 store 读),避免同一 user 的在线点在两个界面间漂移。", + "dim": "dataflow-consistency", + "merged_from": [ + "dataflow-consistency:在线状态有两个各自轮询的来源:聊天成员条/DM 头 vs rail 名册/私聊行/资料卡,同一人两处圆点会打架" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "源码逐行核实无误:view.ts:658 私有 onlineUserIds 全文件从不回写 store,自带 30s 轮询(3776)+重连即刷(2698);rail.ts:1047 另写 store.onlineUserIds 由独立 30s 轮询驱动;profile-popover.ts:49 读 store——两套相位不同的 presence 来源确实会让同一人在聊天条与 rail/资料卡两处在线点漂移最长约30s,属真实且非裁除项的 dataflow 一致性缝隙,medium 恰当。" + } + }, + { + "title": "全新私聊要等 30s 轮询才进左栏(me 流 bump 对未知会话是空操作)", + "category": "dataflow", + "surface": "desktop", + "severity": "low", + "detail": "/me 流收到会话消息通知时对 DM 未读的处理是 bumpDmUnread(dmList, conversationId)(shell.ts:707),而 bumpDmUnread 在 dmList 里找不到该会话时原样返回、什么也不做(rail.ts:205-211);此路径也不会去 ensureDmListLoaded 把这条新 DM 拉进来。DM 消息 severity=normal 又不触发 OS 通知(notify.rs 只对 high/urgent 出通知)。复现:某人第一次给你发私聊 → 你此刻左栏私聊分组里还没有这条会话 → me 流 bump 空转、不建行、不出角标、无 OS 通知 → 这条 DM 与它的未读红点最长要等 rail 的 30s loadDms 轮询(rail.ts:1728)才冒出来。新私聊的到达在左栏有最长 30s 的盲区。", + "evidence": [ + { + "file": "apps/desktop-webview/src/workbench/shell.ts", + "line": 707, + "note": "me 流对未知会话只 bumpDmUnread,无补拉列表" + }, + { + "file": "apps/desktop-webview/src/workbench/rail.ts", + "line": 205, + "note": "bumpDmUnread 找不到会话即原样返回(空操作)" + }, + { + "file": "apps/desktop-webview/src/workbench/rail.ts", + "line": 1728, + "note": "新 DM 只能靠 30s loadDms 才进列表" + } + ], + "suggestion": "me 流拿到带 conversation_id 的会话消息通知、但 dmList 里没有该会话时,触发一次 ensureDmListLoaded/refreshDms,让新私聊即时上墙。", + "dim": "dataflow-consistency", + "merged_from": [ + "dataflow-consistency:全新进来的私聊要等 30s 轮询才出现在左栏(me 流 bump 是空操作、无 OS 通知)" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "经核对:rail.ts:205-211 的 bumpDmUnread 对未在 dmList 的会话确为空操作,shell.ts:707 me流 conversation.message 分支只 bump 不补拉 ensureDmListLoaded,notify.rs urgency_for_event 对 conversation.message 返回 None 故 normal DM 不出 OS 通知,唯一补拉是 rail.ts:1732 的 30s loadDms 轮询——首条私聊左栏 ≤30s 盲区成立,非裁除项,low 诚实(最终一致无数据丢失)。" + } + }, + { + "title": "隐藏的主窗口无法靠点 Dock 图标恢复(缺 RunEvent::Reopen 处理)", + "category": "missing", + "surface": "desktop", + "severity": "medium", + "detail": "The main window has decorations:false (tauri.conf.json:32) and a custom in-app close button (browser.ts:239/291) whose handler invokes hide_main_window (dismissMainWindow → dismissDesktopMainWindow, browser.ts:1235); Cmd+W is likewise intercepted and turned into a hide (main.rs:1423-1428 prevent_close + window.hide()). So users routinely hide the main window. But main() ends with `.run(tauri::generate_context!())` (main.rs:1515) and registers NO RunEvent callback, so macOS `RunEvent::Reopen` (Dock-icon click / applicationShouldHandleReopen) is never handled. After hiding the window, clicking the app's Dock icon — the single most natural way to bring a macOS app back — does nothing. The window is only recoverable via the menu-bar tray, a deep link, or a notification. This is a standard-behavior violation users will hit constantly.", + "suggestion": "Switch to `.build(context)?.run(|app, event| …)` and handle `RunEvent::Reopen { .. }` by calling show_main_window (show + set_focus); also consider showing the window on macOS Dock activation.", + "evidence": [ + { + "file": "client-tauri/src-tauri/src/main.rs", + "line": 1515, + "note": ".run(generate_context!()) with no RunEvent handler — Reopen unhandled" + }, + { + "file": "client-tauri/src-tauri/src/main.rs", + "line": 1424, + "note": "main window CloseRequested → prevent_close + hide (routine hide-to-tray)" + }, + { + "file": "apps/desktop-webview/src/browser.ts", + "line": 1235, + "note": "in-app close button hides the main window via hide_main_window" + } + ], + "dim": "native-shell", + "merged_from": [ + "native-shell:Closed/dismissed main window can't be recovered by Dock-icon click (no RunEvent::Reopen handler)" + ], + "verdict": { + "real": true, + "reason": "证据全部核实:tauri.conf.json:32 decorations:false、main.rs 主窗 CloseRequested→hide 收托盘、main.rs:1515 裸 .run(generate_context!()) 且 src/ 内无任何 Reopen 处理,故隐藏后点 Dock 图标确无响应,仅托盘/深链/通知可恢复,medium 合理。", + "adjusted_severity": "medium" + } + }, + { + "title": "OS 通知点击不打开目标页(路由被算出但无生产消费者)", + "category": "dataflow", + "surface": "native", + "severity": "medium", + "detail": "The Rust SSE worker shows real OS notifications for urgent events via `notifications.builder().title().body().show()` (notify.rs:126-131, called at sse_worker.rs:237) — with NO action identifier or click handler. The carefully-computed target route + window_control (notify.rs:154-176, e.g. permission.ask → /approvals?approvalId=…) travels only inside the `system-notification` Tauri event emitted at sse_worker.rs:235. But the only production binding of the shell runtime, pet-surface.ts:1545 `bindDesktopShellCuuRuntime({...})`, does NOT pass an `onSystemNotification` callback (the main-window entry in browser.ts never binds the runtime at all), so shell-events.ts:294 / desktop-cuu-runtime.ts:904 forward the plan to a no-op. Net: clicking an urgent 'Cuu needs your approval' notification does not deep-link to the approval — at best macOS activates the app, but with the main window hidden and no Reopen handler nothing appears. The proactive-push→act loop dead-ends. The route is also only delivered at notification-creation time, not on click, so it can't represent a user click even if consumed.", + "suggestion": "Wire the native notification click to focus_main_route/show_main_window — e.g. use tauri-plugin-notification action/permissions to receive click events (or register the plan's route as a pending deep link), so clicking the OS notification shows the main window and navigates to plan.route.", + "evidence": [ + { + "file": "client-tauri/src-tauri/src/notify.rs", + "line": 126, + "note": "notification built with title/body only — no action/click handler" + }, + { + "file": "client-tauri/src-tauri/src/sse_worker.rs", + "line": 235, + "note": "route-bearing system-notification event emitted; show_system_notification at :237" + }, + { + "file": "apps/desktop-webview/src/pet-surface.ts", + "line": 1545, + "note": "sole runtime binding omits onSystemNotification → route dropped in production" + } + ], + "dim": "native-shell", + "merged_from": [ + "native-shell:OS notification click does not open the target screen (route computed but has no production consumer)" + ], + "verdict": { + "real": true, + "reason": "证据全部核实:notify.rs只.show()无action/click handler、system-notification事件在创建时(sse_worker.rs:235)携route发出而非点击时、唯一生产绑定pet-surface.ts:1545漏传onSystemNotification致路由丢弃、主窗关闭即hide且无Reopen handler,OS通知点击确实无法深链且非裁除项,medium诚实。", + "adjusted_severity": "medium" + } + }, + { + "title": "原生外壳 locale 启动即冻结,应用内切语言到不了外壳(托盘/通知仍旧语言)", + "category": "dataflow", + "surface": "desktop", + "severity": "medium", + "detail": "The WorkHubLocale mutex is set exactly once in setup from the config file/env (main.rs:1432-1434), and the tray menu labels + tooltip are built once from it (install_workhub_tray, main.rs:1446). There is NO command to update the shell locale at runtime — the invoke_handler list (main.rs:1494-1514) has no locale setter — so current_workhub_locale() always returns the launch-time value, driving tray menu text, deep-link error messages, and OS-notification fallback strings. Meanwhile the webview language switch only writes localStorage and reloads (browser.ts:1321-1332) and cannot write workhub-shell-config.json (the webview has no fs capability — capabilities/default.json grants only core:default + start-dragging). Result: a user who switches WorkHub to English still gets a Chinese tray menu, Chinese tooltip, and Chinese notification fallback text — and because the two sides persist locale differently (config JSON vs localStorage), the mismatch survives restarts until the config file is hand-edited.", + "suggestion": "Add a set_shell_locale command that updates the WorkHubLocale state and rebuilds/relabels the tray menu + tooltip; have the webview call it (and/or persist locale into workhub-shell-config.json) whenever the in-app language changes.", + "evidence": [ + { + "file": "client-tauri/src-tauri/src/main.rs", + "line": 1432, + "note": "locale set once in setup; never updated at runtime" + }, + { + "file": "client-tauri/src-tauri/src/main.rs", + "line": 1494, + "note": "invoke_handler has no locale command bridging webview→shell" + }, + { + "file": "apps/desktop-webview/src/browser.ts", + "line": 1327, + "note": "language switch writes localStorage + reload only; cannot reach shell" + } + ], + "dim": "native-shell", + "merged_from": [ + "native-shell:Native shell locale is frozen at launch; in-app language toggle never reaches the shell" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "证据全部核实:WorkHubLocale mutex 仅 setup 从 config 写一次(main.rs:1433)、invoke_handler(1494-1514)无 locale setter、current_workhub_locale 只读该冻结值驱动托盘/深链/通知文案;webview 切语言(browser.ts:379-382)只写 localStorage+服务端 prefs+reload,capabilities 仅 core:default 无 fs,确实到不了原生壳,切英文后托盘/通知仍中文且重启后不一致。" + } + }, + { + "title": "Spotlight 式启动器没有全局唤起热键", + "category": "missing", + "surface": "desktop", + "severity": "medium", + "detail": "The main window is explicitly built as an Apple-Spotlight-style launcher — positioned top-center on startup (position_main_window_top_center, main.rs:1094-1113), grows/shrinks with content (clamp_spotlight_size / set_spotlight_size), and hides to tray on close. Yet there is no global shortcut: Cargo.toml (deps at :11-23) does not include tauri-plugin-global-shortcut, and no shortcut is registered anywhere. Once the window is hidden (which happens routinely via Cmd+W or the in-app close button), there is no keyboard way to summon it — defeating the core affordance of the Spotlight metaphor it emulates (Cmd+Space). The only re-entry is a menu-bar tray click.", + "suggestion": "Add tauri-plugin-global-shortcut, register a configurable summon accelerator, and bind it to show_main_window; expose the binding in settings.", + "evidence": [ + { + "file": "client-tauri/src-tauri/Cargo.toml", + "line": 16, + "note": "no tauri-plugin-global-shortcut among dependencies" + }, + { + "file": "client-tauri/src-tauri/src/main.rs", + "line": 1094, + "note": "main window framed/positioned as a Spotlight launcher that hides to tray" + } + ], + "dim": "native-shell", + "merged_from": [ + "native-shell:Spotlight-style launcher has no global summon hotkey" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "核实无误:Cargo.toml 无 global-shortcut 插件、全库无热键注册,主窗为 Spotlight 顶部居中且 CloseRequested 拦截为隐藏收托盘,再入口仅托盘菜单,缺 Cmd+Space 式全局唤起确成立且非裁除项,medium 合理。" + }, + "lead_verdict": { + "withdrawn": true, + "reason": "tauri-plugin-global-shortcut 2.3.2 在 Cargo.toml:19,main.rs:841/1668 注册 Option+Space——能力已存在(R15 热键批)" + } + }, + { + "title": "桌面应用缺自动更新、开机自启与崩溃/panic 上报", + "category": "missing", + "surface": "desktop", + "severity": "low", + "detail": "Standard shipping-desktop capabilities are absent: Cargo.toml (:11-23) has no tauri-plugin-updater / tauri-plugin-autostart / tauri-plugin-log; the bundle config (tauri.conf.json:62-75) declares no updater endpoints or createUpdaterArtifacts; and there is no panic hook — startup and worker errors are written with eprintln! to stderr (e.g. main.rs:1519, sse_worker.rs:238), which is invisible in a bundled .app. An old plan deferred these (F11-headless-daemon-client-rewire-plan.md:36/62 push '自动更新、autostart' to P1+), but for a release-hardening pass the shell still cannot self-update, start with the OS (needed for the always-on proactive/SSE/notification loop), or leave a diagnosable crash trail.", + "suggestion": "Add tauri-plugin-updater with signed artifacts + endpoint, tauri-plugin-autostart (opt-in from settings), and a file/log sink (tauri-plugin-log) plus a panic hook so field crashes are recoverable/diagnosable.", + "evidence": [ + { + "file": "client-tauri/src-tauri/Cargo.toml", + "line": 11, + "note": "no updater/autostart/log plugins in dependency set" + }, + { + "file": "client-tauri/src-tauri/tauri.conf.json", + "line": 62, + "note": "bundle config declares no updater artifacts/endpoints" + }, + { + "file": "docs/plans/p0-foundation/F11-headless-daemon-client-rewire-plan.md", + "line": 62, + "note": "auto-update + autostart wiring explicitly deferred to P1+" + } + ], + "dim": "native-shell", + "merged_from": [ + "native-shell:No auto-update, launch-at-login, or crash/panic reporting for the desktop app" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "核对属实:Cargo.toml(:11-23)无 tauri-plugin-updater/autostart/log,tauri.conf.json bundle 无 updater 端点,src 内无 panic::set_hook,错误全走 eprintln 到 stderr(main.rs:1519、sse_worker.rs:238),这些出货级桌面能力确实缺失,severity=low 诚实。" + } + }, + { + "title": "外壳声明拥有 local_file_sync 却无实现(spec_watch.rs 不存在)", + "category": "missing", + "surface": "desktop", + "severity": "low", + "detail": "lib.rs:16-32 RUST_SHELL_OWNS lists 'local_file_sync' as a capability owned by the native shell, and the product glossary names client-tauri/src-tauri/src/spec_watch.rs as the foundation for spec-folder↔server sync (glossary-dejargon.md:85). But no such module exists — `find client-tauri -name spec_watch*` returns nothing, and there is no filesystem watcher, inotify, or sync code anywhere in src-tauri/src (only the lib.rs string references it). Either the contract/doc is stale or a promised native capability (local spec/file sync) was dropped. As-is, the shell claims to own a feature it does not implement, which will mislead anyone relying on RUST_SHELL_OWNS as the source of truth for shell responsibilities.", + "suggestion": "Either implement the local file/spec sync module or remove 'local_file_sync' from RUST_SHELL_OWNS and correct the glossary reference to spec_watch.rs.", + "evidence": [ + { + "file": "client-tauri/src-tauri/src/lib.rs", + "line": 31, + "note": "'local_file_sync' declared as shell-owned with no backing module" + }, + { + "file": "docs/workhub/00-overview/glossary-dejargon.md", + "line": 85, + "note": "doc names client-tauri/.../spec_watch.rs as the sync foundation — file does not exist" + } + ], + "dim": "native-shell", + "merged_from": [ + "native-shell:Shell advertises `local_file_sync` ownership but no implementation (spec_watch.rs) exists" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "核实无误:lib.rs:31 确列 local_file_sync 为壳所有,而 src-tauri/src 下无 spec_watch.rs/sync.rs、无任何 watcher 或双向同步实现,glossary(85/153/261)仍把不存在的文件当同步地基,属真实的契约/文档陈旧,low 级恰当。" + } + }, + { + "title": "没有人发起的『指派/认领工作项』动作,归属只能靠系统主动发的找人卡被动落地", + "category": "missing", + "surface": "cross", + "severity": "medium", + "detail": "经理旅程「派发」与成员旅程「领任务」都撞墙。工作项的人类归属只有两条列:多角色的 work_item_assignments 表 与 单认领人 claimedByUserId。work_item_assignments 在整个生产代码里【只被 SELECT、从不 INSERT】——唯一的两处 insert 都在测试/QA(workbench-repository.test.ts:520、qa/r14-search-smoke.ts:151),即富模型(lead/collaborator/reviewer)是死列。claimedByUserId 的唯一写入口是 claimOwnerlessWorkItem(work-items.ts:310/781),而它只被 R15「找人交互卡」的 decide(claim=认领给自己 / reassign=改派他人)调用;action-cards 路由只暴露 /decide 与 /undo(action-cards.ts:62,74),找人卡本身只由 proactive-intents 主动扫描管线生成(proactive-find-owner-card-delivery.ts)。结论:经理建任务/审批计划草案落地工作项时无法「指派给张三」,成员也无法从一个待领池里主动认领某条任务——只能等系统碰巧对某条任务发出找人卡时才谈得上归属。workitems POST 创建请求(workitems.ts:167)也不接受 assignee 字段。", + "suggestion": "补一个人发起的指派/认领端点(如 POST /workitems/:id/assign 与 /claim),在工作项详情与看板卡上给「指派」「认领」按钮;或至少允许经理在计划草案 materialize 时为每条工作项选负责人。", + "evidence": [ + { + "file": "packages/db/src/repositories/work-items.ts", + "line": 310, + "note": "claimOwnerlessWorkItem 是 claimedByUserId 唯一写入口,注释明说只服务「找人卡」的 claim/reassign" + }, + { + "file": "apps/api/src/routes/action-cards.ts", + "line": 62, + "note": "action-card 路由只有 /decide 与 /undo(74),人无法主动创建找人卡或直接指派" + }, + { + "file": "apps/api/src/services/proactive-find-owner-card-delivery.ts", + "line": 85, + "note": "找人卡仅由主动扫描管线投递,归属完全是被动触发" + }, + { + "file": "apps/api/src/routes/workitems.ts", + "line": 167, + "note": "POST /workitems 创建时不接受负责人字段,work_item_assignments 生产零 INSERT" + } + ], + "dim": "product-missing", + "merged_from": [ + "product-missing:没有「把任务派给某个人 / 认领待办」的直接动作——工作项归属只能靠系统主动发的「找人卡」被动落地" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "在R19审查检出(main含R15-R18)核实:work_item_assignments生产零INSERT、claimedByUserId唯一写口claimOwnerlessWorkItem只服务找人卡、action-cards仅/decide+/undo、POST /workitems无assignee字段、全routes无人工assign/claim端点,人发起的指派/认领动作确实缺失,归属只能靠主动找人卡被动落地。" + } + }, + { + "title": "项目没有关闭/归档/删除,archived/deletedAt 列从无写路径", + "category": "missing", + "surface": "cross", + "severity": "medium", + "detail": "projects 表已经预留了 archived 布尔列和 deletedAt 软删列,而且列表/时间线/网盘等所有查询都在 WHERE 里过滤 archived=false ∧ deletedAt IS NULL(如 projects.ts 仓库多处)——说明产品设计预期项目可归档。但生产代码里【从没有任何一条路径把 archived 置 true 或写 deletedAt】:唯一置 archived:true 的两处都在测试(workbench-repository.test.ts:766、work-items-service.test.ts:2065)。projects 路由整个文件只注册 2 条:GET /(列表)和 POST /bootstrap(建/复用),没有归档/关闭/删除/改名。ProjectRepository 接口的写方法只有 bootstrapPilotProject、bootstrapPersonalProject、updateInstructions(projects.ts:85-92),没有 archive/delete/rename。经理做完一个项目想收尾归档、或删掉一个建错的项目,界面与后端都无从下手,项目列表只会无限增长。", + "suggestion": "补 POST /projects/:id/archive 与 /delete(软删),把 archived 写路径接上;在项目主页/设置给「归档项目」「删除项目」;列表加已归档分区。", + "evidence": [ + { + "file": "apps/api/src/routes/projects.ts", + "line": 35, + "note": "projects 路由只有 GET /(35)与 POST /bootstrap(44),grep 全文件仅 2 条 route 注册,无归档/删除/改名" + }, + { + "file": "packages/db/src/repositories/projects.ts", + "line": 92, + "note": "仓库写方法仅 bootstrap*/updateInstructions,无 archive/delete;archived 列(27)只被读取过滤" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 326, + "note": "projects 表有 archived 与 deletedAt 列,列表查询全程过滤它们,但生产无任何写 true 的路径" + } + ], + "dim": "product-missing", + "merged_from": [ + "product-missing:项目没有关闭/归档/删除:经理旅程「项目收尾」无落点,项目永远堆积" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "证据全部核实:projects 路由仅 GET/与 POST/bootstrap 两条,仓库无 archive/delete/rename 写方法,全库对 projects.archived/deletedAt 皆为 WHERE 过滤读取无写 true 路径,project-home-pages.ts:176 注释更自认 archived 分支「暂不可达」为将来预留——项目收尾无落点属真实 medium 缺口。" + } + }, + { + "title": "文件在线预览只支持纯文本,图片/PDF/Office 一律只能下载", + "category": "missing", + "surface": "cross", + "severity": "medium", + "detail": "预览闸门 isTextPreview/isDriveTextPreview 只放行 text/*、json、xml、yaml 及 .csv/.md/.txt/.tsv/.html 等纯文本(drive-pages.ts:230)。三个预览端点全用同一闸门,非文本一律 415 拒绝并提示「请下载查看」:网盘文件预览(drive.ts:390 → drive_preview_unsupported)、工作项交付物预览(workitems.ts:227 → deliverable_preview_unsupported)、提议变更预览(proposals.ts:872 → proposal_change_preview_unsupported)。而系统本身认识 image/png、image/jpeg、image/gif、application/pdf 这些 mime(proposals.ts:280-287,用于下载头),说明这些文件会真实存在于网盘/交付物里。对一个 AI 军团产出交付物、以文件为核心的产品,经理审阅一张设计图/一份 PDF 报告、成员查看图片交付物时,都只能被迫下载到本地再打开,无法在产品内预览。", + "suggestion": "至少为 image/* 与 application/pdf 加内联预览(图片直出、PDF 用 data-uri 或分页渲染);预览端点按 mime 分派 preview_type 而非只 text。", + "evidence": [ + { + "file": "apps/api/src/services/drive-pages.ts", + "line": 230, + "note": "isDriveTextPreview 只放行文本族 mime/扩展名,图片/PDF 全落到不支持分支" + }, + { + "file": "apps/api/src/routes/drive.ts", + "line": 390, + "note": "网盘预览:非文本 415 drive_preview_unsupported「请下载查看」" + }, + { + "file": "apps/api/src/routes/workitems.ts", + "line": 227, + "note": "交付物预览同一闸门 → deliverable_preview_unsupported(230)" + }, + { + "file": "apps/api/src/routes/proposals.ts", + "line": 872, + "note": "提议变更预览同款 415 proposal_change_preview_unsupported" + } + ], + "dim": "product-missing", + "merged_from": [ + "product-missing:文件在线预览只支持纯文本——图片、PDF、Office 一律只能下载" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "三个预览端点(drive.ts:390、workitems.ts:227、proposals.ts:645)均经同款文本闸门,非文本 mime 一律 415 拒绝且前端 preview_type 仅 text、图片/PDF 只能下载,内联预览对端确不存在,属真实产品缺口,个别行号偏移不影响成立。" + } + }, + { + "title": "没有工作区级审计/操作日志界面,审计只能逐个工作项翻", + "category": "missing", + "surface": "cross", + "severity": "medium", + "detail": "审计日志仓库只提供两个读方法:listAuditLogsForEntity 与 listAuditLogsForWorkItem(audit.ts:54,55),没有按工作区/按用户/按时间范围的列表方法。审计路由也只暴露 GET /workitems/:id/audit(audit.ts:80)——即审计只能逐个工作项地看。而写审计时是带 workspaceId 的(如 auth.ts:826 user_deactivated、审批/指派/停用等都落审计),数据存在,只是没有汇总读出口。工作区管理员想做治理审计(谁邀请了谁、谁改了谁的角色、谁批准了哪次合并、谁停用了谁),没有任何「工作区活动日志/操作审计」页面可以纵览,只能一个工作项一个工作项翻。", + "suggestion": "补 listAuditLogsForWorkspace(带 actor/action/时间范围过滤)+ 一个管理员审计页(/settings 下或独立路由),按时间倒序展示全工作区关键操作。", + "evidence": [ + { + "file": "packages/db/src/repositories/audit.ts", + "line": 54, + "note": "审计仓库只有 forEntity/forWorkItem 两个读方法,无工作区级列表" + }, + { + "file": "apps/api/src/routes/audit.ts", + "line": 80, + "note": "审计路由只有 GET /workitems/:id/audit,无工作区汇总端点/页面" + } + ], + "dim": "product-missing", + "merged_from": [ + "product-missing:没有工作区级审计/操作日志界面:管理员「治理审计」无处可查" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "核实属实:audit.ts:54-55 仅有 forEntity/forWorkItem 两读方法,路由仅 GET /workitems/:id/audit(audit.ts:80),全库 grep 确认无 listAuditLogsForWorkspace 及任何工作区级审计端点/页面,而写审计带 workspaceId(数据存在),管理员治理审计确实只能逐工作项翻,非裁除项,中级合理。" + } + }, + { + "title": "工作项没有人对人评论/讨论区(comments 表建了却是死表)", + "category": "missing", + "surface": "cross", + "severity": "medium", + "detail": "schema 里有一张显然是为「工作项讨论」准备的通用 comments 表(work_item_id + author_nickname + body,core.ts:1520),但它【没被任何仓库/路由/UI 使用】:全库搜不到对它的 insert/select(唯一的 comment 相关读写都是 project_drive_comments 与 approval_comments 两张不同的表)。也就是说,人对人的评论只存在于两处:网盘文件评论(且它主要用来触发 AI 起草)与审批评论。一个成员领了任务、想在这条任务下问同事一句、留一条进展备注、或经理想在任务上留审阅意见,除了跟 Cuu 的 AI 会话外没有轻量的任务级讨论串。任务(工作项)作为核心协作单元缺少最基本的评论区。", + "suggestion": "接上 comments 表:补 GET/POST /workitems/:id/comments 与工作项详情里的讨论串,或在契约里明确废弃该表并说明讨论只在会话进行。", + "evidence": [ + { + "file": "packages/db/src/schema/core.ts", + "line": 1520, + "note": "comments 表按 work_item_id 建好,但全库无任何仓库/路由/查询引用,是死 schema" + }, + { + "file": "apps/api/src/routes/approvals.ts", + "line": 200, + "note": "人对人评论只覆盖审批(approval_comments)与网盘文件,工作项本身没有讨论入口" + } + ], + "dim": "product-missing", + "merged_from": [ + "product-missing:工作项没有人对人的评论/讨论区:comments 表建了却是死表" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "通用 comments 表实存(真实位置 core.ts:937,非原文 1520,列名与描述一致)且全库无任何 insert/select/import 引用为死 schema,工作项确无 /workitems/:id/comments 端点或讨论 UI,人对人评论仅覆盖 drive 与 approval(approvals.ts:200 已核实),DM/协同均非任务级线程,构成真实 medium 缺口;仅证据行号有误不影响实质。" + } + }, + { + "title": "全产品无数据导出(成本/用量/审计/工作项均无 CSV/报表)", + "category": "missing", + "surface": "cross", + "severity": "low", + "detail": "工作区管理员做成本/用量核算是核心诉求,但成本路由只有 GET /cost、GET /usage、GET /policies、PUT /policies/:scope/:id(cost.ts),没有任何导出端点;设置页把预算数字也只是只读展示(settings.ts:62-70)。全库搜不到 CSV 导出/报表下载(text/csv 仅出现在 proposals.ts:280 的预览 mime 映射,不是导出)。管理员想把按人/按项目的花费拉成表交给财务、或导出一段时间的用量趋势,产品内无路可走,只能靠人工截屏抄数。工作项列表、审计记录同理无导出。", + "suggestion": "为成本仪表盘的 by_user/by_team/by_workitem 明细、用量、审计各加一个 CSV 导出(Content-Disposition),前端加「导出」按钮。", + "evidence": [ + { + "file": "apps/api/src/routes/cost.ts", + "line": 139, + "note": "成本路由只有 policies/usage/cost 读端点,无导出;全库无 text/csv 导出实现" + }, + { + "file": "apps/api/src/pages/settings.ts", + "line": 62, + "note": "预算在设置页只读展示,无导出/无编辑入口" + } + ], + "dim": "product-missing", + "merged_from": [ + "product-missing:全产品没有数据导出:成本/用量、审计、工作项都无法导出 CSV/报表" + ], + "verdict": { + "real": true, + "reason": "核实无误:cost.ts(199行)仅有 policies/usage/cost 读端点无导出,全库 Content-Disposition 只服务于 drive/workitem 上传文件下载而非成本/用量/审计报表,settings.ts:62-70 预算确为只读 VM,管理员无内置 CSV/报表导出路径,属真实但打磨级缺口。", + "adjusted_severity": "low" + } + }, + { + "title": "通知偏好只有『按类型静音』,无渠道/免打扰时段/按项目粒度", + "category": "missing", + "surface": "cross", + "severity": "low", + "detail": "通知偏好的整个可配置面只有 muted_notification_types 一个数组(notifications.ts:26,GET/PUT /preferences 只读写这一项)。没有按渠道的偏好(站内 vs Web Push vs 邮件——实际上系统压根没有邮件通知渠道)、没有免打扰/静音时段(DND/quiet hours)、没有按项目或按重要度的细分静音。R15 上线了主动性关怀、审批 SLA 提醒、DDL 追踪等大量主动推送,用户想「非工作时段别推」「只关键审批推 push、其余只留站内」都做不到,只能整类静音——要么全收要么全静,颗粒度过粗容易导致要么被打扰要么漏关键通知。", + "suggestion": "扩展偏好模型:加免打扰时段、按渠道开关、按项目/按类型的细分静音;主动性推送尊重这些偏好。", + "evidence": [ + { + "file": "apps/api/src/routes/notifications.ts", + "line": 26, + "note": "偏好 schema 仅 muted_notification_types,GET/PUT /preferences(61/67)只读写这一项" + } + ], + "dim": "product-missing", + "merged_from": [ + "product-missing:通知偏好只有「按类型静音」一个旋钮:没有渠道、免打扰时段、按项目粒度" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "证据属实:notifications.ts:26 偏好 schema 仅 muted_notification_types,GET/PUT /preferences 只读写这一项,全库无 quiet-hours/按渠道/按项目静音(Cuu 的 do_not_disturb 是桌宠打扰模式而非通知投递偏好,无时段/渠道粒度),确为低级别打磨缺口。" + } + }, + { + "title": "用户只能停用不能恢复,停用是不可逆终态", + "category": "missing", + "surface": "api", + "severity": "low", + "detail": "管理员角色治理里,POST /api/auth/users/:id/deactivate 停用成员(软删+审计,auth.ts:796),但【没有任何恢复/重新启用端点】(搜 reactivate/activate/restore 在 auth 路由与 users 仓库均无)。管理员误停用一个人、或某人离职后又回归,没有一键恢复的路径,只能靠重新注册/走数据库。停用是单向终态,治理操作缺少可逆性。", + "suggestion": "补 POST /users/:id/reactivate(清空软删标记)与成员面板的「恢复」动作。", + "evidence": [ + { + "file": "apps/api/src/routes/auth.ts", + "line": 796, + "note": "只有 deactivate 端点,全文件无 reactivate/restore 对应项" + } + ], + "dim": "product-missing", + "merged_from": [ + "product-missing:用户只能停用、不能恢复:停用是终态,误操作/回归无法撤销" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "核对属实:auth 路由仅 /users/:id/deactivate 走 softDelete(deletedAt 墓碑),users 仓库无 undelete/reactivate,全库 restore 端点只针对 drive 文件与 workitem 交付物,用户账号停用确为不可逆终态,属真实但低优的治理可逆性缺口。" + } + }, + { + "title": "项目没有成员/访问控制,工作区所有人都能看所有项目", + "category": "missing", + "surface": "cross", + "severity": "low", + "detail": "经理旅程「拉人(进项目)」实际无对应能力。没有 project_members 之类的项目级成员表(全库搜不到),访问控制只到工作区级 + 协同子会话参与者级。桌面项目设置直接写明「工作区全部 N 名成员都能进主区群聊,无需单独拉人」(settings/render.ts:319),「拉人」只发生在新建协同子会话选人时(rail.ts:759)。这是有意的扁平设计,但代价是:无法建一个只有部分人可见的保密/受限项目,也无法把某人从某个项目移除(只能从工作区整体移除)。对 PM 工具这是真实的能力缺口,虽属设计取舍。", + "suggestion": "若要支持保密项目,引入项目级成员/可见性;否则至少在 UI 明确「项目对全工作区可见」的心智,避免经理误以为能限制项目可见范围。", + "evidence": [ + { + "file": "apps/desktop-webview/src/workbench/settings/render.ts", + "line": 319, + "note": "明说全工作区成员都能进主区,项目成员分区是「概览+跳转」而非真实成员数据层" + }, + { + "file": "packages/db/src/schema/core.ts", + "line": 326, + "note": "projects 无成员关联表,访问控制只到 workspace 与协同会话参与者两级" + } + ], + "dim": "product-missing", + "merged_from": [ + "product-missing:项目没有成员/访问控制:工作区所有人都能看所有项目,做不出保密项目" + ], + "verdict": { + "real": true, + "adjusted_severity": "low", + "reason": "在真实审查树核对无误:projects 表(core.ts:326)与全库均无项目级成员表,readActiveProjectMembership 实为工作区成员+项目 join,访问控制确只到工作区级与协同参与者级;settings/render.ts:319 与 rail.ts:759 文案证据属实;不属裁除项,是真实的扁平设计能力缺口,low 级诚实。" + } + }, + { + "title": "web 端移出成员/退群及工作区成员移出均无二次确认(桌面同操作有两段式确认)", + "category": "ux", + "surface": "web", + "severity": "medium", + "detail": "web 会话镜像的成员条里,点『退出』直接 doRemove(selfUserId,true)、点『移出』直接 doRemove(targetId,false)(browser.ts:2765/2768),doRemove 二话不说就发 DELETE(browser.ts:2739-2743),没有任何确认。自己退群是对用户不可逆的——doRemove 里自退后还提示『刷新页面后你将无法再看到它』,用户误点一下就丢了整个会话的访问权、还无法自行加回。/settings 管理员控制台的『移出』工作区成员同理:点击即发 DELETE /api/workspace/members/:id(browser.ts:2963-2971),把人从整个工作区踢掉,零确认;旁边的角色下拉改 admin↔member 也是 change 事件即提交(虽失败会回滚下拉)。而桌面工作台对同一类操作是有护栏的:聊天成员条移出/退群走两段式确认,先弹『退出这个群?』/『把 TA 移出?』再点一次才执行(chat/render.ts:241)。web 端把这道护栏整体丢了,破坏性操作与桌面端不一致。", + "evidence": [ + { + "file": "apps/web/src/browser.ts", + "line": 2765, + "note": "『退出』按钮 click 直接 doRemove(selfUserId,true),无确认" + }, + { + "file": "apps/web/src/browser.ts", + "line": 2768, + "note": "『移出』按钮 click 直接 doRemove(targetId,false),无确认" + }, + { + "file": "apps/web/src/browser.ts", + "line": 2739, + "note": "doRemove 立即发 DELETE /participants/:id(2742-2743),自退后『刷新后你将无法再看到它』=对用户不可逆" + }, + { + "file": "apps/web/src/browser.ts", + "line": 2963, + "note": "/settings 工作区成员『移出』:click 即发 DELETE /api/workspace/members/:id(2971),无确认,踢出整个工作区" + }, + { + "file": "apps/desktop-webview/src/workbench/chat/render.ts", + "line": 241, + "note": "对照:桌面端同操作有两段式确认『退出这个群?』/『把 TA 移出?』" + } + ], + "dim": "ux-polish", + "merged_from": [ + "ux-polish:web 端『移出成员/退群』和工作区成员移出均无二次确认,直接发 DELETE(桌面端同样操作有两段式确认)" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "main 上逐行核实无误:web 端 doRemove(browser.ts:2739-2770)与工作区成员移出(2963-2971)点击即发 DELETE 零确认、自退不可逆,而桌面 chat/render.ts:241 确有两段式内联确认(退出这个群?/把 TA 移出?),护栏确实缺失且双端不一致,非裁除项。" + } + }, + { + "title": "工作台网盘行是不可聚焦的 ,键盘用户无法打开文件/进子文件夹", + "category": "ux", + "surface": "desktop", + "severity": "medium", + "detail": "网盘标签页每一行渲成 ``(drive/render.ts:96),openAttr 只是 data-wb-drive-open-item / data-wb-drive-open-folder,行本身没有 role=button、没有 tabindex、view.ts 里也没有针对行的 keydown 处理(只有容器级 click 委托)。行内的『版本/下载/删除』是真 button/a 可聚焦,但『打开预览』和『进入文件夹』这两个主交互只挂在不可聚焦的 div 上——尤其文件夹行通常没有任何 action 按钮,键盘用户完全没有可达控件去进入子目录,等于被挡在浏览流程外。这不是平台限制:同样是网盘项,右栏『文件』面板把每一项渲成了真正的 ``(files/render.ts:71、102),键盘可达。说明代码库知道该用 button,只是网盘标签页这一处漏了,形成端内不一致。看板卡片也明确做了 role=button tabindex=0 + 回车/空格(kanban/view.ts 注释),进一步反衬网盘行的键盘缺口。", + "evidence": [ + { + "file": "apps/desktop-webview/src/workbench/drive/render.ts", + "line": 96, + "note": "行 = ``,openAttr 为 data-wb-drive-open-item/-folder,无 role/tabindex" + }, + { + "file": "apps/desktop-webview/src/workbench/drive/view.ts", + "line": 154, + "note": "仅容器级 click 委托处理 open-item/open-folder,无任何 keydown → 键盘不可触发打开/进入文件夹" + }, + { + "file": "apps/desktop-webview/src/workbench/files/render.ts", + "line": 102, + "note": "对照:同为网盘项,Files 面板用真 ``,键盘可达" + } + ], + "dim": "ux-polish", + "merged_from": [ + "ux-polish:工作台网盘标签页文件/文件夹行是不可聚焦的 (无 role/tabindex/键盘处理),键盘用户无法打开文件或进入子文件夹" + ], + "verdict": { + "real": true, + "adjusted_severity": "medium", + "reason": "已核实:drive/render.ts 的行确为无 role/tabindex 的 div、openAttr 仅挂 data-wb-drive-open-folder/-item,view.ts 只有容器级 click 无任何 keydown,且文件夹行 actions 数组只对 file 填充导致文件夹行零可聚焦控件,键盘用户确实无法进入子目录,是真实的 a11y 残缺(非裁除项)。" + } + } + ], + "rejected": [ + { + "title": "审批与升级的『委派他人』后端就绪却缺选人 UI,动作在两端被剥离/从不调用", + "reason": "发现核心论断\"全产品无任何转交入口\"站不住:route-components.ts:1920-1925 在 web 审批页渲染了专门的转交选人面板(details+成员select+确认转交钮),browser.ts:802-856 完整接线调用 client.delegateApproval 打 POST /api/approvals/:id/delegate;被两端剥掉的只是审批卡上无上下文的裸 delegate 动作按钮,真正能力有独立 UI 且端到端接通。" + }, + { + "title": "工作台决策收件箱把导航类决策打成『去主窗口看』的 toast 死胡同", + "reason": "代码事实属实,但影响被夸大:各类决策的拍板主动作(approval/budget resolveBudgetDecision/escalation resolve/sync_conflict 可编辑合并/proposal)均就地 submit 可用,仅辅助性 GET 导航按钮 bounce 到 toast,且该 toast 是 view.ts 明文\"铁律3:不摆假按钮\"的有意诚实降级(主窗口已接全,UX-M7),非\"决策不可操作的死胡同\"。" + }, + { + "title": "军团总览对无源会话的 run(看板/时间线派发)钻取静默失灵,卡片却像可打开", + "reason": "证据虚构:军团总览 render.ts 对每张 run 卡传 interactive:false,渲染为静态 div 而非可点按钮(:131-133,472-473),根本不可钻取;发现所称的 data-wb-army-ov-drilldown / data-wb-army-conversation-id / pendingArmyRunDetailId / onOpenRun 分支在全仓(含所有 worktree)均不存在,且所引 render.ts:194 实为无关的变更类型标签常量。" + }, + { + "title": "时间线空态把已上线的 E3『Cuu 起草整份计划』误标为即将上线", + "reason": "证据链在本 main 检出中完全不存在:apps/desktop-webview/src 下没有 workbench/timeline 或 schedule 目录,grep 全库也搜不到「起草整份计划」「coming soon (E3)」「createPlanDraft」「plan-drafts」等任一字符串,所引 file:line 均属虚构。" + }, + { + "title": "主动 Cuu 消息与普通回复无差别渲染,缺归因/深链/就地静音(proactive_intent_id 未消费)", + "reason": "发现前提虚构:全仓库(含所有 worktree)grep `proactive_intent_id` 零命中,被引 conversation.ts 第365行实为 `clarify_placeholder`,conversationTextContentSchema 里根本没有 proactive_intent_id 字段,\"契约带此标记待前端消费\"不成立,无从谈\"未被消费\"。" + }, + { + "title": "三处『需要处理』角标口径不一,且工作台窗从不推 Dock", + "reason": "证据伪造:全仓库无 set_shell_badge、browser.ts 仅 1371 行(引用的 1382 行不存在),桌面根本没有含未读通知的 OS Dock/托盘角标,唯一角标是聚焦盒内 setBadges({approvals}) 且只吃 attention.queue.length,所谓\"Dock 口径不一\"的对比对象不存在。" + }, + { + "title": "后台但已打开的会话被当作『正在看』,新消息在所有端被抑制(无 Dock/OS 通知)", + "reason": "三条证据文件在本检出全部不存在(conversation-message-notify.ts 及 workbench/chat/view.ts 均无,isViewingConversation 全库零命中),file:line 无从核对,发现前提不成立。" + }, + { + "title": "通知/气泡/digest 的会话深链不带消息 seq,落到会话却落不到那条未读", + "reason": "承重证据无法证实:全仓库 grep focusSeq 零命中、conversation.message 通知类型不存在、pending-deep-link 实际版本无 seq 字段、cited shell.ts:748 行超出实际文件长度——该发现引用的是本处未检出的更新分支代码,file:line 与「shell 已具备 focusSeq 定位」的前提均站不住。" + }, + { + "title": "R18 web 成员/邀请面板状态与报错写进非 aria-live 的本地 ,读屏用户无播报", + "reason": "证据全对不上:browser.ts 仅 2417 行,所引 2725/2951/3074 行越界,且 data-r18-* 属性与\"已复制令牌/最后一名管理员/私聊不能移出\"等所有状态串在全仓 grep 皆无——该 R18 web 成员/邀请面板在本检出根本不存在,发现基于不存在的代码。" + }, + { + "title": "web 审批列表选中行后详情面板去 hidden 但焦点不跟随", + "reason": "证据行号错位——browser.ts:793-798 实为会议转写导入错误处理,审批面板去hidden/更新textContent实际在741-755;且行732已对选中行设置aria-current提供读屏语境,焦点强移属可辩护形态。" + } + ], + "lead_review": { + "date": "2026-07-17", + "withdrawn": [ + "R19-14 托盘角标", + "R19-17 全局热键", + "R19-38 死枚举" + ], + "corrected": [ + "R19-4 邀请范围", + "R19-37 死表范围" + ], + "spot_checked_true": [ + "R19-12", + "R19-13", + "R19-16", + "R19-41", + "R19-42", + "R19-19", + "R19-22", + "R19-37(三张维持)" + ], + "external_crosscheck": "codex 2026-07-17 反馈已并入" + } +} \ No newline at end of file
,读屏用户无播报", + "reason": "证据全对不上:browser.ts 仅 2417 行,所引 2725/2951/3074 行越界,且 data-r18-* 属性与\"已复制令牌/最后一名管理员/私聊不能移出\"等所有状态串在全仓 grep 皆无——该 R18 web 成员/邀请面板在本检出根本不存在,发现基于不存在的代码。" + }, + { + "title": "web 审批列表选中行后详情面板去 hidden 但焦点不跟随", + "reason": "证据行号错位——browser.ts:793-798 实为会议转写导入错误处理,审批面板去hidden/更新textContent实际在741-755;且行732已对选中行设置aria-current提供读屏语境,焦点强移属可辩护形态。" + } + ], + "lead_review": { + "date": "2026-07-17", + "withdrawn": [ + "R19-14 托盘角标", + "R19-17 全局热键", + "R19-38 死枚举" + ], + "corrected": [ + "R19-4 邀请范围", + "R19-37 死表范围" + ], + "spot_checked_true": [ + "R19-12", + "R19-13", + "R19-16", + "R19-41", + "R19-42", + "R19-19", + "R19-22", + "R19-37(三张维持)" + ], + "external_crosscheck": "codex 2026-07-17 反馈已并入" + } +} \ No newline at end of file