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

9.8 KiB
Raw Blame History

Context

当前 src/server/checker/ 的代码组织存在内聚性不足的问题:

  • 顶层 types.ts 混合了所有 checker 的类型定义(HttpTargetConfigResolvedHttpTargetCommandExpectConfig 等),形成硬编码联合类型 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 形式:

export interface DefaultsConfig {
  interval?: string;
  timeout?: string;
  [checkerKey: string]: unknown;
}

各 checker 的 validate() 方法接收 DefaultsConfig 后自行 narrowdefaults["http"] as HttpDefaultsConfig)。CheckerValidationInputResolveContext 中的 defaults 字段类型保持为 DefaultsConfig,对外部透明。

替代方案: 保留联合但用 barrel 自动聚合——仍需改文件,不够彻底。

具体设计:

// 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 的成本几乎为零。

具体设计:

// 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.tsindex.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.tsvalidateOperatorObject checker/expect/validate-operator.ts 通用 operator 校验
body.ts runner/http/body.ts 仅 HTTP 使用
text.ts runner/command/text.ts 仅 Command 使用
validate.tsvalidateBodyRules runner/http/validate.ts HTTP 专属校验
validate.tsvalidateTextRules 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.tsparseSizeconfig-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。