Skip to content
dsh.fish
Bundle

dsh-blast-radius

DeepSeek Harness plugin: semantic blast radius for agent code edits — who calls what the agent just changed, and which of those callers have no test covering them

Source
haoku123
stars
1 stars
License
MIT
Updated
Updated yesterday

Readme

# dsh-blast-radius

DeepSeek Harness 插件:**agent 改了代码,这次改动会波及什么?**

> diff 告诉你改了 5 行。它不会告诉你这 5 行在一个被另外 3 个文件调用的函数里,
> 而那些调用点**一个测试都没有**。前者是文本视角,后者是风险视角。

用真实的 TypeScript 语义分析(`tsserver`)回答后者:改动落在哪个符号上、谁依赖这个
符号、那些依赖里有多少受测试保护。

```
[高] applyDiscount                                   src/money.ts
     3 处生产调用点,且该符号没有任何测试引用
     影响 2 个其他文件:src/cart.ts、src/pricing.ts
     ↳ src/cart.ts:6      const total = applyDiscount(subtotal, discountPct)
     ↳ src/pricing.ts:10  return applyDiscount(cents, 10)
     ↳ src/pricing.ts:19  return applyDiscount(this.cents, pct)
```

## 两条不会妥协的原则

这两条决定了插件的所有行为,也决定了默认配置:

**1. 分析失败永不阻断 agent。** TypeScript 没装、tsserver 崩了、查询超时 —— 全部放行。
即使 `enforce: true` 也一样。一个会因为子进程死掉就卡住 agent 的质量工具,比没有这个
工具更糟。

**2. 「不知道」永不伪装成「安全」。** 分析不出结论时明确说出来,而不是渲染成一条干净
的结果。这也是 `enforce` 默认关闭的原因:一个第一天就开始拦你写入的插件,会在赢得
信任之前先被卸载。

## 功能

- **语义影响面**:`tsserver` 的 `references` + `navtree`,按符号(不是行号)归因
- **测试覆盖缺口**:区分生产调用点与测试引用 —— 这是最值钱的信号
- **风险分级**:`低 / 中 / 高`,规则确定可复现(不用模型判断)
- **写入前拦截**:`tools/pre-execute` waterfall,超阈值走 `ask`
- **仪表盘**:`shell.overlay` 角落面板,4s 轮询
- **零额外安装**:`typescript` 包自带 `tsserver.js`,优先用被分析项目自己的版本

## 安装

```bash
dsh plugin --profile web add dsh-blast-radius
```

默认 **report-only**:只记录、只显示,不拒绝任何写入。

## 配置

见 [`cordis.patch.yml`](./cordis.patch.yml)。关键项:

| 键 | 默认 | 含义 |
|---|---:|---|
| `enforce` | `false` | 超阈值时是否要求确认。默认只报告 |
| `enforceAtRisk` | `high` | 触发确认的风险等级:`medium` \| `high` |
| `timeoutMs` | `3000` | 单次语义查询预算,超时降级为「未分析」并放行 |
| `highProductionRefs` | `12` | 调用点超过此数即为高风险(即使有测试) |
| `testPatterns` | `[]` | 追加的测试路径正则(内置已覆盖 `*.test.*` / `test/` / `__tests__/`) |
| `maxProjectFiles` | `2000` | 无 tsconfig 时扫描上限,触顶会明确报告 |
| `verbose` | `false` | 每次分析打一行日志,调阈值时有用 |

## 风险分级规则

| 条件 | 等级 |
|---|---|
| 无人引用 / 仅被测试引用 / 仅被导入未调用 | 低 |
| 有生产调用点,无测试引用,且**跨文件** | 高 |
| 有生产调用点,无测试引用,但都在本文件内 | 中 |
| 有生产调用点且有测试引用,调用面 ≥ `highProductionRefs` | 高 |
| 有生产调用点且有测试引用 | 中 |

排序刻意如此:**「跨文件且完全没有测试」排在「调用点很多」之前**,因为前者是改错了会
静默上线的情形,后者至少有测试兜着。

## 设计取舍

这些取舍都是在真实仓库上跑出来的,不是设计阶段想出来的。

### 只分析符号,不分析行

变更行 → `navtree` 定位所属符号 → 查该符号引用。变更区间用前后缀剪裁计算,**分散编辑
会连带包含中间范围**。刻意选这个方向:多分析几个符号只是多花几毫秒,漏掉符号就是漏掉
了插件存在的理由。

### 局部变量不参与分析

第一次在真实文件上跑,30 条结果里大部分是噪音 —— `splitSentences.start` 报告"2 处生产
调用点",那是个循环变量。

按 kind 过滤不够:模块级 `const` 和函数内 `let` 在 tsserver 里 kind 相同。真正的判据是
**是否在可调用体内** —— 模块常量能被任何地方读,局部变量跑不出它所在的函数体。修完
30 条降到 6 条真实符号。

### import 不算调用点

`import { applyDiscount } from './money.ts'` 引用了符号,但不依赖它的行为。算进去会让
影响面虚高一倍(fixture 里 3 处真实调用被报成 5 处)。

import **仍计入「波及文件」** —— 依赖关系是真的,只是不是"使用"。

### 测试文件不算「波及范围」

测试引用一个符号是**保护**,不是风险传播。算进去会让"测试写得好的符号"看起来比裸符号
更危险,正好反了。

### 补丁式编辑明确拒绝分析

`str_replace_editor` 表达的是"把 A 换成 B",不是"文件最终长这样"。要还原结果就得重新
实现一遍该工具的语义,而还原错了的话,下游每条结论都在描述一个**不存在的文件**。

所以报告为「未分析」、放行、**且不记入已审查记录** —— 免得让人以为这次改动被看过了。

### 无 tsconfig 的项目:合成项目范围,但不落盘

没有 `tsconfig.json` 时,tsserver 退化成 inferred project,只知道显式打开的那个文件。
实测后果(在一个真实仓库上):

```
inferred project:  1 refs -> src/segmenter.ts        ← 假的「没有引用」
declared project:  4 refs -> src/segmenter.ts, src/index.ts ✓
```

用 `openExternalProject` 在内存里声明项目范围,**刻意不写 `tsconfig.json` 到用户仓库**
—— 那会出现在 `git status` 里、可能被它正在监视的 agent 提交进去、还可能改变项目真实
构建行为。有测试断言分析前后 `readdir` 结果一致。

代价是范围靠扫描推断,可能漏文件。所以结果带 `approximate` 标记,且 **`enforce` 时
推断范围的结论不参与拦截** —— 低估风险在"报告"里可以接受,作为"打断别人工作的理由"
则不行。

## 架构

```
tools/pre-execute waterfall
  └── extract.ts   工具调用 → 待写入内容(认不出的明确报 opaque)
        └── radius.ts   变更行 → 符号 → 引用 → 分桶 → 分级
              └── lsp.ts   tsserver 长驻会话(超时/崩溃/未就绪全部降级)
                    └── discover.ts   无 tsconfig 时的有界文件扫描

GET /dsh-blast-radius-api/status → { records, enforce, enforceAtRisk, projectRoot }
```

| 文件 | 职责 |
|---|---|
| `src/lsp.ts` | tsserver 会话:帧解析、超时降级、崩溃重启、pending 内容同步 |
| `src/radius.ts` | 纯逻辑:符号定位、引用分桶、风险分级(不碰子进程) |
| `src/discover.ts` | 无 tsconfig 时的项目文件发现(有界) |
| `src/extract.ts` | 从工具调用还原待写入内容 |
| `src/index.ts` | 插件入口:配置、闸门、status 路由 |
| `src/client.tsx` | 仪表盘面板 |

## 性能

在一个真实仓库上实测:

| 指标 | 数值 |
|---|---|
| tsserver 冷启动 + 项目加载 | ~450ms(每会话一次) |
| 单符号引用查询 | 1–20ms |
| 一个文件(含多符号) | 40–120ms |

会话长驻、跨编辑复用。冷启动只付一次,所以不会把 0.5s 摊到每次写入上。

## 测试

```bash
npm test        # 166 用例
npm run typecheck
```

| 套件 | 用例 | 说明 |
|---|---:|---|
| `src/radius.test.ts` | 65 | 纯逻辑,不碰子进程 |
| `src/extract.test.ts` | 28 | 工具调用识别与 opaque 判定 |
| `src/client.test.tsx` | 18 | 面板(jsdom) |
| `src/discover.test.ts` | 16 | 文件发现的边界与截断 |
| `tests/lsp.spec.ts` | 19 | **真实 tsserver**,含无 tsconfig 场景 |
| `tests/plugin.spec.ts` | 20 | **真实 cordis**,闸门与 status 路由 |

LSP 层刻意不 mock:它是唯一有真实失败模式的组件(子进程、帧协议、项目加载),
mock 掉就等于没测到唯一的难点。

## 原型工具

仓库里带一个 probe 脚本,可以在任意真实仓库上直接跑分析、打印结果 —— 用来判断信号
质量、调阈值,不需要先装成插件。**只在源码仓库里可用**(未随 npm 包发布):

```bash
git clone https://github.com/haoku123/dsh-blast-radius
cd dsh-blast-radius && npm install --legacy-peer-deps
node scripts/probe.ts ~/your-project src/some-file.ts [symbolName]
```

需要 Node ≥ 22.6(原生运行 TypeScript)。输出示例见本文开头。

## 限制

- **只支持 TypeScript / JavaScript**。Python 需要另装 pyright,选型与降级策略完全不同,
  架构上留了 provider 接口但 v1 不做。
- **`references` 拿不到动态调用**:反射、字符串索引、`eval`、跨语言边界。
- **补丁式编辑工具无法分析**(见上)。
- **无 tsconfig 时项目范围是推断的**,可能漏掉调用点。
- **不做「AI 判断风险」**。全部基于确定性事实 —— 一个会给出不可复现结论的质量门是
  负资产。

## License

MIT

Install

dsh plugin --profile web add github:haoku123/dsh-blast-radius

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source