1
0
Files
DiAL/openspec/specs/expect-rule-system/spec.md
lanyuanxiaoyao cf847ccd7a 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
2026-05-22 14:00:47 +08:00

19 KiB
Raw Blame History

Purpose

定义共享 expect 断言规则系统的核心概念和基础设施ValueMatcher 统一匹配器、ContentExpectations 内容断言数组、KeyedExpectations 键控断言数组、以及相关的启动期校验和失败路径规范。

Requirements

Requirement: ValueMatcher 统一匹配器

系统 SHALL 提供共享 ValueMatcher 作为所有非状态类 value expectation 的基础匹配结构。ValueMatcher SHALL 支持 equalscontainsregexexistsemptygtgteltlte 字段。equals MUST 支持任意 JSON value并使用深度相等比较。containsregex SHALL 将实际值转换为字符串后匹配。gtgteltlte SHALL 将实际值转换为有限数字后比较,无法转换为有限数字时 SHALL 判定不匹配。一个 ValueMatcher 对象包含多个 matcher 字段时,系统 SHALL 要求全部 matcher 均通过。

所有类型为 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}}
  • THEN 系统 SHALL 使用深度相等判定该 matcher 通过

Scenario: contains 字符串化匹配

  • WHEN 实际值为 "service ready" 且 matcher 为 {contains: "ready"}
  • THEN 系统 SHALL 判定该 matcher 通过

Scenario: 数字范围组合匹配

  • WHEN 实际值为 50 且 matcher 为 {gte: 0, lte: 100}
  • THEN 系统 SHALL 判定该 matcher 通过

Scenario: 多 matcher 快速失败

  • WHEN 实际值为 "healthy" 且 matcher 为 {contains: "health", regex: "^ready$"}
  • THEN 系统 SHALL 在任一 matcher 不满足时判定该 matcher 对象不通过

Scenario: 字符串原始值简写在 Normalized 阶段等价 equals

  • WHEN Authoring Config 中 expect 字段配置为 finishReason: "stop"
  • THEN Normalized Config SHALL 将 "stop" 归一化为 {equals: "stop"},运行期实际值为 "stop" 时判定通过

Scenario: 数字原始值简写在 Normalized 阶段等价 equals

  • WHEN Authoring Config 中 expect 字段配置为 rowCount: 1
  • THEN Normalized Config SHALL 将 1 归一化为 {equals: 1},运行期实际值为 1 时判定通过

Scenario: 布尔原始值简写在 Normalized 阶段等价 equals

  • WHEN Authoring Config 中 RawValueExpectation 类型字段值为 true
  • THEN Normalized Config SHALL 将 true 归一化为 {equals: true},运行期实际值为 true 时判定通过

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 在启动期对所有 normalized ValueMatcher 字段执行严格的类型和语义校验。Authoring primitive 简写 MUST 在语义校验前由 Normalized 阶段转换为 {equals: value},因此语义 validator SHALL 校验 ValueMatcher 对象而不是 Raw primitive 输入。当输入为 ValueMatcher 对象时,contains MUST 为 string。equals MAY 为任意 JSON value。existsempty MUST 为 boolean。gtgteltlte MUST 为有限数字(Number.isFinite)。regex MUST 为可编译的 string pattern并通过 ReDoS 风险校验。ValueMatcher 对象 MUST 至少包含一个合法 matcher 字段,空对象 {} SHALL 导致启动期配置错误。ValueMatcher 对象 MUST NOT 包含未知字段,任何不属于 equalscontainsregexexistsemptygtgteltlte 的字段 SHALL 导致启动期配置错误。

Scenario: 空 matcher 对象被拒绝

  • WHEN YAML 配置中任一 matcher 对象为空 {}
  • THEN 系统 SHALL 在启动期配置校验失败,提示 matcher 必须包含至少一个合法字段

Scenario: 未知 matcher 字段被拒绝

  • WHEN YAML 配置中任一 matcher 对象包含 foo: "bar" 等未知字段
  • THEN 系统 SHALL 在启动期配置校验失败,提示该字段未知

Scenario: 数值 matcher 非有限数字被拒绝

  • WHEN YAML 配置中任一 matcher 的 gtgteltlte 值为 NaNInfinity 或非数字类型
  • THEN 系统 SHALL 在启动期配置校验失败,提示数值 matcher 必须为有限数字

Scenario: 布尔 matcher 非布尔值被拒绝

  • WHEN YAML 配置中任一 matcher 的 existsempty 值不是布尔类型
  • THEN 系统 SHALL 在启动期配置校验失败,提示该字段必须为布尔值

Scenario: 字符串原始值在 Normalized schema 中被拒绝

  • WHEN Normalized schema 校验 RawValueExpectation 字段值为字符串 "stop"
  • THEN schema 校验 SHALL 失败,因为 Normalized Config 只接受 ValueMatcher 对象

Scenario: 数字原始值在 Normalized schema 中被拒绝

  • WHEN Normalized schema 校验 RawValueExpectation 字段值为数字 5000
  • THEN schema 校验 SHALL 失败,因为 Normalized Config 只接受 ValueMatcher 对象

Scenario: null 原始值在 Normalized schema 中被拒绝

  • WHEN Normalized schema 校验 RawValueExpectation 字段值为 null
  • THEN schema 校验 SHALL 失败,因为 Normalized Config 只接受 ValueMatcher 对象

Scenario: 数组原始值被拒绝

  • WHEN YAML 配置中 RawValueExpectation 字段值为数组 [1, 2]
  • THEN 系统 SHALL 在启动期配置校验失败,提示必须为 primitive 原始值或 matcher 对象;如需数组 equals 匹配应写成 {equals: [1, 2]}

Scenario: 对象原始值必须显式 equals

  • WHEN YAML 配置中 RawValueExpectation 字段值为对象 {foo: "bar"},且 foo 不是合法 matcher 字段
  • THEN 系统 SHALL 在启动期配置校验失败,提示 foo 是未知 matcher如需对象 equals 匹配应写成 {equals: {foo: "bar"}}

Requirement: empty matcher 语义

empty: true SHALL 在以下情况判定通过:实际值为 nullundefined、空字符串 ""、空数组 [] 或空对象 {}empty: false SHALL 在以上条件均不满足时判定通过。数字 0 和布尔 false SHALL NOT 被视为 empty。

Scenario: null 视为 empty

  • WHEN 实际值为 null 且 matcher 为 {empty: true}
  • THEN 系统 SHALL 判定该 matcher 通过

Scenario: 空字符串视为 empty

  • WHEN 实际值为 "" 且 matcher 为 {empty: true}
  • THEN 系统 SHALL 判定该 matcher 通过

Scenario: 空数组视为 empty

  • WHEN 实际值为 [] 且 matcher 为 {empty: true}
  • THEN 系统 SHALL 判定该 matcher 通过

Scenario: 空对象视为 empty

  • WHEN 实际值为 {} 且 matcher 为 {empty: true}
  • THEN 系统 SHALL 判定该 matcher 通过

Scenario: 数字 0 不视为 empty

  • WHEN 实际值为 0 且 matcher 为 {empty: true}
  • THEN 系统 SHALL 判定该 matcher 不通过

Scenario: 布尔 false 不视为 empty

  • WHEN 实际值为 false 且 matcher 为 {empty: true}
  • THEN 系统 SHALL 判定该 matcher 不通过

Requirement: exists 与其他 matcher 的组合语义

ValueMatcher 同时包含 exists: false 和其他非存在性 matchercontainsregexequals 等)时,系统 SHALL 在启动期配置校验失败,提示 exists: false 不能与其他 matcher 组合使用。exists: true MAY 与其他 matcher 组合,语义为先确认存在再执行其他 matcher。

Scenario: exists false 与 contains 组合被拒绝

  • WHEN YAML 配置中 matcher 为 {exists: false, contains: "foo"}
  • THEN 系统 SHALL 在启动期配置校验失败,提示 exists: false 不能与其他 matcher 组合

Scenario: exists true 与 contains 组合允许

  • WHEN 实际值为 "hello foo" 且 matcher 为 {exists: true, contains: "foo"}
  • THEN 系统 SHALL 判定该 matcher 通过

Requirement: regex 字段语义

系统 SHALL 使用 regex 作为唯一正则 matcher 字段。regex 值 MUST 为可编译的字符串 pattern。运行期 SHALL 固定使用无 flags 的 new RegExp(pattern).test(String(actual)) 执行匹配。系统 MUST NOT 支持旧 match 字段。系统 SHALL 在启动期对所有 regex pattern 执行可编译校验和 ReDoS 风险校验。

Scenario: regex 任意位置匹配

  • WHEN 实际值为 "api status ok" 且 matcher 为 {regex: "status"}
  • THEN 系统 SHALL 判定该 matcher 通过,因为无 flags 的 JavaScript 正则仍会搜索整个字符串中的第一次匹配

Scenario: regex 完整匹配由用户声明锚点

  • WHEN 实际值为 "OK\n" 且 matcher 为 {regex: "^OK$"}
  • THEN 系统 SHALL 判定该 matcher 不通过,因为系统 MUST NOT 默认启用 multiline flags

Scenario: match 字段启动失败

  • WHEN YAML 配置中任一 matcher 对象包含 match: "ok"
  • THEN 系统 SHALL 在启动期配置校验失败,提示 match 是未知字段或不支持字段

Scenario: regex ReDoS 风险启动失败

  • WHEN YAML 配置中任一 regex"(a+)+$"
  • THEN 系统 SHALL 在启动期配置校验失败,提示正则存在 ReDoS 风险

Requirement: ContentExpectations 内容断言数组

系统 SHALL 提供共享 ContentExpectations 表达返回内容断言。Authoring ContentExpectations MUST 为有序数组,数组项 SHALL 为直接 ValueMatcher,或 jsoncssxpath 三类 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
  • THEN 系统 SHALL 判定该内容字段通过

Scenario: 内容 expectation 数组快速失败

  • WHEN 内容字段配置三条 expectation 且第二条 expectation 失败
  • THEN 系统 SHALL 返回第二条 expectation 的 failure并 MUST NOT 执行第三条 expectation

Scenario: 内容字段必须为数组

  • WHEN YAML 中内容字段配置为 {contains: "ok"} 而不是数组
  • THEN 系统 SHALL 在启动期配置校验失败,提示该内容字段必须为 expectation 数组

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 或恰好一个 extractorjsoncssxpath 之一)。系统 MUST NOT 允许同一条 Raw expectation 同时包含多个 extractor。直接 ValueMatcher expectation MUST NOT 包含 jsoncssxpath 字段。系统 SHALL 在启动期对违反互斥性的 Raw expectation 报错。

Scenario: 多 extractor 被拒绝

  • WHEN YAML 中内容 expectation 为 {json: {path: "$.a"}, css: {selector: "div"}}
  • THEN 系统 SHALL 在启动期配置校验失败,提示一条 expectation 不能同时包含多个 extractor

Scenario: 直接 matcher 混入 extractor 被拒绝

  • WHEN YAML 中内容 expectation 为 {contains: "ok", json: {path: "$.a"}}
  • THEN 系统 SHALL 在启动期配置校验失败,提示直接 matcher 不能与 extractor 混用

Requirement: 空 ContentExpectations 数组语义

ContentExpectations 空数组 [] SHALL 被系统接受为合法配置。运行期空数组 SHALL 等价于无内容 expectation即该内容字段的断言直接通过。

Scenario: 空 body 数组通过

  • WHEN HTTP target 配置 expect.body: [] 且响应体为任意内容
  • THEN 系统 SHALL 判定 body 阶段通过

Requirement: ContentExpectations 非字符串值序列化

ContentExpectations 的观测源为非字符串值(如对象或数组)时,直接 ValueMatchercontainsregex SHALL 先将值 JSON 序列化为字符串后匹配。equals SHALL 直接在原始结构化值上使用深度相等比较,不进行序列化。

Scenario: 对象序列化后 contains 匹配

  • WHEN ContentExpectations 观测源为 {status: "ok"} 且 expectation 为 {contains: "ok"}
  • THEN 系统 SHALL 将对象 JSON 序列化后执行 contains 匹配

Scenario: 对象 equals 不序列化

  • WHEN ContentExpectations 观测源为 {status: "ok"} 且 expectation 为 {equals: {status: "ok"}}
  • THEN 系统 SHALL 直接在结构化值上使用深度相等比较

Requirement: ContentExpectations 提取器

系统 SHALL 支持在 Authoring ContentExpectations 中使用 jsoncssxpath 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}}
  • THEN 系统 SHALL 解析 JSON、提取 $.count 并判定该 expectation 通过

Scenario: json extractor 存在性默认语义

  • WHEN 原始内容为 JSON 字符串 {"user": {"id": null}} 且 expectation 为 {json: {path: "$.user.id"}}
  • 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 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 表达键值型观测值断言。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-typeapplication/json,且配置为 headers: {Content-Type: "application/json"}
  • THEN Normalized 阶段 SHALL 将该项解析为 keyed expectation {key: "Content-Type", matcher: {equals: "application/json"}},运行期按大小写不敏感 key 匹配并判定通过

Scenario: headers matcher 写法

  • WHEN 响应 headers 中 content-typeapplication/json; charset=utf-8,且配置为 headers: {Content-Type: {contains: "application/json"}}
  • THEN 系统 SHALL 判定该 header expectation 通过

Scenario: 缺失键 exists false

  • WHEN 观测键值表中不存在 x-debug,且配置为 {x-debug: {exists: false}}
  • THEN 系统 SHALL 判定该 keyed expectation 通过

Scenario: keyed 对象值必须显式 equals

  • WHEN Raw keyed expectation 的某个值是对象 {foo: "bar"} 且未写在 equals
  • THEN 系统 SHALL 在启动期配置校验失败,提示对象 equals 必须显式写成 {equals: {foo: "bar"}}

Scenario: keyed 数组值必须显式 equals

  • WHEN Raw keyed expectation 的某个值是数组 ["a"] 且未写在 equals
  • THEN 系统 SHALL 在启动期配置校验失败,提示数组 equals 必须显式写成 {equals: ["a"]}

Scenario: header 归一化重复 key 被拒绝

  • WHEN HTTP 或 LLM expect.headers 同时配置 Content-Typecontent-type
  • THEN 系统 SHALL 在启动期配置校验失败,提示 header key 归一化后重复

Requirement: 结构化失败路径

系统 SHALL 在共享 matcher、content 和 keyed expectation 断言失败时生成结构化 CheckFailure。failure SHALL 包含 kindphasepathmessage,并在 mismatch 场景包含 expectedactual。内容 expectation failure path SHALL 包含数组下标keyed expectation failure path SHALL 包含键名extractor failure path SHALL 包含 extractor 类型和 path/selector 信息。failure.expected SHOULD 使用用户可理解的 matcher 或 expectation 片段MUST NOT 直接暴露 Resolved kind 执行计划;单字段 equals 包装 SHOULD 展示为原始 expected 值。

Scenario: ContentExpectations 失败路径

  • WHEN expect.body[1].json expectation 失败
  • THEN failure.path SHALL 指向 body[1].json($.path) 或等价可定位路径failure.phase SHALL 为 body

Scenario: KeyedExpectations 失败路径

  • WHEN expect.headers.Content-Type 不匹配
  • THEN failure.path SHALL 指向 headers.Content-Typefailure.phase SHALL 为 headers

Scenario: actual 截断

  • WHEN matcher 失败时 actual 字符串长度超过 200 字符
  • THEN 系统 SHALL 使用现有截断策略保存 failure.actual避免历史记录写入过长内容