1
0
Files
DiAL/openspec/specs/checker-runner-abstraction/spec.md
lanyuanxiaoyao 60a54b483f refactor: expect 类型模型重构,Raw/Resolved 双层分离与断言基础设施内聚
- 重命名 ContentRules→ContentExpectations, KeyValueExpect→KeyedExpectations
- 新增 Raw/Resolved 双层模型:resolve 阶段物化为执行计划,store 持久化 Raw 快照
- HTTP body 按需读取:status/headers 失败或无 body expectation 时不读取 body
- 新增 displayValueExpectation() 解包 failure.expected 用户可读展示
- 修复 checkEarlyTimeout 独立 lte/lt 检查,修复 KeyedExpectations JSON Schema
- 新增 expect/value.ts(resolve/check/display)、keyed.ts、content.ts、headers.ts、status.ts
- 删除旧 normalize.ts/matcher.ts/validate-matcher.ts/key-value.ts
- 更新 DEVELOPMENT.md:expect 五层管线表、displayValueExpectation、1.7↔1.10 交叉引用
- 同步 13 个 main specs,归档 refactor-expect-type-model 变更(62/62 tasks)
2026-05-20 16:12:48 +08:00

15 KiB
Raw Blame History

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 表示配置校验问题,至少包含 codepathmessage,并支持可选 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>,包含 typeconfigKey、TypeBox 配置契约、启动期语义校验、resolveexecuteserializebuildDetail 成员。泛型参数 SHALL 约束 executeserialize 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 = ResolvedTargetBase 保证中间层registry、engine、config-loader无需指定泛型。

Scenario: Checker 接口包含必要方法

  • WHEN 开发者实现一个新的 Checker
  • THEN 该实现 MUST 提供 type(字符串标识)、configKey配置分组名、TypeBox 配置契约、启动期语义校验、resolve(target, context)(解析配置并填充默认值)、execute(target, ctx)(执行探测返回 CheckResultserialize(target)(返回 target 展示文本和 config JSONbuildDetail(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 为 TResolvedexecute 的参数 SHALL 为 TResolvedserialize 的参数 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 列)和配置 JSONconfig 列),替代 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.expectMUST 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 resolveMUST 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 工具执行 equalscontainsregexexistsemptygtgteltlte 语义

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 断言函数,支持精确状态码和 1xx5xx 范围模式

Requirement: 超时控制由引擎注入 signal

Checker 实现的 execute() MUST 使用 ctx.signal 感知超时,不得自行创建 AbortControllersetTimeout 用于超时控制。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.tsCheckFailure.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 直接展示而不做类型判断