1
0

feat: 重构配置校验为 TypeBox + Ajv + semantic validator,严格禁止未知字段

- 新增 config-contract 模块(TypeBox fragments、Ajv 契约校验、ConfigValidationIssue)
- CheckerDefinition 扩展为含 configKey、schemas、validate 的完整插件接口
- HTTP/Command 各自维护 contract.ts + validate.ts,校验从 resolve 中分离
- resolve 不再承担校验,只做默认值合并和路径/单位解析
- config-loader 流程: unknown → RawProbeConfig → ValidatedProbeConfig → ResolvedConfig
- 导出 probe-config.schema.json,新增 schema/schema:check 脚本
- 更新 DEVELOPMENT.md 新增 1.7 开发新 Checker 完整指引
- 同步更新 4 个 main specs(probe-config、command-checker、expect-body-checkers、checker-runner-abstraction)
This commit is contained in:
2026-05-13 12:19:36 +08:00
parent bce0f8e7a8
commit 7b20b59b79
38 changed files with 3034 additions and 675 deletions

View File

@@ -4,17 +4,70 @@
## Requirements
### Requirement: Checker 配置契约片段
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 defaults 分组、target 领域分组和 expect 分组。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并组合为启动期 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。
#### Scenario: HTTP checker 提供契约片段
- **WHEN** HTTP checker 被注册
- **THEN** registry SHALL 能提供 HTTP defaults、HTTP target 和 HTTP expect 的 TypeBox 契约片段
#### Scenario: Command checker 提供契约片段
- **WHEN** Command checker 被注册
- **THEN** registry SHALL 能提供 Command defaults、Command target 和 Command expect 的 TypeBox 契约片段
#### Scenario: 新 checker 只维护自身契约
- **WHEN** 开发者新增一个 checker 类型
- **THEN** 该 checker SHALL 提供自身 TypeBox 配置契约和语义 validator而不需要把 checker 专属字段写入中央手工校验逻辑
#### Scenario: 外部 schema 通过 registry 生成
- **WHEN** 系统生成 `probe-config.schema.json`
- **THEN** 生成流程 SHALL 从 registry 获取已注册 checker 的契约片段,并将其组合进完整配置 schema
#### Scenario: 契约组装不依赖全局 singleton
- **WHEN** 测试或 schema 生成流程需要组装配置契约
- **THEN** 系统 SHALL 支持传入 fresh CheckerRegistry 实例完成契约组装,避免重复注册或全局状态污染
### Requirement: Checker 启动期语义校验
系统 SHALL 支持 checker 提供启动期语义 validator用于校验 TypeBox/Ajv 契约不适合表达或需要 checker 业务知识判断的配置规则。语义 validator MUST 在 resolver 填充最终 ResolvedTarget 之前执行,并 MUST 返回 `ConfigValidationIssue[]`
#### Scenario: checker 语义校验先于 resolve
- **WHEN** config-loader 准备解析一个 target
- **THEN** 系统 SHALL 先完成该 target 的 checker 语义校验,再调用 checker.resolve()
#### Scenario: 语义校验失败阻止启动
- **WHEN** checker 语义 validator 发现非法配置
- **THEN** 系统 SHALL 以配置错误退出,不进入 checker 执行阶段
### Requirement: 结构化配置校验 issue
系统 SHALL 使用统一 `ConfigValidationIssue` 表示配置校验问题,至少包含 `code``path``message`,并支持可选 `targetName`。契约校验和 checker 语义校验都 SHALL 产出该结构,由配置加载模块统一渲染为中文错误。
#### Scenario: Ajv 错误转换为 issue
- **WHEN** Ajv 校验发现 required、type 或 additionalProperties 错误
- **THEN** 系统 SHALL 将该错误转换为 `ConfigValidationIssue`,保留配置路径和可读 message
#### Scenario: checker validator 返回 issue
- **WHEN** checker 语义 validator 发现非法 XPath 或正则表达式
- **THEN** checker SHALL 返回 `ConfigValidationIssue`,而不是直接抛出最终用户错误字符串
### Requirement: Checker 接口定义
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义 `Checker` 接口,包含 `type``resolve``execute``serialize` 四个成员。`CheckerContext` SHALL 包含引擎注入的 `AbortSignal`
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的 `CheckerDefinition`,包含 `type``configKey`、TypeBox 配置契约、启动期语义校验、`resolve``execute``serialize` 成员。`CheckerContext` SHALL 包含引擎注入的 `AbortSignal`
#### Scenario: Checker 接口包含必要方法
- **WHEN** 开发者实现一个新的 Checker
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`resolve(target, context)`(解析配置并校验)、`execute(target, ctx)`(执行探测返回 CheckResult`serialize(target)`(返回 target 展示文本和 config JSON
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`配置分组名、TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult`serialize(target)`(返回 target 展示文本和 config JSON
#### Scenario: CheckerContext 注入 signal
- **WHEN** 引擎调用 `checker.execute(target, ctx)`
- **THEN** `ctx.signal` SHALL 是一个由引擎创建的 `AbortSignal`,在超时或引擎关闭时 abort
#### Scenario: resolve 不承担通用契约校验
- **WHEN** config-loader 调用 checker.resolve()
- **THEN** checker.resolve() SHALL 假定配置已经通过 TypeBox/Ajv 契约校验和启动期语义校验,只负责默认值填充、路径解析和领域配置转换
#### Scenario: type 与 configKey 默认一致
- **WHEN** checker 定义 `type: "tcp"`
- **THEN** checker 的 `configKey` SHALL 默认使用 `"tcp"`,对应 target 的 `tcp` 分组和 defaults.tcp 分组
### Requirement: CheckerRegistry 注册中心
系统 SHALL 在 `src/server/checker/runner/registry.ts` 中提供 `CheckerRegistry` 类,支持 `register(checker)``get(type)``supportedTypes`。重复注册同一 type SHALL 抛出错误。
@@ -46,27 +99,31 @@
- **THEN** engine SHALL 创建 `AbortController`,设置超时定时器,将 `controller.signal` 注入 `CheckerContext`,执行完成后清理定时器
### Requirement: 配置解析通过 registry 委托 checker
系统 SHALL 在 `config-loader.ts` `resolveTarget()` 中通过 `checkerRegistry.get(target.type).resolve(target, context)` 委托解析,替代原有的 `if/else` 分支`validateConfig()` SHALL 仅校验通用字段name 非空、name 不重复、group 类型,不包含 type 专属字段校验。
系统 SHALL 在 `config-loader.ts`配置加载流程中通过 `checkerRegistry` 发现已注册 checker组合公共 TypeBox 契约与 checker 契约,并将 checker 专属语义校验和解析委托给对应 checker`validateConfig()` SHALL 仅保留公共语义校验name 非空、name 不重复、group 类型、type 已注册等)和契约调度职责,不包含 checker 专属字段校验。
#### Scenario: 配置契约通过 registry 组合
- **WHEN** config-loader 校验配置文件
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的契约片段,并用于校验 defaults 与 targets 中对应 checker 的配置形状
#### Scenario: 配置解析委托 checker
- **WHEN** config-loader 解析一个 type 为 "command" 的 target
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("command").resolve()` 进行解析、校验和默认值填充
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("command")` 获取对应 checker并委托该 checker 执行语义校验和 resolve
#### Scenario: 通用字段校验保留在 config-loader
- **WHEN** YAML 配置中某个 target 缺少 name 或 type 字段
- **THEN** config-loader 的 `validateConfig()` SHALL 仍负责校验这些通用字段
- **THEN** config-loader 的公共校验流程 SHALL 仍负责校验这些通用字段
#### Scenario: type 专属校验下沉到 checker
- **WHEN** YAML 配置中 HTTP target 缺少 `http.url`
- **THEN** HttpChecker 的 `resolve()` SHALL 抛出校验错误,提示缺少必填字段
- **THEN** HTTP checker 的契约或语义校验 SHALL 抛出校验错误,提示缺少必填字段
#### Scenario: HTTP method 非法校验
- **WHEN** YAML 配置中 HTTP target 的 `http.method`合法方法列表中
- **THEN** HttpChecker `resolve()` SHALL 抛出校验错误,提示 method 不合法
- **WHEN** YAML 配置中 HTTP target 的 `http.method`是大写合法方法枚举值
- **THEN** HTTP checker 契约或语义校验 SHALL 抛出校验错误,提示 method 不合法
#### Scenario: URL 格式校验
- **WHEN** YAML 配置中 HTTP target 的 `http.url` 不以 `http://``https://` 开头
- **THEN** HttpChecker 的 `resolve()` SHALL 抛出校验错误,提示 URL 格式不合法
- **THEN** HttpChecker 的语义校验 SHALL 抛出校验错误,提示 URL 格式不合法
### Requirement: 存储序列化通过 registry 获取展示格式
系统 SHALL 在 `ProbeStore.syncTargets()` 中通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要(`target` 列)和配置 JSON`config` 列),替代 `buildTargetDisplay()` / `buildTargetConfig()` 中的类型分支。