## Purpose

定义 checker 模块的内聚化组织结构，确保每个 checker 以独立目录形式存在，包含其全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。同时定义严格的依赖方向约束、Checker 接口定义、CheckerRegistry 注册中心、配置契约片段、配置校验 issue、引擎调度和服务注册委托。

## Requirements

### Requirement: Checker 目录内聚结构
每个 checker SHALL 以独立目录形式存在于 checker runner 目录下，目录内 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。

#### Scenario: checker 目录完整性
- **WHEN** 开发者查看某个 checker 目录
- **THEN** 该目录 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑

#### Scenario: 新增 checker 最小改动
- **WHEN** 开发者新增一个 checker 类型（如 dns）
- **THEN** 开发者 SHALL 只需创建 checker 目录及其内部文件，并在注册列表中添加一行 import 和一行数组项

### Requirement: 断言基础设施
系统 SHALL 提供所有 checker 共享的断言基础设施，使用 Raw/Resolved expectation 术语和 value/content/keyed/status/headers 模块边界。

### Requirement: Schema 体系
系统 SHALL 通过 schema 体系组织配置校验、契约片段和 issue 报告，支持从 registry 动态构建整体配置 schema、共享 schema 片段引用、Ajv 校验入口和 ConfigValidationIssue 构造。

### Requirement: 依赖方向约束
checker 系统内的模块依赖 SHALL 遵循严格的分层方向。

#### Scenario: checker 之间无横向依赖
- **WHEN** 开发者查看任何 checker 目录的 import 语句
- **THEN** 该 checker SHALL NOT 导入其他 checker 目录的任何模块

#### Scenario: expect/ 不依赖 runner/
- **WHEN** 开发者查看 `expect/` 目录的 import 语句
- **THEN** `expect/` 中的文件 SHALL NOT 导入 `runner/` 目录的任何模块

#### Scenario: schema/ 不依赖 runner/ 的具体 checker
- **WHEN** 开发者查看 `schema/` 目录的 import 语句
- **THEN** `schema/` 中的文件 SHALL 仅通过 `CheckerDefinition` 接口与 checker 交互，SHALL NOT 直接导入具体 checker 目录

### Requirement: 显式注册列表
系统 SHALL 在 checker 注册入口文件中使用显式 import 列表注册所有 checker。

#### Scenario: 注册入口结构
- **WHEN** 开发者查看 checker 注册入口文件
- **THEN** 该文件 SHALL 包含所有 checker 的静态 import 和一个 checker 实例数组，通过循环调用 `registry.register()` 完成注册

#### Scenario: 新增 checker 注册
- **WHEN** 开发者新增一个 checker
- **THEN** 开发者 SHALL 在注册入口文件中添加一行 import 和一行数组项，无需修改其他文件

### Requirement: Checker 配置契约片段
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段，用于描述该 checker 的 target 领域分组和 expect 分组。checker 契约 SHALL 区分 Authoring schema 与 Normalized schema。Authoring schema SHALL 描述用户 YAML 可书写形式，包括变量引用和 expect 简写；Normalized schema SHALL 描述 `normalizeAuthoringConfig()` 输出形式，不接受变量引用、不接受 expect primitive 简写。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段，并按用途组合为运行时 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。

#### Scenario: HTTP checker 提供契约片段
- **WHEN** HTTP checker 被注册
- **THEN** registry SHALL 能提供 HTTP target 和 HTTP expect 的 TypeBox 契约片段

#### Scenario: Cmd checker 提供契约片段
- **WHEN** Cmd checker 被注册
- **THEN** registry SHALL 能提供 Cmd target 和 Cmd expect 的 TypeBox 契约片段

#### Scenario: 新 checker 只维护自身契约
- **WHEN** 开发者新增一个 checker 类型
- **THEN** 该 checker SHALL 提供自身 TypeBox 配置契约和语义 validator，而不需要把 checker 专属字段写入中央手工校验逻辑

#### Scenario: 外部 schema 通过 registry 生成
- **WHEN** 系统生成 `probe-config.schema.json`
- **THEN** 生成流程 SHALL 从 registry 获取已注册 checker 的 Authoring 契约片段，并将其组合进完整配置 schema

#### Scenario: 运行时 schema 通过 registry 生成
- **WHEN** config-loader 执行运行时 AJV 契约校验
- **THEN** 校验流程 SHALL 从 registry 获取已注册 checker 的 Normalized 契约片段，并将其组合进完整配置 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 定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`，包含 `type`、`configKey`、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve`、`execute`、`serialize`、`buildDetail` 成员。泛型参数 SHALL 约束 `execute` 和 `serialize` 方法的 target 参数类型，使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层（registry、engine、config-loader）无需指定泛型。

#### Scenario: Checker 接口包含必要方法
- **WHEN** 开发者实现一个新的 Checker
- **THEN** 该实现 MUST 提供 `type`（字符串标识）、`configKey`（配置分组名）、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve(target, context)`（解析配置并填充默认值）、`execute(target, ctx)`（执行探测返回 CheckResult）、`serialize(target)`（返回 target 展示文本和 config JSON）和 `buildDetail(observation)`（从 observation 构造人可读摘要）

#### 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 假定配置已经通过 Normalized TypeBox/Ajv 契约校验和启动期语义校验，只负责默认值填充、路径解析和领域配置转换

#### Scenario: resolve 接收 Normalized target
- **WHEN** config-loader 调用 checker.resolve()
- **THEN** 传入的 target SHALL 已通过 Normalized schema 和语义校验，且不包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL

#### Scenario: type 与 configKey 默认一致
- **WHEN** checker 定义 `type: "tcp"`
- **THEN** checker 的 `configKey` SHALL 默认使用 `"tcp"`，对应 target 的 `tcp` 分组

#### Scenario: 接口方法使用泛型约束
- **WHEN** 开发者查看 `CheckerDefinition<TResolved>` 接口签名
- **THEN** `resolve` 的返回值 SHALL 为 `TResolved`；`execute` 的参数 SHALL 为 `TResolved`；`serialize` 的参数 SHALL 为 `TResolved`

#### Scenario: checker 实现无需手动断言
- **WHEN** HttpChecker 实现 `CheckerDefinition<ResolvedHttpTarget>`
- **THEN** `execute` 方法的 target 参数类型 SHALL 直接为 `ResolvedHttpTarget`，无需在方法内部使用 `as` 类型断言

#### Scenario: registry 使用默认泛型参数
- **WHEN** CheckerRegistry 存储和返回 checker 实例
- **THEN** registry 内部 SHALL 使用 `CheckerDefinition`（等价于 `CheckerDefinition<ResolvedTargetBase>`），实现类型擦除

#### Scenario: buildDetail 方法签名
- **WHEN** 开发者实现 buildDetail 方法
- **THEN** 方法签名 SHALL 为 `buildDetail(observation: Record<string, unknown>): string | null`，接收 observation 对象并返回人可读摘要字符串或 null

#### Scenario: buildDetail 由 API 层调用
- **WHEN** API 序列化 CheckResult
- **THEN** API 层 SHALL 通过 registry 获取对应 checker 并调用 buildDetail，而非由 execute 方法直接生成 detail

### Requirement: CheckerRegistry 注册中心
系统 SHALL 提供 `CheckerRegistry` 类，支持 `register(checker)`、`get(type)` 和 `supportedTypes`。重复注册同一 type SHALL 抛出错误。registry 内部 SHALL 存储 `CheckerDefinition`（使用默认泛型参数），对外提供类型擦除后的接口。

#### Scenario: 注册并获取 Checker
- **WHEN** 调用 `registry.register(new HttpChecker())` 后再调用 `registry.get("http")`
- **THEN** 返回的 SHALL 是之前注册的 HttpChecker 实例（类型为 `CheckerDefinition`）

#### Scenario: 获取未注册的 type
- **WHEN** 调用 `registry.get("unknown")` 且未注册对应 type 的 checker
- **THEN** 系统 SHALL 抛出错误，提示不支持的 probe type

#### Scenario: 重复注册
- **WHEN** 同一 type 值被重复 `register()`
- **THEN** 系统 SHALL 抛出错误，提示该 type 已注册

#### Scenario: 查询支持的 type 列表
- **WHEN** 注册了 "http" 和 "cmd" 两个 checker 后查询 `registry.supportedTypes`
- **THEN** 返回的数组 SHALL 包含 `["http", "cmd"]`（按注册顺序）

### Requirement: 引擎通过 registry 调度 checker
系统 SHALL 在引擎执行检查时通过 `checkerRegistry.get(target.type).execute(target, ctx)` 调度检查，替代原有的 `switch/case` 分支。

#### Scenario: 引擎使用 registry 调度
- **WHEN** engine 需要执行一个 type 为 "http" 的 target
- **THEN** engine SHALL 从 `checkerRegistry` 中获取对应 checker 并调用其 `execute()` 方法，不再使用 `switch/case`

#### Scenario: 引擎注入超时 signal
- **WHEN** engine 调度一次 checker 执行
- **THEN** engine SHALL 创建 `AbortController`，设置超时定时器，将 `controller.signal` 注入 `CheckerContext`，执行完成后清理定时器

### Requirement: 配置解析通过 registry 委托 checker
系统 SHALL 在配置加载流程中通过 `checkerRegistry` 发现已注册 checker，组合公共 TypeBox 契约与 checker 契约，并将 checker 专属语义校验和解析委托给对应 checker。公共配置校验 SHALL 仅保留公共语义校验（name 非空、name 不重复、group 类型、type 已注册等）和契约调度职责，不包含 checker 专属字段校验。

#### Scenario: 配置契约通过 registry 组合
- **WHEN** config-loader 校验配置文件
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的契约片段，并用于校验 defaults 与 targets 中对应 checker 的配置形状

#### Scenario: Authoring 契约通过 registry 组合
- **WHEN** 系统导出用户配置 JSON Schema
- **THEN** 配置 builder SHALL 从 `checkerRegistry` 获取已注册 checker 的 Authoring 契约片段

#### Scenario: Normalized 契约通过 registry 组合
- **WHEN** config-loader 校验 normalized 配置对象
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的 Normalized 契约片段

#### Scenario: 配置解析委托 checker
- **WHEN** config-loader 解析一个 type 为 "cmd" 的 target
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("cmd")` 获取对应 checker，并委托该 checker 执行语义校验和 resolve

#### Scenario: 通用字段校验保留在 config-loader
- **WHEN** YAML 配置中某个 target 缺少 name 或 type 字段
- **THEN** config-loader 的公共校验流程 SHALL 仍负责校验这些通用字段

#### Scenario: type 专属校验下沉到 checker
- **WHEN** YAML 配置中 HTTP target 缺少 `http.url`
- **THEN** HTTP checker 的契约或语义校验 SHALL 抛出校验错误，提示缺少必填字段

#### Scenario: HTTP method 非法校验
- **WHEN** YAML 配置中 HTTP target 的 `http.method` 不是大写合法方法枚举值
- **THEN** HTTP checker 契约或语义校验 SHALL 抛出校验错误，提示 method 不合法

#### Scenario: URL 格式校验
- **WHEN** YAML 配置中 HTTP target 的 `http.url` 不以 `http://` 或 `https://` 开头
- **THEN** HttpChecker 的语义校验 SHALL 抛出校验错误，提示 URL 格式不合法

### Requirement: 存储序列化通过 registry 获取展示格式
系统 SHALL 在存储同步 targets 时通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要和配置 JSON，替代函数中的类型分支。系统 SHALL 将 `targets.expect` 持久化为 null，不依赖 Raw expect 或 Resolved expect。

#### Scenario: 序列化委托 checker
- **WHEN** store 同步 targets 表
- **THEN** store SHALL 对每个 target 调用对应 checker 的 `serialize()` 方法获取 `{ target, config }`

#### Scenario: expect 持久化不依赖 rawExpect
- **WHEN** store 同步带 expect 的 target 到 targets 表
- **THEN** store SHALL 将 `targets.expect` 写入 NULL，MUST NOT 依赖 `rawExpect` 或 Raw expect 快照

### Requirement: Checker resolve 只接收已去糖配置
每个 checker 的 `resolve()` SHALL 接收已通过 Normalized schema 和语义校验的配置，不再包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL。`config-loader` SHALL 继续通过 registry 委托 checker resolve，MUST NOT 在中间层理解 checker 专属 expect 字段。

#### Scenario: resolve 不再展开 Raw expect
- **WHEN** config-loader 解析一个带 `expect.durationMs: {equals: 1000}` 的 target
- **THEN** 对应 checker 的 resolved target SHALL 直接使用 Normalized expect 中的 `{equals: 1000}`，resolve 只负责默认值和运行期配置转换

#### Scenario: 中间层不感知 checker expect 字段
- **WHEN** 新增 checker 定义自己的 Raw/Resolved expect 字段
- **THEN** config-loader SHALL 只调用该 checker 的 `validate()` 和 `resolve()`，不新增 checker 类型分支

### Requirement: 共享 expect 断言函数
系统 SHALL 提供可被多个 checker 复用的 expect 基础设施。共享基础设施 SHALL 包含 value expectation、content expectations、keyed expectations、status code 断言、headers keyed 断言、failure 构造和 ReDoS 校验。checker 专用的状态类断言 SHALL 保留在对应 checker 目录，或在多个 checker 复用时移动到共享模块。仅被单个 checker 使用且不属于通用 value/content/keyed/status/header 模型的断言模块 SHALL 位于该 checker 目录内。

#### Scenario: 共享 ValueExpectation 断言
- **WHEN** 任何 checker 需要对数字、字符串、布尔或 JSON value 执行 matcher 匹配
- **THEN** SHALL 调用共享 value expectation 工具执行 `equals`、`contains`、`regex`、`exists`、`empty`、`gt`、`gte`、`lt` 和 `lte` 语义

#### Scenario: 共享 ContentExpectations 断言
- **WHEN** HTTP body、LLM output、Cmd stdout/stderr、UDP response 或 TCP banner 需要执行返回内容校验
- **THEN** SHALL 调用共享 content expectations 工具，而不是在 checker 目录内复制 contains/regex/json/css/xpath 逻辑

#### Scenario: 共享 KeyedExpectations 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers，或 DB checker 需要校验 rows 中的列值
- **THEN** SHALL 调用共享 keyed expectations 工具，并按调用方规则决定 key 是否大小写敏感

#### Scenario: 共享 headers 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers
- **THEN** SHALL 调用共享 header expectation 包装函数，确保 header key 大小写不敏感

#### Scenario: 共享 regex ReDoS 校验
- **WHEN** 任一 matcher 或 content expectation 配置 `regex`
- **THEN** SHALL 调用共享 ReDoS 校验工具在启动期拒绝危险正则

#### Scenario: 共享 failure 构造
- **WHEN** 任何 checker 需要构造 CheckFailure 对象
- **THEN** SHALL 调用共享的 `errorFailure()` 或 `mismatchFailure()` 构造 CheckFailure，并保留 actual 截断策略

#### Scenario: 共享 status 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应状态码
- **THEN** SHALL 复用共享 status code 断言函数，支持精确状态码和 `1xx` 到 `5xx` 范围模式

### Requirement: 超时控制由引擎注入 signal
Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时，不得自行创建 `AbortController` 或 `setTimeout` 用于超时控制。Cmd checker 和 ping checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。

#### Scenario: HTTP checker 使用 signal
- **WHEN** HttpChecker 执行 HTTP 请求
- **THEN** SHALL 将 `ctx.signal` 传入 `fetch()` 的 `signal` 选项，不自行创建 `AbortController`

#### Scenario: Cmd checker 响应 signal
- **WHEN** CommandChecker 执行命令且 signal 被 abort
- **THEN** SHALL 调用 `proc.kill()` 终止子进程，并在 CheckResult 中记录超时错误

#### Scenario: Ping checker 响应 signal
- **WHEN** IcmpChecker 执行 ping 命令且 signal 被 abort
- **THEN** SHALL 调用 `proc.kill()` 终止 ping 子进程，并在 CheckResult 中记录超时错误

### Requirement: CheckFailure.phase 使用 string 类型
`CheckFailure.phase` 的类型 SHALL 定义为 `string`，替代原有的硬编码联合类型 `"status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr"`。

#### Scenario: phase 支持 checker 专用值
- **WHEN** cmd checker 在执行失败（spawn error）时生成 failure
- **THEN** `failure.phase` SHALL 可以是 `"spawn"` 等任意字符串值，类型系统 SHALL 不报错

#### Scenario: 前端展示 phase 不依赖硬编码类型
- **WHEN** 前端收到任意 phase 字符串值
- **THEN** 前端 SHALL 直接展示而不做类型判断
