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

@@ -139,7 +139,7 @@ probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动
启动流程:
dev.ts / main.ts → readRuntimeConfig(cli args, 仅提取 configPath)
→ bootstrap({ configPath, mode })
→ loadConfig(yamlYAML 解析 → 变量替换 → 契约校验 → 语义校验 → resolve)
→ loadConfig(yamlYAML 解析 → Authoring normalize变量替换 + expect 简写展开)→ Normalized 契约校验 → 语义校验 → resolve)
→ ResolvedConfig{ host(server.listen), port(server.listen), dataDir(server.storage), maxConcurrentChecks(probes.execution), retentionMs(server.storage), targets, logging(server.logging) }
→ createRuntimeLogger(logging) → Logger配置加载失败时使用 ConsoleFallbackLogger
→ ProbeStore(db) → store.syncTargets(targets)
@@ -246,9 +246,13 @@ export function handleMetrics(idStr: string, url: URL, store: ProbeStore, mode:
### 1.6 配置契约与校验
配置加载流程固定为:`unknown -> 变量替换 -> RawProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`
配置加载流程固定为:`unknown -> AuthoringProbeConfig -> NormalizedProbeConfig -> ValidatedProbeConfig -> ResolvedConfig`
变量替换阶段由 `variables.ts` 负责,在 YAML 解析之后、AJV 契约校验之前执行。顶层 `variables` 支持 string/number/boolean 字面量且自身不参与替换;`server``probes``targets` 字符串字段支持 `${key}``${key|default}``${key|}``$${key}`,解析优先级为 `variables -> process.env -> 默认值 -> unresolved-variable 报错`;替换范围不包含对象 key且仅跳过 `targets[].id``targets[].type` 字段
Authoring Config 是用户 YAML 可书写形态,允许 `${key}``${key|default}``${key|}``$${key}` 变量引用和 expect primitive/keyed/content 简写。`normalizeAuthoringConfig()` 在 YAML 解析之后、AJV 契约校验之前执行,只做去糖:调用 `variables.ts` 完成变量替换,展开 expect 简写,并移除顶层 `variables` 段。Normalized Config 不包含变量引用、不包含 Raw expect primitive 简写,也不补默认值、不解析 duration/size/path/env、不合并 `cmd.env`
根目录 `probe-config.schema.json` 由 Authoring schema 导出,服务 VSCode 和外部用户校验;运行时 `validateProbeConfigContract()` 使用 Normalized schema。checker 必须提供 `schemas.authoring.config``schemas.authoring.expect``schemas.normalized.config``schemas.normalized.expect` 四个 TypeBox 片段Authoring 片段描述用户可写 DSLNormalized 片段描述 normalizer 输出。
变量替换解析优先级为 `variables -> process.env -> 默认值 -> unresolved-variable 报错`;替换范围不包含对象 key且仅跳过 `targets[].id``targets[].type` 字段。字段值完整等于单个变量引用时保留 number/boolean/string 类型推断,部分拼接时统一转为字符串。
`config-loader.ts` 只负责 YAML 解析、契约调度、公共语义校验和最终运行期解析checker 专属规则必须下沉到对应 checker 的 `schema.ts``validate.ts`
@@ -375,29 +379,28 @@ TcpChecker implements Checker
**`resolve()` 规范**
- 只做内置默认值填充、路径解析、单位转换,**不执行校验**
- 若 checker 支持 expect必须保留 `rawExpect` 为变量替换后的 Raw 配置快照,并生成只供 `execute()` 使用的 Resolved `expect`
- `resolve()` 接收的 target 已通过 Normalized schema 和语义校验expect 已是 normalized 形态primitive 简写已展开为 `{equals}`、keyed 已转为 `{key, matcher}[]`、content 已转为带 `kind` 的执行结构)
- `resolve()` 对 expect 只做默认值填充(如未配置 `status` 时物化 `[200]`)和 spread 透传,不再调用 Raw 简写展开函数
- 返回 `satisfies ResolvedXxxTarget` 确保类型正确
**expect 五层管线**每种断言模型从定义到执行经过五个阶段,每层使用对应的共享函数
**expect 管线**配置从定义到执行经过 Authoring → Normalized → Resolved → Execute 四层,简写展开在 normalizer 阶段完成
| 断言模型 | 类型层Raw | Schema | Validate 层 | Resolve 层 | Execute 层 |
| --------------------- | ----------------------------------- | ----------------------------------- | ---------------------------------- | ------------------------------ | ---------------------------- |
| `ValueExpectation` | `number \| ValueMatcher` | `createValueMatcherSchema()` | `validateRawValueExpectation()` | `resolveValueExpectation()` | `checkValueExpectation()` |
| `ContentExpectations` | `(ValueMatcher \| ExtractorRule)[]` | `createContentExpectationsSchema()` | `validateRawContentExpectations()` | `resolveContentExpectations()` | `checkContentExpectations()` |
| `KeyedExpectations` | `Record<string, ValueExpectation>` | `createKeyedExpectationsSchema()` | `validateRawKeyedExpectations()` | `resolveKeyedExpectations()` | `checkKeyedExpectations()` |
| 断言模型 | Authoring 输入 | Normalizer| Schema 层Authoring / Normalized | Validate 层(接收 Normalized | Execute 层 |
| --------------------- | ----------------------------------- | ------------------------------ | -------------------------------------------------------------------- | ---------------------------------- | ---------------------------- |
| `ValueExpectation` | `number \| ValueMatcher` | `resolveValueExpectation()` | `createAuthoringValueExpectationSchema()` / `createNormalized*()` | `validateRawValueExpectation()` | `checkValueExpectation()` |
| `ContentExpectations` | `(ValueMatcher \| ExtractorRule)[]` | `resolveContentExpectations()` | `createAuthoringContentExpectationsSchema()` / `createNormalized*()` | `validateRawContentExpectations()` | `checkContentExpectations()` |
| `KeyedExpectations` | `Record<string, ValueExpectation>` | `resolveKeyedExpectations()` | `createAuthoringKeyedExpectationsSchema()` / `createNormalized*()` | `validateRawKeyedExpectations()` | `checkKeyedExpectations()` |
选择哪种模型参考 [1.10 expect 字段选择规范](#110-expect-断言系统)的决策树。
**resolve 中的标准模式**
```typescript
// resolve() 内:逐字段调用对应的 resolve 函数,未配置的字段保持 undefined
const rawExpect = raw.expect ?? {};
expect: {
durationMs: rawExpect.durationMs != null ? resolveValueExpectation(rawExpect.durationMs) : undefined,
body: rawExpect.body != null ? resolveContentExpectations(rawExpect.body) : undefined,
headers: rawExpect.headers != null ? resolveKeyedExpectations(rawExpect.headers) : undefined,
}
// resolve() 内:expect 已是 normalized 形态spread 后补默认值
const expect = target.expect as ResolvedXxxExpectConfig | undefined;
const resolvedExpect: ResolvedXxxExpectConfig = expect
? { ...expect, status: expect.status ?? [200] }
: { status: [200] };
```
**execute 中的标准模式**
@@ -627,7 +630,7 @@ expect(logger.entries[0]!.msg).toContain("UP → DOWN");
两层模型:**观测值收集** → **规则校验**。共享断言基础设施位于 `checker/expect/`checker 专属状态断言位于各自目录。
**Raw vs Resolved**:用户 YAML 写的是 Raw 形态primitive 简写、`{ json: { path, equals } }` 提取器对象、`Record<string, value>` 键值表),`config-loader` 的 resolve 阶段将其转换为 Resolved 形态供运行期执行`{ equals: primitive }``{ kind, matcher, ... }` content 联合、`{ key, matcher }[]` 有序数组)。Store 持久化 Raw 快照(`rawExpect`checker.execute 消费 Resolved `expect`
**Authoring → Normalized → Resolved**:用户 YAML 写的是 Authoring 形态primitive 简写、`{ json: { path, equals } }` 提取器对象、`Record<string, value>` 键值表),`normalizeAuthoringConfig()` 在 AJV 前将其转换为 Normalized 形态`{ equals: primitive }``{ kind, matcher, ... }` content 联合、`{ key, matcher }[]` 有序数组)。语义校验器接收 Normalized 形态并校验。`resolve()` 只补默认值和做运行期转换。Store `targets.expect` 列当前写入 NULL不持久化 expect 快照;checker.execute 消费 Resolved `expect`
**共享模型**
@@ -637,11 +640,11 @@ expect(logger.entries[0]!.msg).toContain("UP → DOWN");
| `ContentExpectations` | 返回内容或半结构化内容断言,必须是数组 | `body``stdout``stderr``banner``response``output``result` |
| `KeyedExpectations` | 动态键值断言,字面量等价于 `{ equals: value }` | `headers`、DB `rows[]` 中的列值 |
`ValueMatcher` 支持 `equals``contains``regex``empty``exists``gte``lte``gt``lt`。一个 matcher 对象内多个字段为 AND 语义;`exists: false` 不能和其他 matcher 组合;`equals` 使用 `es-toolkit/isEqual` 做 JSON 深度相等;`regex` 固定为无 flags 的 `new RegExp(pattern).test(String(actual))`。ValueExpectation Raw 输入可使用 string、number、boolean 或 null 简写,resolve 阶段归一化为 `{ equals: value }`;数组和对象简写不支持,必须显式写成 `{ equals: ... }`
`ValueMatcher` 支持 `equals``contains``regex``empty``exists``gte``lte``gt``lt`。一个 matcher 对象内多个字段为 AND 语义;`exists: false` 不能和其他 matcher 组合;`equals` 使用 `es-toolkit/isEqual` 做 JSON 深度相等;`regex` 固定为无 flags 的 `new RegExp(pattern).test(String(actual))`。ValueExpectation Authoring 输入可使用 string、number、boolean 或 null 简写,normalizer 阶段归一化为 `{ equals: value }`;数组和对象简写不支持,必须显式写成 `{ equals: ... }`
`ContentExpectations` 数组按顺序快速失败。数组项可以是直接 matcher也可以是 `{ json: {...} }``{ css: {...} }``{ xpath: {...} }` 提取器规则;一条规则不能混用直接 matcher 和 extractor多个 extractor 也不能共存。Extractor 未配置 matcher 时等价于 `exists: true`。对对象或数组源执行直接 `contains`/`regex` 时会先 JSON 序列化,`equals` 仍对原始结构做深度相等。
启动期语义校验统一由 `expect/validate.ts` 负责,校验空 matcher、未知字段、字段类型、`exists:false` 组合、ContentExpectations 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性、ReDoS 风险以及 HTTP/LLM headers 大小写归一化后重复 key。语义校验不修改 Raw 输入。旧字段 `match``maxDurationMs`、ICMP 的 `max*` 阈值字段不再支持。
启动期语义校验统一由 `expect/validate.ts` 负责,校验器通过兼容层同时支持 Raw 和 Normalized expect 形态(`validateRawKeyedExpectations` 遇到数组自动分派到 `validateNormalizedKeyedExpectations``validateRawContentExpectation` 遇到 `kind` 字段自动分派到 `validateNormalizedContentExpectation`)。校验内容包括空 matcher、未知字段、字段类型、`exists:false` 组合、ContentExpectations 互斥性、JSONPath 子集、XPath 可编译性、regex 可编译性、ReDoS 风险以及 HTTP/LLM headers 大小写归一化后重复 key。语义校验不修改输入。旧字段 `match``maxDurationMs`、ICMP 的 `max*` 阈值字段不再支持。
**快速失败顺序**