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:
@@ -7,7 +7,7 @@
|
||||
### Requirement: ValueMatcher 统一匹配器
|
||||
系统 SHALL 提供共享 `ValueMatcher` 作为所有非状态类 value expectation 的基础匹配结构。`ValueMatcher` SHALL 支持 `equals`、`contains`、`regex`、`exists`、`empty`、`gt`、`gte`、`lt` 和 `lte` 字段。`equals` MUST 支持任意 JSON value,并使用深度相等比较。`contains` 和 `regex` SHALL 将实际值转换为字符串后匹配。`gt`、`gte`、`lt` 和 `lte` SHALL 将实际值转换为有限数字后比较,无法转换为有限数字时 SHALL 判定不匹配。一个 `ValueMatcher` 对象包含多个 matcher 字段时,系统 SHALL 要求全部 matcher 均通过。
|
||||
|
||||
所有类型为 `RawValueExpectation` 的 expect 字段 SHALL 同时接受 primitive 原始值(string / number / boolean / null)作为简写形式。原始值简写 SHALL 等价于 `{ equals: value }`。系统 SHALL 在 resolve 阶段将 primitive 原始值归一化为 `{ equals: value }` 对象形式,运行期逻辑 SHALL 仅处理 `ValueMatcher` 对象形式。数组和对象 MUST NOT 作为原始值简写;需要对数组或对象执行 equals 匹配时,配置 MUST 显式写成 `{ equals: value }`。
|
||||
所有类型为 Authoring `RawValueExpectation` 的 expect 字段 SHALL 同时接受 primitive 原始值(string / number / boolean / null)作为简写形式。原始值简写 SHALL 等价于 `{ equals: value }`。系统 SHALL 在 Normalized 阶段将 primitive 原始值归一化为 `{ equals: value }` 对象形式。Normalized Config、checker 语义校验、checker.resolve() 和运行期逻辑 SHALL 仅处理 `ValueMatcher` 对象形式。数组和对象 MUST NOT 作为原始值简写;需要对数组或对象执行 equals 匹配时,配置 MUST 显式写成 `{ equals: value }`。
|
||||
|
||||
#### Scenario: equals 匹配对象
|
||||
- **WHEN** 实际值为 `{status: "ok", count: 1}` 且 matcher 为 `{equals: {status: "ok", count: 1}}`
|
||||
@@ -25,28 +25,28 @@
|
||||
- **WHEN** 实际值为 `"healthy"` 且 matcher 为 `{contains: "health", regex: "^ready$"}`
|
||||
- **THEN** 系统 SHALL 在任一 matcher 不满足时判定该 matcher 对象不通过
|
||||
|
||||
#### Scenario: 字符串原始值简写等价 equals
|
||||
- **WHEN** expect 字段配置为 `finishReason: "stop"` 且实际值为 `"stop"`
|
||||
- **THEN** 系统 SHALL 在 resolve 阶段将 `"stop"` 归一化为 `{equals: "stop"}` 并判定通过
|
||||
#### Scenario: 字符串原始值简写在 Normalized 阶段等价 equals
|
||||
- **WHEN** Authoring Config 中 expect 字段配置为 `finishReason: "stop"`
|
||||
- **THEN** Normalized Config SHALL 将 `"stop"` 归一化为 `{equals: "stop"}`,运行期实际值为 `"stop"` 时判定通过
|
||||
|
||||
#### Scenario: 数字原始值简写等价 equals
|
||||
- **WHEN** expect 字段配置为 `rowCount: 1` 且实际值为 `1`
|
||||
- **THEN** 系统 SHALL 在 resolve 阶段将 `1` 归一化为 `{equals: 1}` 并判定通过
|
||||
#### Scenario: 数字原始值简写在 Normalized 阶段等价 equals
|
||||
- **WHEN** Authoring Config 中 expect 字段配置为 `rowCount: 1`
|
||||
- **THEN** Normalized Config SHALL 将 `1` 归一化为 `{equals: 1}`,运行期实际值为 `1` 时判定通过
|
||||
|
||||
#### Scenario: 布尔原始值简写等价 equals
|
||||
- **WHEN** expect 字段配置为 RawValueExpectation 类型且值为 `true`,实际值为 `true`
|
||||
- **THEN** 系统 SHALL 在 resolve 阶段将 `true` 归一化为 `{equals: true}` 并判定通过
|
||||
#### Scenario: 布尔原始值简写在 Normalized 阶段等价 equals
|
||||
- **WHEN** Authoring Config 中 RawValueExpectation 类型字段值为 `true`
|
||||
- **THEN** Normalized Config SHALL 将 `true` 归一化为 `{equals: true}`,运行期实际值为 `true` 时判定通过
|
||||
|
||||
#### Scenario: null 原始值简写等价 equals
|
||||
- **WHEN** expect 字段配置为 RawValueExpectation 类型且值为 `null`,实际值为 `null`
|
||||
- **THEN** 系统 SHALL 在 resolve 阶段将 `null` 归一化为 `{equals: null}` 并判定通过
|
||||
#### Scenario: null 原始值简写在 Normalized 阶段等价 equals
|
||||
- **WHEN** Authoring Config 中 RawValueExpectation 类型字段值为 `null`
|
||||
- **THEN** Normalized Config SHALL 将 `null` 归一化为 `{equals: null}`,运行期实际值为 `null` 时判定通过
|
||||
|
||||
#### Scenario: 原始值简写不匹配
|
||||
- **WHEN** expect 字段配置为 `finishReason: "stop"` 且实际值为 `"error"`
|
||||
- **THEN** 系统 SHALL 判定不通过并生成 mismatch failure
|
||||
|
||||
### Requirement: ValueMatcher 启动期校验
|
||||
系统 SHALL 在启动期对所有 `RawValueExpectation` 字段执行严格的类型和语义校验。校验 SHALL 同时接受 primitive 原始值和 `ValueMatcher` 对象两种形式,但 MUST NOT 修改输入对象。当输入为 primitive 原始值时,系统 SHALL 视为合法配置(等价于 `{equals: value}`),无需进一步校验 matcher 字段。当输入为 `ValueMatcher` 对象时,`contains` MUST 为 string。`equals` MAY 为任意 JSON value。`exists` 和 `empty` MUST 为 boolean。`gt`、`gte`、`lt` 和 `lte` MUST 为有限数字(`Number.isFinite`)。`regex` MUST 为可编译的 string pattern,并通过 ReDoS 风险校验。`ValueMatcher` 对象 MUST 至少包含一个合法 matcher 字段,空对象 `{}` SHALL 导致启动期配置错误。`ValueMatcher` 对象 MUST NOT 包含未知字段,任何不属于 `equals`、`contains`、`regex`、`exists`、`empty`、`gt`、`gte`、`lt`、`lte` 的字段 SHALL 导致启动期配置错误。
|
||||
系统 SHALL 在启动期对所有 normalized `ValueMatcher` 字段执行严格的类型和语义校验。Authoring primitive 简写 MUST 在语义校验前由 Normalized 阶段转换为 `{equals: value}`,因此语义 validator SHALL 校验 `ValueMatcher` 对象而不是 Raw primitive 输入。当输入为 `ValueMatcher` 对象时,`contains` MUST 为 string。`equals` MAY 为任意 JSON value。`exists` 和 `empty` MUST 为 boolean。`gt`、`gte`、`lt` 和 `lte` MUST 为有限数字(`Number.isFinite`)。`regex` MUST 为可编译的 string pattern,并通过 ReDoS 风险校验。`ValueMatcher` 对象 MUST 至少包含一个合法 matcher 字段,空对象 `{}` SHALL 导致启动期配置错误。`ValueMatcher` 对象 MUST NOT 包含未知字段,任何不属于 `equals`、`contains`、`regex`、`exists`、`empty`、`gt`、`gte`、`lt`、`lte` 的字段 SHALL 导致启动期配置错误。
|
||||
|
||||
#### Scenario: 空 matcher 对象被拒绝
|
||||
- **WHEN** YAML 配置中任一 matcher 对象为空 `{}`
|
||||
@@ -64,17 +64,17 @@
|
||||
- **WHEN** YAML 配置中任一 matcher 的 `exists` 或 `empty` 值不是布尔类型
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该字段必须为布尔值
|
||||
|
||||
#### Scenario: 字符串原始值校验通过
|
||||
- **WHEN** YAML 配置中 RawValueExpectation 字段值为字符串 `"stop"`
|
||||
- **THEN** 系统 SHALL 接受该配置,但 SHALL NOT 在语义校验阶段修改输入对象
|
||||
#### Scenario: 字符串原始值在 Normalized schema 中被拒绝
|
||||
- **WHEN** Normalized schema 校验 RawValueExpectation 字段值为字符串 `"stop"`
|
||||
- **THEN** schema 校验 SHALL 失败,因为 Normalized Config 只接受 `ValueMatcher` 对象
|
||||
|
||||
#### Scenario: 数字原始值校验通过
|
||||
- **WHEN** YAML 配置中 RawValueExpectation 字段值为数字 `5000`
|
||||
- **THEN** 系统 SHALL 接受该配置,但 SHALL NOT 在语义校验阶段修改输入对象
|
||||
#### Scenario: 数字原始值在 Normalized schema 中被拒绝
|
||||
- **WHEN** Normalized schema 校验 RawValueExpectation 字段值为数字 `5000`
|
||||
- **THEN** schema 校验 SHALL 失败,因为 Normalized Config 只接受 `ValueMatcher` 对象
|
||||
|
||||
#### Scenario: null 原始值校验通过
|
||||
- **WHEN** YAML 配置中 RawValueExpectation 字段值为 `null`
|
||||
- **THEN** 系统 SHALL 接受该配置,但 SHALL NOT 在语义校验阶段修改输入对象
|
||||
#### Scenario: null 原始值在 Normalized schema 中被拒绝
|
||||
- **WHEN** Normalized schema 校验 RawValueExpectation 字段值为 `null`
|
||||
- **THEN** schema 校验 SHALL 失败,因为 Normalized Config 只接受 `ValueMatcher` 对象
|
||||
|
||||
#### Scenario: 数组原始值被拒绝
|
||||
- **WHEN** YAML 配置中 RawValueExpectation 字段值为数组 `[1, 2]`
|
||||
@@ -142,7 +142,7 @@
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示正则存在 ReDoS 风险
|
||||
|
||||
### Requirement: ContentExpectations 内容断言数组
|
||||
系统 SHALL 提供共享 `ContentExpectations` 表达返回内容断言。`ContentExpectations` MUST 为有序数组,数组项 SHALL 为直接 `ValueMatcher`,或 `json`、`css`、`xpath` 三类 extractor expectation 之一。系统 SHALL 在 resolve 阶段将 Raw content DSL 解析为带 `kind` 字段的 Resolved `ContentExpectation` 执行计划,并按数组顺序执行全部 expectation,任一 expectation 失败时 SHALL 立即停止并返回该 expectation 的 failure。系统 MUST NOT 支持内容字段的非数组对象快捷写法。
|
||||
系统 SHALL 提供共享 `ContentExpectations` 表达返回内容断言。Authoring `ContentExpectations` MUST 为有序数组,数组项 SHALL 为直接 `ValueMatcher`,或 `json`、`css`、`xpath` 三类 extractor expectation 之一。系统 SHALL 在 Normalized 阶段将 Authoring content DSL 解析为带 `kind` 字段的 Normalized `ContentExpectation` 执行计划,并按数组顺序执行全部 expectation,任一 expectation 失败时 SHALL 立即停止并返回该 expectation 的 failure。系统 MUST NOT 支持内容字段的非数组对象快捷写法。
|
||||
|
||||
#### Scenario: 直接 matcher 内容 expectation
|
||||
- **WHEN** 内容字段配置 `[{contains: "ready"}, {regex: "listening on \\d+"}]` 且原始内容同时满足两条 expectation
|
||||
@@ -156,9 +156,9 @@
|
||||
- **WHEN** YAML 中内容字段配置为 `{contains: "ok"}` 而不是数组
|
||||
- **THEN** 系统 SHALL 在启动期配置校验失败,提示该内容字段必须为 expectation 数组
|
||||
|
||||
#### Scenario: Resolved content expectation 使用 kind
|
||||
- **WHEN** Raw 内容字段包含直接 matcher、json extractor、css extractor 和 xpath extractor
|
||||
- **THEN** resolve 阶段 SHALL 分别生成 `kind="value"`、`kind="json"`、`kind="css"` 和 `kind="xpath"` 的 Resolved `ContentExpectation`
|
||||
#### Scenario: Normalized content expectation 使用 kind
|
||||
- **WHEN** Authoring 内容字段包含直接 matcher、json extractor、css extractor 和 xpath extractor
|
||||
- **THEN** Normalized 阶段 SHALL 分别生成 `kind="value"`、`kind="json"`、`kind="css"` 和 `kind="xpath"` 的 `ContentExpectation`
|
||||
|
||||
### Requirement: ContentExpectation 互斥性约束
|
||||
一条 Raw `ContentExpectation` MUST 为直接 `ValueMatcher` 或恰好一个 extractor(`json`、`css`、`xpath` 之一)。系统 MUST NOT 允许同一条 Raw expectation 同时包含多个 extractor。直接 `ValueMatcher` expectation MUST NOT 包含 `json`、`css`、`xpath` 字段。系统 SHALL 在启动期对违反互斥性的 Raw expectation 报错。
|
||||
@@ -190,7 +190,7 @@
|
||||
- **THEN** 系统 SHALL 直接在结构化值上使用深度相等比较
|
||||
|
||||
### Requirement: ContentExpectations 提取器
|
||||
系统 SHALL 支持在 `ContentExpectations` 中使用 `json`、`css` 和 `xpath` extractor。`json.path` MUST 使用现有 JSONPath 子集。`css.selector` MUST 为非空字符串,并 MAY 配置 `attr` 提取属性值。`xpath.path` MUST 为非空字符串,并 SHALL 在启动期进行可编译校验。Extractor 内部 MAY 包含任意 `ValueMatcher` 字段。Extractor expectation 未配置任何 matcher 时,resolve 阶段 SHALL 将其 Resolved matcher 物化为 `{ exists: true }`。
|
||||
系统 SHALL 支持在 Authoring `ContentExpectations` 中使用 `json`、`css` 和 `xpath` extractor。`json.path` MUST 使用现有 JSONPath 子集。`css.selector` MUST 为非空字符串,并 MAY 配置 `attr` 提取属性值。`xpath.path` MUST 为非空字符串,并 SHALL 在启动期进行可编译校验。Extractor 内部 MAY 包含任意 `ValueMatcher` 字段。Extractor expectation 未配置任何 matcher 时,Normalized 阶段 SHALL 将其 matcher 物化为 `{ exists: true }`。
|
||||
|
||||
#### Scenario: json extractor 数字比较
|
||||
- **WHEN** 原始内容为 JSON 字符串 `{"count": 2}` 且 expectation 为 `{json: {path: "$.count", gte: 1}}`
|
||||
@@ -198,22 +198,22 @@
|
||||
|
||||
#### Scenario: json extractor 存在性默认语义
|
||||
- **WHEN** 原始内容为 JSON 字符串 `{"user": {"id": null}}` 且 expectation 为 `{json: {path: "$.user.id"}}`
|
||||
- **THEN** resolve 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}` 并在运行期判定通过
|
||||
- **THEN** Normalized 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}` 并在运行期判定通过
|
||||
|
||||
#### Scenario: css attr 存在性默认语义
|
||||
- **WHEN** 原始内容包含 `<meta name="status" content="ok">` 且 expectation 为 `{css: {selector: "meta[name=status]", attr: "content"}}`
|
||||
- **THEN** resolve 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}` 并在属性存在时判定通过
|
||||
- **THEN** Normalized 阶段 SHALL 将该 expectation 的 matcher 物化为 `{exists: true}` 并在属性存在时判定通过
|
||||
|
||||
#### Scenario: xpath 无匹配节点失败
|
||||
- **WHEN** XML 内容中不存在 XPath 指向的节点,且 expectation 为 `{xpath: {path: "/root/status"}}`
|
||||
- **THEN** 系统 SHALL 判定该 expectation 不通过并生成 phase 对应内容字段的 mismatch failure
|
||||
|
||||
### Requirement: KeyedExpectations 键控断言数组
|
||||
系统 SHALL 提供共享 `KeyedExpectations` 表达键值型观测值断言。Raw `KeyedExpectations` SHALL 为动态键对象,每个键对应的值 MUST 为 `RawValueExpectation`;数组和对象简写 MUST 被拒绝,数组或对象 equals 匹配 MUST 显式写为 `{equals: <value>}`。Resolved `KeyedExpectations` SHALL 为有序数组,每个元素包含原始 key 和已归一化的 `ValueExpectation` matcher。调用方 MAY 指定 key 规范化策略;HTTP 与 LLM headers MUST 使用大小写不敏感的 key 匹配,并 MUST 在启动期校验或 resolve 阶段拒绝归一化后重复的 header key。DB rows 不做 key 归一化,按查询结果列名大小写敏感匹配。
|
||||
系统 SHALL 提供共享 `KeyedExpectations` 表达键值型观测值断言。Authoring `KeyedExpectations` SHALL 为动态键对象,每个键对应的值 MUST 为 Authoring `RawValueExpectation`;数组和对象简写 MUST 被拒绝,数组或对象 equals 匹配 MUST 显式写为 `{equals: <value>}`。Normalized `KeyedExpectations` SHALL 为有序数组,每个元素包含原始 key 和已归一化的 `ValueExpectation` matcher。调用方 MAY 指定 key 规范化策略;HTTP 与 LLM headers MUST 使用大小写不敏感的 key 匹配,并 MUST 在启动期校验或 Normalized 阶段拒绝归一化后重复的 header key。DB rows 不做 key 归一化,按查询结果列名大小写敏感匹配。
|
||||
|
||||
#### Scenario: headers 字面量快捷写法
|
||||
- **WHEN** 响应 headers 中 `content-type` 为 `application/json`,且配置为 `headers: {Content-Type: "application/json"}`
|
||||
- **THEN** resolve 阶段 SHALL 将该项解析为 keyed expectation `{key: "Content-Type", matcher: {equals: "application/json"}}`,运行期按大小写不敏感 key 匹配并判定通过
|
||||
- **THEN** Normalized 阶段 SHALL 将该项解析为 keyed expectation `{key: "Content-Type", matcher: {equals: "application/json"}}`,运行期按大小写不敏感 key 匹配并判定通过
|
||||
|
||||
#### Scenario: headers matcher 写法
|
||||
- **WHEN** 响应 headers 中 `content-type` 为 `application/json; charset=utf-8`,且配置为 `headers: {Content-Type: {contains: "application/json"}}`
|
||||
|
||||
Reference in New Issue
Block a user