- 项目:
dsh/dsh-todo-sidebar(DSH 原生右侧栏「实时状态」面板插件)
- 分支:
v0.2.2
- 基准(base):
138f0cdeb0e816e442391019bfbd7ae390ae79fe(= v0.2.1)
- 目标(head):
4691d3add577b3ff0478ad57366bcc86460c089a(= v0.2.2)
- 扫描口径:
codescan_git_diff + codescan_issues_diff(真实数据)+ 变更区域源码审读
1. 结论速览(TL;DR)
| 维度 | 结论 |
|---|
| 变更性质 | 纯客户端适配层改造:把插件从「只吃 0.1.5-rc.x 的会话列表快照」扩展为「0.1.5 快照 / 0.1.7 独立服务」双数据源自动选路。无破坏性接口变更(老行为保持不变)。 |
| 质检原始数据 | issues_diff:修复 31 条 / 新增 34 条(全部为 Low/Medium 的命名、复杂度、嵌套、重复代码类规则) |
| 是否引入功能性缺陷 | 未发现会导致崩溃或数据错乱的确定性缺陷。新增问题里没有 Critical/High。 |
| 原始新增/修复数据可信度 | ⚠️ 被行号漂移显著放大:本版在 lib/client.js 顶部插入约 80+ 行新代码,导致大量同一条既有问题在 base 报旧行号(算"修复")、在 head 报新行号(算"新增")。真正的新增技术债集中在新增的兼容层函数上。 |
| 真实可跟进的点 | 2 条 Medium(watchRows 缺异常保护、live 流水线双份重复)+ 若干 Low(诊断字段误报、子树口径不一致、可选服务分支无测试覆盖)。 |
| 误报 | 10 条「命名不规范」实为 React 组件 PascalCase 约定;2 条来自 jsx/jsxs 与 TODO_FIXTURE 的字面匹配。 |
| 回归重点 | 真实 DSH 0.1.7 上:命令区是否出现、__dshTodoSidebarDebug.jobsSource === "service"、watchedSessions 是否含当前会话、子代理详情是否经 uiWorkspace.openSession 打开。 |
2. 变更范围(base → head)
变更文件(4 个)
| 文件 | 变更 |
|---|
lib/client.js | 主体改动(+~240 净行):新增两代兼容层与 0.1.7 服务接入 |
tests/harness.cjs | 新增「0.1.7 数据源 / 导航」两段用例(+~110 行)与 jobs/uiSession 替身 |
package.json | version 0.2.1 → 0.2.2;description 更新 |
README.md | 文档同步(数据契约、版本差异表、排查指南) |
lib/client.js 的新增/改造单元
| 新增/改造 | 作用 |
|---|
normalizeById(byId)(新) | 0.1.7 的 id/parentId 归一回 sessionId/parentSessionId |
isRunningAgent(id, byId, statuses)(新) | 运行态以 uiSession.sessionStatus 推送优先,回落 byId.running |
buildCatalogs(list, byId, statuses)(新) | 目录两代兼容:subagentsByParent ↔ projectionsBySession[].values.subagentCatalog,并回填 kind/activity |
probeService(ctx, name)(新) | 可选服务运行时可探测(吞掉 cordis 未 inject 的抛错),命中后缓存 |
createSessionOpener(ctx, sessions)(新,取代 openSessionById) | 跳转双代:sessions.openSubagent/open ↔ uiWorkspace.openSession |
useServiceSnapshot / (新) |
3. 修复的问题(31 条,来自 codescan_issues_diff)
规则分布:命名 8、圈复杂度/嵌套/函数过长 16、重复代码块 3、行过长 1、参数过多 1、待办标记 1、未使用参数 1。
| # | 严重度/类型 | 位置(base) | 建议 |
|---|
| 1 | Low/Naming | lib/client.js:39 | use lowercase first letter for private functions |
| 2 | Low/Maintainability 重复代码块 | lib/client.js:56 | extract the repeated block into a shared function |
| 3 | Low/Naming | lib/client.js:61 | use lowercase first letter for private functions |
| 4 | Low/Naming | lib/client.js:76 | use lowercase first letter for private functions |
| 5 | Low/Maintainability 行过长 | lib/client.js:126 | break long lines for readability |
| 6 | Medium/Complexity 圈复杂度 | lib/client.js:141 | consider splitting into smaller functions |
| 7 | Medium/Complexity 嵌套深度 | lib/client.js:167 | extract nested logic into separate functions |
解读:其中 #1、#3、#4(base 39/61/76)与新增清单里的 120/142/157 恰好相差 +81 行,是同一批组件(TodoIcon/Dot/Chevron)被顶部插入代码顶下去的产物;#6/#7、#19–#21、#23–#28 同理对应新增清单的 222/258、637、1032/1159 等。这 31 条"修复"中绝大多数并非真正被修掉,而是行号漂移。
4. 新增问题(34 条,来自 codescan_issues_diff)
| # | 严重度/类型 | 位置(head) | 详情 | 判定 |
|---|
| 1 | Medium/Complexity 圈复杂度 | lib/client.js:73 | count=14 | 新增代码(buildCatalogs 附近),可接受 |
| 2 | Low/Naming | lib/client.js:120 | private function has exported name style | 误报(组件 PascalCase) |
| 3 | Low/Maintainability 重复代码块 | lib/client.js:137 | duplicated block of 4 lines (8 occurrences) | 参考 |
| 4 | Low/Naming | lib/client.js:142 | 同上 | 误报(组件) |
| 5 | Low/Naming | lib/client.js:157 | 同上 | 误报(组件) |
| 6 |
注:工具输出为 34 条,上表按类别一一对应并补充了交叉判定。
新增问题的真实构成:
- 约 8 成为行号漂移(既有代码下移,被计为"新增")——与 §3 的"修复"成对出现,属净噪声。
- 真正由本版引入的:仅 3 类——
- 新增兼容层函数的圈复杂度(
buildCatalogs @73)与重复小片段(@137/@421);
summarizeLive 耦合度越线(@561,#14);
createTodoPane/createTodoHeaderButton 因接入 jobs/status/watch 而进一步变长、复杂。
- 剩余为 React 组件命名误报与测试文件字面量误报。
5. 代码审读:需人工确认的真实风险(非工具规则)
以下为通读 lib/client.js 变更区域后确认的问题,按优先级排列。
M1(Medium,健壮性)watchRows 调用未做异常保护,与 refreshProjections 处理不一致
createTodoPane 的目录保活 effect 里对 sessions.refreshProjections(id) 做了 try/catch + Promise .catch() 兜底;但新增的 useJobsWatch 里:
const releases = subtreeKey.split("\u0000").map((id) => jobs.watchRows(id));
没有任何 try/catch。若 watchRows 在服务尚未就绪 / 传入未知会话 id 时抛错:
- 在 effect 阶段抛错(虽会被
PaneBoundary 兜住并显示错误面板,但用户看到的是"面板崩了"而非"降级为无命令");
- 若在
map 中途抛错,前面已建立的 release 函数全部丢失,造成 roster 流泄漏(该会话被永久 watch)。
建议:逐会话 try/catch,并把已拿到的 release 存入数组后统一清理,与 refreshProjections 的容错口径保持一致。
createTodoPane 与 createTodoHeaderButton 各自复制了完整的一段:
const jobs = probeJobs();
const jobsSnapshot = useServiceSnapshot(probeJobs, selectJobsState);
const statuses = useServiceSnapshot(probeUiSession, selectSessionStatus);
const live = useMemo(() => summarizeLive(list, sessionId, statuses, jobsSnapshot?.rows), [...]);
const watchKey = useMemo(() => live.watchIds.join("\u0000"), [live]);
useJobsWatch(jobs, watchKey);
本版为支持 0.1.7 把 jobs/uiSession/watch 逻辑各写了一遍,两处必须同步演进,极易漂移(工具已在 summarizeLive @561 报出耦合越线,createTodoHeaderButton @1307 报出 18 依赖)。建议抽成 useLive(ctx, sessionId) 单一 hook。
L1(Low,诊断准确性)jobsSource 首帧会误报 "snapshot"
jobsSource = jobsRows !== undefined ? "service" : "snapshot"。而 0.1.7 上 jobs 服务存在但 store 快照尚未产出时 jobsSnapshot 为 undefined → jobsRows 为 undefined → 诊断字段报 "snapshot"。README 把该字段作为排障首要线索("先看 jobsSource 是不是 service"),误报会误导排障。建议改为依据 jobs 服务是否探测到来判断。
L2(Low,口径一致性)collectSubtree 新增目录 BFS 后,子树里混入"非运行的历史目录项"
// 沿 catalogs BFS,无条件把每个 entry.id 加入 ids(不判 activity/running)
subtree 随后用于 collectLiveJobs。在 0.1.5 路径(jobsBySession 按 owner 分桶,可能仍保留已结束会话的桶)下,这会聚合出"已结束子代理遗留的后台命令";而 README 对 0.1.7 明确写了"不 watch 非在跑的后代,所以'子代理已结束、命令还在跑'这种情况不会显示"——两代口径不一致,且与该文档描述相矛盾。若产品上希望"只显示在跑 agent 的命令",应在 BFS 时按 activity/running 过滤。
L3(Low,边界)activity 回填只在 projections 路径生效
buildCatalogs 在 legacy 非空时直接 return legacy,不补 activity。若某个 harness 同时给出**非空的 subagentsByParent(无 activity)**与 projectionsBySession,则走 legacy 分支,collectCatalogAgents 的 entry.activity === "running" 判据会让"只在目录、不在 byId"的运行中子代理丢失。当前实测两代不共存,属低概率边界,但建议在 legacy 路径也按 isRunningAgent 补一次 activity(成本极低、可消除该分支风险)。
L4(Low,性能/噪声)probeService 负结果不缓存
只有命中(candidate 非空)才 resolved = true。在老 harness 上,ctx.jobs / ctx.uiSession 的读取会抛错并被吞,于是每次渲染(每次进 Pane、每次头部按钮渲染)都要各抛+catch 一次异常 ×2。异常构造/栈展开成本不低。建议对"抛错"也做短期记忆(例如标记 unsupported 并缓存)。
L5(Low,一致性)缩进层级存在两套算法
fromList(byId 命中)用 agentDepth()(沿 parentSessionId 向上数到 scopeId);fromCatalog 用目录 BFS 的层数。两者在常规装配下结果一致(测试已覆盖"子代理B 导轨 = 1"),但混合来源时对同一棵树可能出现层级不一致。建议统一为一个 depth 函数。
L6(Low,测试)0.1.7 降级分支的测试覆盖不真实
- 桩把
ctx.jobs / ctx.uiSession 设为普通属性,读取不会抛;因此 probeService 里 catch { /* 这个 harness 没有该服务 */ } 这条降级/兜底分支在测试中从未被执行——而这恰是本功能的卖点。真实 cordis 对未 inject 的服务属性是否会抛、抛错是否仅发生在"服务不存在"时,决定了 0.1.7 路径是否真的可达,属关键未验证假设(README 称已在 DSH 2.0.15 / harness 0.1.7-rc.2 实测)。
- 未覆盖:
jobs 服务在但 state store 缺失、uiSession 在但 sessionStatus 缺失、watchRows 抛错(M1)。
- 测试用
delete sessions.setSubagentCatalogOpen / delete sessions.openSubagent 并永久污染 ctx.jobs/ctx.uiSession/ctx.uiWorkspace,存在用例顺序依赖,后续重排用例会互相影响。建议每个场景用独立的 ctx/替身工厂。
6. 判定为误报 / 噪声的项
| 项 | 原因 |
|---|
10 条 [Low/Naming] private function has exported name style(lib/client.js 120/142/157/637/716/785/889/924/957/1032) | 命中的是 TodoIcon/Dot/Chevron/StatusHeader/SessionRow/SubagentDetail/CommandSection/TodoSection 等 React 组件,PascalCase 是 JSX 约定的正确写法;该规则不适用于组件,与 base 的 39/61/76/427/457/536/744/852 同源。 |
jsx fan-in 77 / jsxs fan-in 24(quality_map,非增量) | jsx/jsxs 来自 react/jsx-runtime,是宿主/host 函数,天然被全文件调用而自身不出边,属结构性误报。 |
tests/harness.cjs:207 contains TODO marker | 该行是 faceOf: () => ({ … getSnapshot: () => TODO_FIXTURE }),由标识符 TODO_FIXTURE 字面匹配 "TODO" 触发,并非待办注释。(已逐行核对源码确认。) |
tests/harness.cjs:278 parameter 'row' may be unused | 跨行箭头函数 const railCount = (row) => (row && … )(base 时在 223 行), 实际被使用;且 base/head 同时出现,属行号漂移。 |
7. 建议与回归重点
修复优先级
- M1:给
useJobsWatch 加逐会话 try/catch + release 收集,避免 effect 抛错与流泄漏(改动小、收益明确)。
- M2:抽
useLive(ctx, sessionId),消除 Pane/Header 双份流水线。
- L1/L3:修正
jobsSource 判定口径;legacy 路径也回填 activity(两处都是几行改动)。
- L6:补一条"服务读取抛错 → 回落快照"的替身用例(让桩按 cordis 语义抛错),并消除用例顺序依赖。
回归验证清单(真实环境)
数据侧提醒:本版 issues_diff 的"修复 31 / 新增 34"对比受行号漂移严重干扰,不建议作为版本质量趋势指标;建议后续以"文件 + 规则 + 代码片段指纹"归一后再统计。
附:本次结论所依据的真实工具数据
codescan_git_diff(base=138f0cd, head=4691d3a):4 文件变更(lib/client.js、tests/harness.cjs、package.json、README.md)。
codescan_issues_diff(...):修复 31 条 / 新增 34 条(明细见 §3、§4)。
codescan_quality_list(v0.2.2, head=4691d3a):全量 57 条(含非增量项,用于识别误报与既有债)。
- 源码核对:
lib/client.js(probeService/buildCatalogs/collectSubtree/collectRunningAgents/collectLiveJobs/summarizeLive/createTodoPane/createTodoHeaderButton)与 tests/harness.cjs(第 203–210、262–290 行)。