1
0
Files
DiAL/openspec/specs/expect-rule-system/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

18 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 均通过。

所有类型为 RawValueExpectation 的 expect 字段 SHALL 同时接受 primitive 原始值string / number / boolean / null作为简写形式。原始值简写 SHALL 等价于 { equals: value }。系统 SHALL 在 resolve 阶段将 primitive 原始值归一化为 { equals: value } 对象形式,运行期逻辑 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: 字符串原始值简写等价 equals

  • WHEN expect 字段配置为 finishReason: "stop" 且实际值为 "stop"
  • THEN 系统 SHALL 在 resolve 阶段将 "stop" 归一化为 {equals: "stop"} 并判定通过

Scenario: 数字原始值简写等价 equals

  • WHEN expect 字段配置为 rowCount: 1 且实际值为 1
  • THEN 系统 SHALL 在 resolve 阶段将 1 归一化为 {equals: 1} 并判定通过

Scenario: 布尔原始值简写等价 equals

  • WHEN expect 字段配置为 RawValueExpectation 类型且值为 true,实际值为 true
  • THEN 系统 SHALL 在 resolve 阶段将 true 归一化为 {equals: true} 并判定通过

Scenario: null 原始值简写等价 equals

  • WHEN expect 字段配置为 RawValueExpectation 类型且值为 null,实际值为 null
  • THEN 系统 SHALL 在 resolve 阶段将 null 归一化为 {equals: 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。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: 字符串原始值校验通过

  • WHEN YAML 配置中 RawValueExpectation 字段值为字符串 "stop"
  • THEN 系统 SHALL 接受该配置,但 SHALL NOT 在语义校验阶段修改输入对象

Scenario: 数字原始值校验通过

  • WHEN YAML 配置中 RawValueExpectation 字段值为数字 5000
  • THEN 系统 SHALL 接受该配置,但 SHALL NOT 在语义校验阶段修改输入对象

Scenario: null 原始值校验通过

  • WHEN YAML 配置中 RawValueExpectation 字段值为 null
  • THEN 系统 SHALL 接受该配置,但 SHALL NOT 在语义校验阶段修改输入对象

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 表达返回内容断言。ContentExpectations MUST 为有序数组,数组项 SHALL 为直接 ValueMatcher,或 jsoncssxpath 三类 extractor expectation 之一。系统 SHALL 在 resolve 阶段将 Raw content DSL 解析为带 kind 字段的 Resolved 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: 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

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 支持在 ContentExpectations 中使用 jsoncssxpath 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 }

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 resolve 阶段 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} 并在属性存在时判定通过

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 归一化,按查询结果列名大小写敏感匹配。

Scenario: headers 字面量快捷写法

  • WHEN 响应 headers 中 content-typeapplication/json,且配置为 headers: {Content-Type: "application/json"}
  • THEN resolve 阶段 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避免历史记录写入过长内容