1
0
Files
DiAL/openspec/changes/refactor-checker-cohesion/design.md
lanyuanxiaoyao c396c29402 docs: 添加 checker 内聚化重构方案及归档历史变更
新增 refactor-checker-cohesion 变更提案,包含 proposal、design、
specs 和 tasks,定义 checker 目录内聚结构规范。同时归档已完成的
历史变更记录。
2026-05-13 13:30:05 +08:00

233 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
当前 `src/server/checker/` 的代码组织存在内聚性不足的问题:
- 顶层 `types.ts` 混合了所有 checker 的类型定义(`HttpTargetConfig``ResolvedHttpTarget``CommandExpectConfig` 等),形成硬编码联合类型 `ResolvedTarget = ResolvedHttpTarget | ResolvedCommandTarget`
- `runner/shared/` 中混合了真正跨 checker 共享的断言基础设施和仅单个 checker 使用的模块
- `config-contract/` 命名不直观,内部 `schema.ts` 与目录名语义重叠
- 新增 checker 需要修改 3-4 个文件(顶层 types、联合类型、注册入口、可能还有 shared
项目核心是 checker 系统,需要让每个 checker 的代码尽可能内聚,降低新增和维护成本。
## Goals / Non-Goals
**Goals:**
- 新增 checker 只需:创建一个目录 + 在注册列表加一行 import
- 每个 checker 目录包含完整的类型、schema、校验、执行逻辑
- 共享断言基础设施有明确的物理位置和语义命名
- 依赖方向清晰checker → expect/断言基础设施、schema/fragmentsschema 片段、utils工具函数
**Non-Goals:**
- 不改变任何运行时行为、配置文件格式、API 接口
- 不引入自动目录扫描机制(保持显式注册)
- 不改变测试覆盖范围(只更新 import 路径)
## Decisions
### Decision 1: ResolvedTarget 和 TargetConfig 改为 base interface
**选择**: 删除硬编码联合类型,改为 base interface + 各 checker 内部 narrow
**理由**: engine、store、config-loader 从来不关心具体 checker 类型——它们只用 base 字段type、name、group、intervalMs、timeoutMs+ 通过 registry dispatch。各 checker 的 execute/resolve/serialize 内部第一行就是 `as ResolvedXxxTarget`,联合类型对它们没有实际约束价值。
同理,`DefaultsConfig` 当前也是硬编码联合(`command?: CommandDefaultsConfig; http?: HttpDefaultsConfig`),新增 checker 仍需改这个 interface。改为宽松 base 形式:
```typescript
export interface DefaultsConfig {
interval?: string;
timeout?: string;
[checkerKey: string]: unknown;
}
```
各 checker 的 `validate()` 方法接收 `DefaultsConfig` 后自行 narrow`defaults["http"] as HttpDefaultsConfig`)。`CheckerValidationInput``ResolveContext` 中的 `defaults` 字段类型保持为 `DefaultsConfig`,对外部透明。
**替代方案**: 保留联合但用 barrel 自动聚合——仍需改文件,不够彻底。
**具体设计**:
```typescript
// checker/types.ts — 公共 base
export interface ResolvedTargetBase {
type: string;
name: string;
group: string;
intervalMs: number;
timeoutMs: number;
expect?: unknown;
}
export interface RawTargetConfig {
type: string;
name: string;
group?: string;
interval?: string;
timeout?: string;
expect?: unknown;
[configKey: string]: unknown;
}
// runner/http/types.ts — HTTP 专属
export interface ResolvedHttpTarget extends ResolvedTargetBase {
type: "http";
http: ResolvedHttpConfig;
expect?: HttpExpectConfig;
}
```
### Decision 2: 显式列表注册
**选择**: `runner/index.ts` 维护 import 列表,新增 checker 加一行
**理由**: Bun bundler 和 tree-shaking 依赖静态 import运行时扫描目录引入不确定性临时文件、.bak 等);一行 import 的成本几乎为零。
**具体设计**:
```typescript
// runner/index.ts
import { CheckerRegistry } from "./registry";
import { HttpChecker } from "./http";
import { CommandChecker } from "./command";
const checkers = [
new HttpChecker(),
new CommandChecker(),
];
export function createDefaultCheckerRegistry(): CheckerRegistry {
const registry = new CheckerRegistry();
for (const checker of checkers) {
registry.register(checker);
}
return registry;
}
export const checkerRegistry = createDefaultCheckerRegistry();
```
### Decision 3: 各 checker 的 index.ts 仅做 re-export
**选择**: class 定义在 `execute.ts``index.ts` 只做 `export { HttpChecker } from "./execute"`
**理由**: 保持单一职责——`execute.ts` 专注执行逻辑,`index.ts` 是对外入口。
### Decision 4: runner/shared/ 拆分策略
**选择**: 按实际使用情况拆分为三个去向
| 原文件 | 去向 | 理由 |
|--------|------|------|
| `operator.ts` | `checker/expect/operator.ts` | 所有 checker 的 expect 最终都走这里 |
| `duration.ts` | `checker/expect/duration.ts` | 任何 checker 都可能有 maxDurationMs |
| `failure.ts` | `checker/expect/failure.ts` | 构造 CheckFailure所有 checker 共用 |
| `validate.ts``validateOperatorObject` | `checker/expect/validate-operator.ts` | 通用 operator 校验 |
| `body.ts` | `runner/http/body.ts` | 仅 HTTP 使用 |
| `text.ts` | `runner/command/text.ts` | 仅 Command 使用 |
| `validate.ts``validateBodyRules` 等 | `runner/http/validate.ts` | HTTP 专属校验 |
| `validate.ts``validateTextRules` | `runner/command/validate.ts` | Command 专属校验 |
**理由**: `expect/` 命名比 `shared/` 更能表达语义——这些是断言系统的基础设施。仅单个 checker 使用的模块搬入对应目录实现真正内聚。
### Decision 5: config-contract/ 重命名为 schema/
**选择**: 目录改名 `schema/`,内部 `schema.ts` 改名 `builder.ts`
**理由**: "config-contract" 过于抽象,`schema/` 直接表达"这里是 schema 定义和校验"。内部 `schema.ts` 与目录名冲突,改为 `builder.ts` 表达"从 registry 动态构建整体 schema"的职责。
### Decision 6: 纯工具函数归入 utils.ts
**选择**: `size.ts``parseSize``config-loader.ts` 中的 `parseDuration` 合并到 `checker/utils.ts`
**理由**: 这些是无状态的纯解析函数,不属于任何特定领域。统一放置便于复用。
### Decision 7: 文件重命名
| 原名 | 新名 | 理由 |
|------|------|------|
| 各 checker 的 `runner.ts` | `execute.ts` | 避免和目录名 `runner/` 混淆 |
| 各 checker 的 `contract.ts` | `schema.ts` | 和顶层 `schema/` 目录呼应,统一术语 |
## 最终目录结构
```
src/server/checker/
├── index.ts
├── engine.ts
├── store.ts
├── config-loader.ts
├── types.ts ← ResolvedTargetBase, RawTargetConfig, CheckResult,
│ ExpectOperator, CheckFailure, StoredTarget 等公共类型
├── utils.ts ← parseSize, parseDuration
├── expect/ ← 断言基础设施(所有 checker 共享)
│ ├── types.ts ← ExpectResult 等共享类型
│ ├── operator.ts ← applyOperator, evaluateJsonPath, checkExpectValue
│ ├── duration.ts ← checkDuration
│ ├── failure.ts ← errorFailure, mismatchFailure, truncateActual
│ └── validate-operator.ts ← validateOperatorObject, isJsonValue
├── schema/ ← 配置 schema 体系
│ ├── builder.ts ← createProbeConfigSchema从 registry 动态构建)
│ ├── fragments.ts ← 共享 schema 片段
│ ├── validate.ts ← Ajv 校验入口
│ ├── issues.ts ← issue 类型和工具
│ ├── types.ts
│ └── export.ts
└── runner/
├── index.ts ← 显式注册列表
├── registry.ts ← CheckerRegistry
├── types.ts ← CheckerDefinition 接口(使用 base 类型)
├── http/
│ ├── index.ts ← re-export HttpChecker
│ ├── types.ts ← HttpTargetConfig, ResolvedHttpTarget, HttpExpectConfig 等
│ ├── schema.ts ← TypeBox schemas
│ ├── execute.ts ← class HttpChecker
│ ├── expect.ts ← checkStatus, checkHeaders
│ ├── body.ts ← checkBodyExpect从 shared 搬来)
│ └── validate.ts ← validateHttpConfig + validateBodyRules 等
├── command/
│ ├── index.ts ← re-export CommandChecker
│ ├── types.ts ← CommandTargetConfig, ResolvedCommandTarget 等
│ ├── schema.ts ← TypeBox schemas
│ ├── execute.ts ← class CommandChecker
│ ├── expect.ts ← checkExitCode
│ ├── text.ts ← checkTextRules从 shared 搬来)
│ └── validate.ts ← validateCommandConfig + validateTextRules
└── [future-checker]/ ← 新增 checker 模板
├── index.ts
├── types.ts
├── schema.ts
├── execute.ts
├── expect.ts
└── validate.ts
```
## 依赖方向约束
```
engine.ts / store.ts / config-loader.ts
▼ 依赖
runner/registry.ts + runner/types.ts + types.ts (base)
▼ 依赖
runner/http/ / runner/command/ / runner/[future]/
▼ 依赖
expect/ (断言基础设施) + schema/fragments.ts + utils.ts
```
禁止checker 之间横向依赖、expect/ 依赖 runner/、schema/ 依赖 runner/。
## Risks / Trade-offs
- **[编译期类型安全降低]** → 外部消费方engine、store不再能通过联合类型 narrow 到具体 checker 类型。缓解:这些消费方本来就不应该知道具体 checker 内部结构,这是设计意图而非缺陷。
- **[大量 import 路径变更]** → 所有测试文件和内部引用都需要更新。缓解:纯机械操作,可批量处理;项目未上线无兼容性负担。
- **[validate.ts 拆分复杂度]** → 原文件混合了通用和专属逻辑,拆分时需要仔细处理共享的辅助函数。缓解:`isPlainRecord` 等小工具可以在两处各自内联或放入 utils。