新增 refactor-checker-cohesion 变更提案,包含 proposal、design、 specs 和 tasks,定义 checker 目录内聚结构规范。同时归档已完成的 历史变更记录。
233 lines
9.8 KiB
Markdown
233 lines
9.8 KiB
Markdown
## 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/fragments(schema 片段)、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。
|