## Purpose

定义 Checker 接口规范、注册机制、CheckerContext 上下文注入，以及共享 expect 断言函数的职责边界。此 capability 是 checker 系统的架构基础，不定义任何具体 checker 类型的业务行为。

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

#### Scenario: Checker 接口包含必要方法
- **WHEN** 开发者实现一个新的 Checker
- **THEN** 该实现 MUST 提供 `type`（字符串标识）、`configKey`（配置分组名）、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 假定配置已经通过 TypeBox/Ajv 契约校验和启动期语义校验，只负责默认值填充、路径解析和领域配置转换

#### Scenario: type 与 configKey 默认一致
- **WHEN** checker 定义 `type: "tcp"`
- **THEN** checker 的 `configKey` SHALL 默认使用 `"tcp"`，对应 target 的 `tcp` 分组和 defaults.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 在 `src/server/checker/runner/registry.ts` 中提供 `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 在 `ProbeEngine.runCheck()` 中通过 `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 在 `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 为 "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 在 `ProbeStore.syncTargets()` 中通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要（`target` 列）和配置 JSON（`config` 列），替代 `buildTargetDisplay()` / `buildTargetConfig()` 中的类型分支。系统 SHALL 将 `targets.expect` 持久化为 resolved target 上的 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 将 `rawExpect` 序列化写入 `targets.expect`，MUST NOT 将包含 `kind` 的 Resolved content expectation 写入该列

### Requirement: Checker resolve 生成 Raw 与 Resolved expect
每个 checker 的 `resolve()` SHALL 在解析 checker 专属 target 配置时，同时保留变量替换后的 Raw expect 快照并生成运行期 Resolved expect 执行计划。Raw expect SHALL 用于配置快照持久化；Resolved expect SHALL 用于 checker `execute()`。`config-loader` SHALL 继续通过 registry 委托 checker resolve，MUST NOT 在中间层理解 checker 专属 expect 字段。

#### Scenario: resolve 输出双 expect 模型
- **WHEN** config-loader 解析一个带 `expect.durationMs: 1000` 的 target
- **THEN** 对应 checker 的 resolved target SHALL 包含 Raw expect 中的 `durationMs: 1000`，并在 Resolved expect 中包含 `{equals: 1000}` 形式的运行期 matcher

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

### Requirement: 共享 expect 断言函数
系统 SHALL 在 `src/server/checker/expect/` 中提供可被多个 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 调用 `expect/failure.ts` 中的 `errorFailure()` 或 `mismatchFailure()`，并保留 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 类型
`shared/api.ts` 中 `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 直接展示而不做类型判断
