1
0

feat: 重构配置生命周期为 Authoring/Normalized/Resolved 三层

将变量替换和 expect 简写展开统一放入 Normalized 阶段,
运行时 AJV 使用 Normalized schema,导出 schema 面向 Authoring Config。

主要变更:
- 新增 normalizer.ts 实现 normalizeAuthoringConfig()
- 拆分 Authoring/Normalized 双 schema,checker 接口支持 authoring/normalized 片段
- config-loader 流程:normalize → Normalized AJV → semantic → resolve
- validator 兼容层自动分派 raw/normalized expect 形态
- 删除 rawExpect,store.expect 列写入 null
- Authoring schema 对 integer/boolean/enum 字段接受变量引用
- 修复 DB/HTTP validate 入口守卫和 LLM options integer 变量引用
- 优化 compact() 避免 undefined 覆盖隐患
- 移除 content.ts 恒为 true 的前置条件
- 同步 5 个主规范并归档 change
This commit is contained in:
2026-05-22 14:00:47 +08:00
parent 6e53c8130d
commit cf847ccd7a
56 changed files with 1717 additions and 656 deletions

View File

@@ -5,7 +5,7 @@
## Requirements
### Requirement: Checker 配置契约片段
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 target 领域分组和 expect 分组。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并组合为启动期 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。
系统 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 被注册
@@ -21,7 +21,11 @@
#### Scenario: 外部 schema 通过 registry 生成
- **WHEN** 系统生成 `probe-config.schema.json`
- **THEN** 生成流程 SHALL 从 registry 获取已注册 checker 的契约片段,并将其组合进完整配置 schema
- **THEN** 生成流程 SHALL 从 registry 获取已注册 checker 的 Authoring 契约片段,并将其组合进完整配置 schema
#### Scenario: 运行时 schema 通过 registry 生成
- **WHEN** config-loader 执行运行时 AJV 契约校验
- **THEN** 校验流程 SHALL 从 registry 获取已注册 checker 的 Normalized 契约片段,并将其组合进完整配置 schema
#### Scenario: 契约组装不依赖全局 singleton
- **WHEN** 测试或 schema 生成流程需要组装配置契约
@@ -50,11 +54,11 @@
- **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无需指定泛型。
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `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`配置分组名、TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult`serialize(target)`(返回 target 展示文本和 config JSON`buildDetail(observation)`(从 observation 构造人可读摘要)
- **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)`
@@ -62,7 +66,11 @@
#### Scenario: resolve 不承担通用契约校验
- **WHEN** config-loader 调用 checker.resolve()
- **THEN** checker.resolve() SHALL 假定配置已经通过 TypeBox/Ajv 契约校验和启动期语义校验,只负责默认值填充、路径解析和领域配置转换
- **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"`
@@ -125,6 +133,14 @@
- **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
@@ -146,22 +162,22 @@
- **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 执行计划
系统 SHALL 在 `ProbeStore.syncTargets()` 中通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要(`target` 列)和配置 JSON`config` 列),替代 `buildTargetDisplay()` / `buildTargetConfig()` 中的类型分支。系统 SHALL 将 `targets.expect` 持久化为 null不依赖 Raw expect Resolved expect。
#### Scenario: 序列化委托 checker
- **WHEN** store 同步 targets 表
- **THEN** store SHALL 对每个 target 调用对应 checker 的 `serialize()` 方法获取 `{ target, config }`
#### Scenario: expect 持久化使用 rawExpect
#### Scenario: expect 持久化不依赖 rawExpect
- **WHEN** store 同步带 expect 的 target 到 targets 表
- **THEN** store SHALL 将 `rawExpect` 序列化写入 `targets.expect`MUST NOT 将包含 `kind` Resolved content expectation 写入该列
- **THEN** store SHALL 将 `targets.expect` 写入 NULLMUST NOT 依赖 `rawExpect` Raw expect 快照
### 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 字段。
### Requirement: Checker resolve 只接收已去糖配置
每个 checker 的 `resolve()` SHALL 接收已通过 Normalized schema 和语义校验的配置不再包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL`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: 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 字段