## Purpose

定义拨测工具的 YAML 配置文件格式、解析校验规则和 CLI 启动流程。

## Requirements

### Requirement: 旧配置入口拒绝
系统 SHALL 拒绝旧版顶层 `runtime` 和顶层 `logging` 配置入口。系统 SHALL 拒绝旧版 `server.host`、`server.port` 和 `server.dataDir` 入口，并要求使用 `server.listen` 与 `server.storage` 下的新路径。

#### Scenario: 顶层 runtime 被拒绝
- **WHEN** 配置文件声明顶层 `runtime`
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `runtime`

#### Scenario: 顶层 logging 被拒绝
- **WHEN** 配置文件声明顶层 `logging`
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段 `logging`

#### Scenario: server 旧监听字段被拒绝
- **WHEN** 配置文件声明 `server.host` 或 `server.port`
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段

#### Scenario: server 旧 dataDir 字段被拒绝
- **WHEN** 配置文件声明 `server.dataDir`
- **THEN** 系统 SHALL 以配置错误退出并提示 `server` 中存在未知字段 `dataDir`

### Requirement: YAML 配置文件格式
系统 SHALL 支持通过 YAML 配置文件定义全部运行参数，包括 server 配置、probes 执行配置、可选的 variables 段、checker 默认值和 typed target 列表（含可选 group 字段）。server 配置 SHALL 将 HTTP 监听参数放在 `server.listen` 分组，将本地数据目录和历史数据保留时长放在 `server.storage` 分组，将运行时日志配置放在 `server.logging` 分组。拨测全局执行策略 SHALL 放在 `probes.execution` 分组。target MUST 使用 `id` 字段作为唯一标识符，MUST 使用 `type` 字段声明 checker 类型，SHALL 支持可选的 `name` 字段作为展示名称元信息，SHALL 支持可选的 `description` 字段作为目标说明。`name` 和 `description` 均 SHALL 允许省略或显式配置为 `null`；省略或显式 null 时解析结果 SHALL 保留为 null。HTTP 领域字段 MUST 放在 `http` 分组，cmd 领域字段 MUST 放在 `cmd` 分组，db 领域字段 MUST 放在 `db` 分组，tcp 领域字段 MUST 放在 `tcp` 分组，icmp 领域字段 MUST 放在 `icmp` 分组，udp 领域字段 MUST 放在 `udp` 分组，LLM 领域字段 MUST 放在 `llm` 分组。HTTP target 的 `http` 分组 SHALL 支持可选的 `ignoreSSL`（布尔值）和 `maxRedirects`（非负整数）字段。Db target 的 `db` 分组 SHALL 支持 `url`（必填）和 `query`（可选）字段。Tcp target 的 `tcp` 分组 SHALL 支持 `host`（必填）、`port`（必填）、`readBanner`（可选）、`bannerReadTimeout`（可选）和 `maxBannerBytes`（可选）字段。Icmp target 的 `icmp` 分组 SHALL 支持 `host`（必填）、`count`（可选，默认 3）和 `packetSize`（可选，默认 56）字段。Udp target 的 `udp` 分组 SHALL 支持 `host`（必填）、`port`（必填）、`payload`（可选，默认空字符串）、`encoding`（可选，默认 `text`）、`responseEncoding`（可选，默认 `text`）和 `maxResponseBytes`（可选，默认 4096）字段。LLM target 的 `llm` 分组 SHALL 支持 `provider`（必填）、`url`（必填）、`model`（必填）、`prompt`（必填）、`mode`（可选，默认 `http`）、`key`（可选，默认空字符串）、`authToken`（可选）、`headers`（可选）、`ignoreSSL`（可选，默认 `false`）、`options`（可选）和 `providerOptions`（可选）字段。

`defaults.http` 分组 SHALL 仅支持 `headers`（可选）和 `maxBodyBytes`（可选）字段。`defaults.http` 分组 MUST NOT 支持 `method` 字段。`defaults.tcp` 分组 SHALL 仅支持 `bannerReadTimeout`（可选）和 `maxBannerBytes`（可选）字段。`defaults.icmp` 分组 SHALL 仅支持空对象。`defaults.udp` 分组 SHALL 仅支持 `encoding`（可选）、`responseEncoding`（可选）和 `maxResponseBytes`（可选）字段。`defaults.llm` 分组 SHALL 仅支持 `mode`（可选）、`headers`（可选）、`ignoreSSL`（可选）、`options`（可选）和 `providerOptions`（可选）字段。

#### Scenario: 完整配置文件解析
- **WHEN** 系统启动并读取包含 server.listen、server.storage、server.logging、probes.execution、variables、defaults、targets（含 id、group 字段）的 YAML 配置文件
- **THEN** 系统 SHALL 正确解析所有字段并用于初始化服务、调度引擎和对应 checker runner

#### Scenario: 最简 HTTP 配置文件解析
- **WHEN** 系统读取只包含一个 `type: http` target（含 `id` 和 `http.url`）的 YAML 配置文件（省略 server、probes、variables、defaults 和 expect）
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段（host=127.0.0.1, port=3000, dataDir=./data, interval=30s, timeout=10s, probes.execution.maxConcurrentChecks=20, server.storage.retention=7d, http.method=GET, http.maxBodyBytes=100MB, http.ignoreSSL=false, http.maxRedirects=0, group="default"），并保留 name=null、description=null

#### Scenario: 最简 cmd 配置文件解析
- **WHEN** 系统读取只包含一个 `type: cmd` target（含 `id` 和 `cmd.exec`）的 YAML 配置文件
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段（interval=30s, timeout=10s, cmd.cwd 为配置文件所在目录, cmd.maxOutputBytes=100MB），并保留 name=null、description=null

#### Scenario: per-target 配置覆盖全局默认值
- **WHEN** 某个 target 指定 interval、timeout 或对应领域分组中的默认字段
- **THEN** 该 target SHALL 使用其自身的值，不受 defaults 中对应字段影响

#### Scenario: HTTP target 配置 ignoreSSL
- **WHEN** YAML 配置中 HTTP target 设置 `http.ignoreSSL: true`
- **THEN** 系统 SHALL 解析该字段并在执行时跳过 SSL 证书校验

#### Scenario: HTTP target 配置 maxRedirects
- **WHEN** YAML 配置中 HTTP target 设置 `http.maxRedirects: 5`
- **THEN** 系统 SHALL 解析该字段并在执行时允许最多跟随 5 次重定向

#### Scenario: 最简 db 配置文件解析
- **WHEN** 系统读取只包含一个 `type: db` target（含 `id` 和 `db.url`）的 YAML 配置文件
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段（interval=30s, timeout=10s, group="default"），并保留 name=null、description=null

#### Scenario: 最简 tcp 配置文件解析
- **WHEN** 系统读取只包含一个 `type: tcp` target（含 `id`、`tcp.host` 和 `tcp.port`）的 YAML 配置文件
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段（interval=30s, timeout=10s, group="default", tcp.readBanner=false, tcp.bannerReadTimeout=2000, tcp.maxBannerBytes=4096），并保留 name=null、description=null

#### Scenario: defaults.tcp 配置 banner 默认值
- **WHEN** YAML 配置中 defaults.tcp 设置 `bannerReadTimeout` 和 `maxBannerBytes`
- **THEN** 未显式覆盖对应字段的 tcp target SHALL 使用 defaults.tcp 中的值

#### Scenario: defaults.http.method 触发校验错误
- **WHEN** 配置文件中出现 `defaults.http.method` 字段
- **THEN** 系统 SHALL 以配置错误退出，提示 defaults.http 中存在未知字段 method

#### Scenario: per-target http.method 仍然有效
- **WHEN** HTTP target 配置 `http.method: POST`
- **THEN** 系统 SHALL 使用 POST 作为该 target 的请求方法

#### Scenario: 未配置 http.method 使用内置默认值
- **WHEN** HTTP target 未配置 `http.method` 且 defaults.http 中无 method 字段
- **THEN** 系统 SHALL 使用内置默认值 GET 作为该 target 的请求方法

#### Scenario: 最简 icmp 配置文件解析
- **WHEN** 系统读取只包含一个 `type: icmp` target（含 `id` 和 `icmp.host`）的 YAML 配置文件
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段（interval=30s, timeout=10s, group="default", icmp.count=3, icmp.packetSize=56），并保留 name=null、description=null

#### Scenario: 最简 udp 配置文件解析
- **WHEN** 系统读取只包含一个 `type: udp` target（含 `id`、`udp.host` 和 `udp.port`）的 YAML 配置文件
- **THEN** 系统 SHALL 使用内置默认值填充未指定的字段（interval=30s, timeout=10s, group="default", udp.payload="", udp.encoding="text", udp.responseEncoding="text", udp.maxResponseBytes=4096），并保留 name=null、description=null

#### Scenario: defaults.udp 配置默认值
- **WHEN** YAML 配置中 defaults.udp 设置 `encoding`、`responseEncoding` 和 `maxResponseBytes`
- **THEN** 未显式覆盖对应字段的 udp target SHALL 使用 defaults.udp 中的值

#### Scenario: 最简 llm 配置文件解析
- **WHEN** 系统读取只包含一个 `type: llm` target（含 `id`、`llm.provider`、`llm.url`、`llm.model` 和 `llm.prompt`）的 YAML 配置文件
- **THEN** 系统 SHALL 使用内置默认值填充未指定字段（interval=30s, timeout=10s, group="default", llm.mode="http", llm.key="", llm.ignoreSSL=false, llm.options.maxOutputTokens=16, llm.options.temperature=0），并保留 name=null、description=null

#### Scenario: defaults.llm 配置默认值
- **WHEN** YAML 配置中 defaults.llm 设置 `mode`、`headers`、`ignoreSSL`、`options` 或 `providerOptions`
- **THEN** 未显式覆盖对应字段的 llm target SHALL 使用 defaults.llm 中的值

### Requirement: CLI 参数
系统 SHALL 通过单一命令行参数接受 YAML 配置文件路径。

#### Scenario: 指定配置文件启动
- **WHEN** 用户执行 `./dial-server ./probes.yaml`
- **THEN** 系统 SHALL 读取并解析指定路径的 YAML 文件作为配置

#### Scenario: 未提供配置文件路径
- **WHEN** 用户启动程序时未提供任何命令行参数
- **THEN** 系统 SHALL 以错误退出并提示需要指定配置文件路径

#### Scenario: 配置文件不存在
- **WHEN** 用户指定的配置文件路径不存在
- **THEN** 系统 SHALL 以错误退出并提示文件不存在

### Requirement: 配置校验
系统 SHALL 在启动时对 YAML 配置进行完整校验，校验失败时以非零状态退出并输出清晰的错误信息。系统 SHALL 使用 TypeBox 定义配置契约和 raw config TypeScript 类型，由 Ajv 校验 TypeBox 生成的 JSON Schema，再执行启动期语义 validator。配置加载流程 SHALL 明确区分 `RawProbeConfig`、`ValidatedProbeConfig`、`ResolvedConfig` 三段生命周期，并在 YAML 解析之后、AJV 校验之前执行变量替换阶段。JSON Schema 契约 SHALL 覆盖业务无关的结构规则，包括字段类型、必填字段、枚举、数组与对象形状、数值范围和未知字段。语义 validator SHALL 覆盖契约不适合表达的业务规则，包括 target id 唯一性、id 命名规则校验、checker type 注册状态、时长和大小解析、HTTP URL、正则可编译、JSONPath 子集和 XPath 可编译。

契约校验和语义 validator SHALL 统一产出 `ConfigValidationIssue`，最终由配置加载流程统一渲染为中文错误信息。

系统 SHALL 导出完整 `probe-config.schema.json`，该文件 SHALL 与运行期 TypeBox fragments 生成的 JSON Schema 保持一致，用于用户配置引用和编辑器提示。

除 `headers`、`env`、`variables` 等明确声明为动态键值表的对象外，配置中的未知字段 SHALL 导致启动期配置错误。系统 MUST NOT 静默忽略未知字段。

所有 `RawValueExpectation` 类型的 expect 字段 SHALL 在 JSON Schema 契约中声明为 `anyOf: [primitiveValue, matcherObject]` 联合类型，同时接受 primitive 原始值（string / number / boolean / null）和 `ValueMatcher` 对象。语义 validator SHALL 在不修改输入的前提下校验 `RawValueExpectation`；primitive 原始值 SHALL 被视为合法 Raw 输入，并在 checker resolve 阶段转换为 `{equals: value}` 对象形式。数组和对象 MUST NOT 作为 `RawValueExpectation` 原始值简写；需要对数组或对象执行 equals 匹配时，配置 MUST 显式写成 `{equals: value}`。`RawKeyedExpectations` 的动态键值 schema SHALL 复用 `RawValueExpectation`，MUST NOT 通过额外 JSON value 分支接受数组或对象简写。

#### Scenario: target 缺少必填字段
- **WHEN** YAML 中某个 target 缺少 id 或 type 字段
- **THEN** 系统 SHALL 以错误退出，提示哪个 target 缺少哪个字段

#### Scenario: HTTP target 缺少 url
- **WHEN** YAML 中某个 target 配置 `type: http` 但缺少 `http.url`
- **THEN** 系统 SHALL 以错误退出，提示该 target 缺少 http.url 字段

#### Scenario: cmd target 缺少 exec
- **WHEN** YAML 中某个 target 配置 `type: cmd` 但缺少 `cmd.exec`
- **THEN** 系统 SHALL 以错误退出，提示该 target 缺少 cmd.exec 字段

#### Scenario: target type 非法
- **WHEN** YAML 中某个 target 的 type 不是已注册 checker 类型
- **THEN** 系统 SHALL 以错误退出，提示不支持的 target type 和当前支持的 type 列表

#### Scenario: target id 重复
- **WHEN** YAML 中存在两个 id 相同的 target
- **THEN** 系统 SHALL 以错误退出，提示重复的 id

#### Scenario: target id 不合法
- **WHEN** YAML 中某个 target 的 id 不符合 `[a-zA-Z0-9][a-zA-Z0-9_-]*` 规则
- **THEN** 系统 SHALL 以错误退出，提示 id 命名不合法

#### Scenario: group 字段类型校验
- **WHEN** YAML 中某个 target 的 `group` 字段不是字符串
- **THEN** 系统 SHALL 以错误退出并提示 group 字段类型错误

#### Scenario: interval 格式非法
- **WHEN** interval 或 timeout 值不是有效的时长格式（如 `30s`、`5m`、`500ms`）
- **THEN** 系统 SHALL 以错误退出并提示格式错误

#### Scenario: maxConcurrentChecks 非法
- **WHEN** probes.execution.maxConcurrentChecks 不是正整数
- **THEN** 系统 SHALL 以错误退出并提示 probes.execution.maxConcurrentChecks 格式错误

#### Scenario: interval 或 timeout 解析结果非法
- **WHEN** interval 或 timeout 解析结果不是正整数毫秒（如 `0ms` 或 `1.5ms`）
- **THEN** 系统 SHALL 以错误退出并提示必须为正整数毫秒

#### Scenario: size 格式非法
- **WHEN** maxBodyBytes 或 maxOutputBytes 值不是有效的 size 格式
- **THEN** 系统 SHALL 以错误退出并提示支持 B、KB、MB、GB 格式

#### Scenario: size 解析结果非法
- **WHEN** maxBodyBytes 或 maxOutputBytes 解析结果不是非负安全整数字节数（如 `1.5B`）
- **THEN** 系统 SHALL 以错误退出并提示必须为非负安全整数字节数

#### Scenario: HTTP method 非法
- **WHEN** YAML 中某个 HTTP target 的 `http.method` 不是 GET、HEAD、POST、PUT、PATCH、DELETE、OPTIONS 之一
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 method 不合法

#### Scenario: HTTP method 小写非法
- **WHEN** YAML 中某个 HTTP target 配置 `http.method: get`
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 method 必须为大写枚举值

#### Scenario: URL 格式非法
- **WHEN** YAML 中某个 HTTP target 的 `http.url` 不是合法 URL，或协议不是 `http:` / `https:`
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 URL 格式不合法

#### Scenario: maxRedirects 非法
- **WHEN** YAML 中某个 HTTP target 的 `http.maxRedirects` 为负数
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 maxRedirects 必须为非负整数

#### Scenario: maxRedirects 非整数非法
- **WHEN** YAML 中某个 HTTP target 的 `http.maxRedirects` 不是非负整数（如 `1.5` 或 `"5"`）
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 maxRedirects 必须为非负整数

#### Scenario: ignoreSSL 类型非法
- **WHEN** YAML 中某个 HTTP target 的 `http.ignoreSSL` 不是布尔值
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 ignoreSSL 必须为布尔值

#### Scenario: HTTP headers 类型非法
- **WHEN** YAML 中某个 HTTP target 的 `http.headers` 不是对象，或任一 header 值不是字符串
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 http.headers 格式错误

#### Scenario: HTTP body 类型非法
- **WHEN** YAML 中某个 HTTP target 的 `http.body` 已配置但不是字符串
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 http.body 必须为字符串

#### Scenario: maxBodyBytes 数字非法
- **WHEN** YAML 中某个 HTTP target 的 `http.maxBodyBytes` 或 defaults.http.maxBodyBytes 是负数、非整数或非安全整数
- **THEN** 系统 SHALL 以错误退出，提示 maxBodyBytes 必须为非负安全整数字节数或合法 size 字符串

#### Scenario: status 模式非法
- **WHEN** YAML 中某个 HTTP target 的 `expect.status` 包含不符合 `1xx` 到 `5xx` 格式的字符串（如 `"abc"`、`"2x"`、`"6xx"`）
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 status 模式不合法

#### Scenario: status 数字非法
- **WHEN** YAML 中某个 HTTP target 的 `expect.status` 包含非整数或不在 100-599 范围内的数字
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 status 数字不合法

#### Scenario: durationMs matcher 非法
- **WHEN** YAML 中某个 target 的 `expect.durationMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 expect.durationMs 格式错误

#### Scenario: durationMs 原始值简写合法
- **WHEN** YAML 中某个 target 配置 `expect.durationMs: 5000`
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 修改输入；checker resolve 阶段 SHALL 将其解析为 `{equals: 5000}`

#### Scenario: ValueMatcher 字段字符串简写合法
- **WHEN** YAML 中某个 target 配置 `expect.finishReason: "stop"`
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 修改输入；checker resolve 阶段 SHALL 将其解析为 `{equals: "stop"}`

#### Scenario: ValueMatcher 字段 null 简写合法
- **WHEN** YAML 中某个 target 配置 ValueMatcher 字段值为 `null`
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 修改输入；checker resolve 阶段 SHALL 将其解析为 `{equals: null}`

#### Scenario: ValueMatcher 字段数组简写非法
- **WHEN** YAML 中某个 target 配置 ValueMatcher 字段值为数组 `[1, 2]`
- **THEN** 系统 SHALL 以错误退出，提示该字段必须为 primitive 原始值或 matcher 对象；如需数组 equals 匹配应写成 `{equals: [1, 2]}`

#### Scenario: ValueMatcher 字段对象简写非法
- **WHEN** YAML 中某个 target 配置 ValueMatcher 字段值为对象 `{foo: "bar"}`，且 `foo` 不是合法 matcher 字段
- **THEN** 系统 SHALL 以错误退出，提示 `foo` 是未知 matcher；如需对象 equals 匹配应写成 `{equals: {foo: "bar"}}`

#### Scenario: icmp target 缺少 host
- **WHEN** YAML 中某个 target 配置 `type: icmp` 但缺少 `icmp.host`
- **THEN** 系统 SHALL 以错误退出，提示该 target 缺少 icmp.host 字段

#### Scenario: icmp expect 未知字段
- **WHEN** YAML 中 icmp target 的 expect 包含非 icmp expect 字段
- **THEN** 系统 SHALL 以配置错误退出，提示 expect 包含未知字段

#### Scenario: HTTP expect headers 非法
- **WHEN** YAML 中某个 HTTP target 的 `expect.headers` 不是对象，或某个 header 期望不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 expect.headers 格式错误

#### Scenario: HTTP expect body 必须为数组
- **WHEN** YAML 中某个 HTTP target 的 `expect.body` 已配置但不是数组
- **THEN** 系统 SHALL 以错误退出，提示该 target 的 expect.body 必须为数组

#### Scenario: HTTP body expectation 缺少支持字段
- **WHEN** YAML 中某个 HTTP target 的 `expect.body` 数组项未包含 contains、regex、json、css、xpath 任一支持字段
- **THEN** 系统 SHALL 以错误退出，提示该 body expectation 缺少支持的 expectation 类型

#### Scenario: HTTP body expectation 同时配置多个支持字段
- **WHEN** YAML 中某个 HTTP target 的同一条 body expectation 同时包含 contains、regex、json、css、xpath 中的多个支持字段
- **THEN** 系统 SHALL 以错误退出，提示每条 body expectation 只能配置一种 expectation 类型

#### Scenario: HTTP body regex 非法
- **WHEN** YAML 中某个 HTTP target 的 body regex expectation 不是字符串或不是可编译正则表达式
- **THEN** 系统 SHALL 以错误退出，提示该 body regex 不合法

#### Scenario: HTTP body json path 非法
- **WHEN** YAML 中某个 HTTP target 的 body json expectation 缺少 path，或 path 不符合系统支持的 JSONPath 子集
- **THEN** 系统 SHALL 以错误退出，提示该 body json path 不合法

#### Scenario: HTTP body css selector 非法
- **WHEN** YAML 中某个 HTTP target 的 body css expectation 缺少 selector，或 selector 不是非空字符串
- **THEN** 系统 SHALL 以错误退出，提示该 body css selector 不合法

#### Scenario: HTTP body xpath path 非法
- **WHEN** YAML 中某个 HTTP target 的 body xpath expectation 缺少 path，或 path 不是非空字符串，或可被现有 XPath 库静态判定为语法错误
- **THEN** 系统 SHALL 以错误退出，提示该 body xpath path 不合法

#### Scenario: expect matcher 类型非法
- **WHEN** YAML 中某个 expect matcher 的 regex 不是可编译正则字符串，empty/exists 不是布尔值，或 gt/gte/lt/lte 不是有限数字
- **THEN** 系统 SHALL 以错误退出，提示对应 matcher 配置不合法

#### Scenario: expect match 字段不再支持
- **WHEN** YAML 中某个 expect matcher 配置 `match` 字段
- **THEN** 系统 SHALL 以错误退出，提示 `match` 是未知字段，请使用 `regex`

#### Scenario: unknown 字段失败
- **WHEN** YAML 中任一结构化配置对象包含契约未声明的字段，且该对象不是明确允许动态键的对象
- **THEN** 系统 SHALL 以错误退出，提示未知字段所在路径

#### Scenario: 动态 headers 字段允许
- **WHEN** YAML 中 `http.headers`、`defaults.http.headers` 或 `expect.headers` 包含任意 header 名称，且对应值符合契约
- **THEN** 系统 SHALL 接受这些动态 header 名称

#### Scenario: 动态 env 字段允许
- **WHEN** YAML 中 `cmd.env` 包含任意环境变量名称，且对应值为字符串
- **THEN** 系统 SHALL 接受这些动态 env 名称

#### Scenario: JSON Schema 不修改输入
- **WHEN** 系统执行 JSON Schema 契约校验
- **THEN** 系统 MUST NOT 通过契约校验器强制转换类型、注入默认值或删除未知字段

#### Scenario: 配置生命周期分离
- **WHEN** 系统加载配置文件
- **THEN** 系统 SHALL 按 `unknown -> 变量替换 -> RawProbeConfig -> ValidatedProbeConfig -> ResolvedConfig` 的顺序执行变量替换、契约校验、语义校验和运行期配置解析

#### Scenario: 结构化校验 issue
- **WHEN** 契约校验、语义 validator 或变量替换阶段发现非法配置
- **THEN** 系统 SHALL 先生成包含 code、path、message 和可选 targetName 的结构化 `ConfigValidationIssue`，再统一渲染为中文错误信息

#### Scenario: 导出配置 JSON Schema
- **WHEN** 仓库生成或检查配置契约
- **THEN** 根目录 SHALL 存在 draft-07 `probe-config.schema.json`，且其内容 SHALL 与当前公共 fragments 和已注册 checker fragments 组装出的完整 schema 一致（包含 variables 段和 target 的 id/name 字段）。所有 `RawValueExpectation` 字段的 schema SHALL 声明为 `anyOf: [primitiveValue, matcherObject]` 联合类型，`RawKeyedExpectations` 的 dynamic value schema SHALL 复用 `RawValueExpectation`

#### Scenario: JSON Schema RawValueExpectation 接受原始值
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为数字 `5000`
- **THEN** JSON Schema 校验 SHALL 通过，因为 RawValueExpectation schema 声明为 `anyOf: [primitiveValue, matcherObject]`

#### Scenario: JSON Schema RawValueExpectation 接受 matcher 对象
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为 `{lte: 5000}`
- **THEN** JSON Schema 校验 SHALL 通过

#### Scenario: JSON Schema RawValueExpectation 拒绝数组原始值
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为数组 `[1, 2]`
- **THEN** JSON Schema 校验 SHALL 失败，因为数组不属于 primitive 原始值或 matcher 对象

#### Scenario: JSON Schema RawValueExpectation 接受 equals 数组对象
- **WHEN** 使用 JSON Schema 校验配置文件中 RawValueExpectation 字段值为 `{equals: [1, 2]}` 或 `{equals: {status: "ok"}}`
- **THEN** JSON Schema 校验 SHALL 通过，因为 `equals` 支持任意 JSON value

#### Scenario: JSON Schema RawKeyedExpectations 拒绝数组对象简写
- **WHEN** 使用 JSON Schema 校验配置文件中 `expect.headers.X` 或 DB row 列值为数组 `["a"]` 或对象 `{nested: "ok"}`
- **THEN** JSON Schema 校验 SHALL 失败，除非该值显式写在 `{equals: ...}` 下
系统 SHALL 支持使用单位字符串配置读取上限，单位包括 `B`、`KB`、`MB` 和 `GB`。

#### Scenario: 解析 MB
- **WHEN** YAML 中配置 `maxBodyBytes: "100MB"`
- **THEN** 系统 SHALL 将其解析为 104857600 bytes

#### Scenario: 解析 KB
- **WHEN** YAML 中配置 `maxOutputBytes: "512KB"`
- **THEN** 系统 SHALL 将其解析为 524288 bytes

### Requirement: runtime 并发配置
系统 SHALL 支持 `probes.execution.maxConcurrentChecks` 配置全局最大并发检查数。`probes` 和 `probes.execution` 配置段均 SHALL 为可选，省略时使用默认值。

#### Scenario: 使用默认并发限制
- **WHEN** YAML 中未配置 `probes` 或未配置 `probes.execution.maxConcurrentChecks`
- **THEN** 系统 SHALL 使用默认值 20

#### Scenario: 配置并发限制
- **WHEN** YAML 中配置 `probes.execution.maxConcurrentChecks: 5`
- **THEN** 系统 SHALL 将全局最大并发检查数设置为 5

### Requirement: YAML 配置使用 Bun 内置解析
系统 SHALL 使用 Bun 内置的 `Bun.YAML.parse()` 解析配置文件，不引入外部 YAML 解析库。

#### Scenario: 解析 YAML 内容
- **WHEN** 系统读取 YAML 文件内容
- **THEN** 系统 SHALL 调用 `Bun.YAML.parse()` 将内容解析为配置对象

### Requirement: expect 配置增强
系统 SHALL 支持 typed target 的领域专用 expect 配置，并通过共享 `ValueMatcher`、`ContentExpectations` 和 `KeyedExpectations` 表达可复用断言能力。状态类字段 SHALL 保持枚举或布尔语义，包括 HTTP/LLM 的 `status`（支持精确数字和范围模式）、cmd 的 `exitCode`、tcp 的 `connected`、icmp 的 `alive` 和 udp 的 `responded`。value 类指标字段 SHALL 使用 `RawValueExpectation` 输入，并在 resolve 阶段归一化为运行期 `ValueExpectation`，包括通用 `durationMs`、db 的 `rowCount`、udp 的 `responseSize`/`sourceHost`/`sourcePort`、icmp 的 `packetLossPercent`/`avgLatencyMs`/`maxLatencyMs`、llm 的 usage token 与 stream 首 token 耗时。内容类字段 MUST 使用 `RawContentExpectations` 数组表达配置顺序，包括 HTTP `body`、cmd `stdout`/`stderr`、tcp `banner`、udp `response`、llm `output` 和 db `result`。LLM `finishReason` 和 `rawFinishReason` SHALL 使用 `RawValueExpectation`（非 ContentExpectations），因为它们是单值字符串元数据。键值类字段 SHALL 使用 `RawKeyedExpectations`，包括 HTTP/LLM `headers` 和 db `rows` 中的列值断言（db `rows` 的类型为 `Array<RawKeyedExpectations>`，外层数组按行索引，内层每个元素表达该行的列值断言）。

配置加载流程 SHALL 保留变量替换后的 Raw expect 作为用户配置快照，同时生成 Resolved expect 作为运行期执行计划。语义校验 SHALL 只读取 Raw expect 并报告问题，MUST NOT 原地归一化或修改 Raw expect。Store 持久化 SHALL 写入 Raw expect；checker execute SHALL 只消费 Resolved expect。

#### Scenario: 解析 HTTP expect 配置
- **WHEN** YAML 配置文件中 HTTP target 的 expect 包含 status、headers、body expectation 数组和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw HTTP expect 快照，并生成包含默认 status、resolved keyed headers、resolved content body 和 resolved durationMs 的 HTTP Resolved expect

#### Scenario: 解析 cmd expect 配置
- **WHEN** YAML 配置文件中 cmd target 的 expect 包含 exitCode、stdout、stderr 和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw cmd expect 快照，并生成包含默认 exitCode、resolved stdout/stderr content expectations 和 resolved durationMs 的 cmd Resolved expect

#### Scenario: 解析 db expect 配置
- **WHEN** YAML 配置文件中 db target 的 expect 包含 durationMs、rowCount、rows 和 result
- **THEN** 系统 SHALL 保留 Raw db expect 快照，并生成包含 resolved rowCount、rows keyed expectations 和 result content expectations 的 db Resolved expect

#### Scenario: 解析 tcp expect 配置
- **WHEN** YAML 配置文件中 tcp target 的 expect 包含 connected、banner expectation 数组和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw tcp expect 快照，并生成包含默认 connected、resolved banner content expectations 和 resolved durationMs 的 tcp Resolved expect

#### Scenario: 解析 icmp expect 配置
- **WHEN** YAML 配置文件中 icmp target 的 expect 包含 alive、packetLossPercent、avgLatencyMs、maxLatencyMs 和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw icmp expect 快照，并生成包含默认 alive 和 resolved 数值 expectations 的 icmp Resolved expect

#### Scenario: 解析 udp expect 配置
- **WHEN** YAML 配置文件中 udp target 的 expect 包含 responded、response、responseSize、sourceHost、sourcePort 和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw udp expect 快照，并生成包含默认 responded、resolved response content expectations 和 resolved value expectations 的 udp Resolved expect

#### Scenario: 解析 llm expect 配置
- **WHEN** YAML 配置文件中 llm target 的 expect 包含 status、headers、output、finishReason、rawFinishReason、usage、stream 和 durationMs matcher
- **THEN** 系统 SHALL 保留 Raw llm expect 快照，并生成包含默认 status、resolved headers、output、finishReason、rawFinishReason、usage、stream 和 durationMs 的 llm Resolved expect，并保留 output 内容 expectation 数组顺序

#### Scenario: 解析有序 ContentExpectations 数组
- **WHEN** YAML 中任一内容类 expect 配置 contains、json、regex 三个数组项
- **THEN** 系统 SHALL 在 Raw expect 中保留数组顺序，并在 Resolved expect 中保留执行顺序，供执行阶段按配置顺序快速失败

#### Scenario: 不配置 HTTP status
- **WHEN** HTTP target 未配置 `expect.status`
- **THEN** 系统 SHALL 在 Resolved HTTP expect 中物化默认 `status: [200]` 语义

#### Scenario: 配置 HTTP status 范围模式
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`
- **THEN** 系统 SHALL 在执行 expect 时匹配所有 200-299 状态码

#### Scenario: 不配置 cmd exitCode
- **WHEN** cmd target 未配置 `expect.exitCode`
- **THEN** 系统 SHALL 在 Resolved cmd expect 中物化默认 `exitCode: [0]` 语义

#### Scenario: 不配置 expect
- **WHEN** target 未配置任何 expect 规则
- **THEN** 系统 SHALL 正常处理，Raw expect 快照为 undefined，Resolved expect 由各 checker 物化自身默认状态语义

#### Scenario: Raw expect 不被语义校验修改
- **WHEN** YAML 中配置 `expect.durationMs: 1000`
- **THEN** 语义校验 SHALL 接受该 Raw primitive 简写且 MUST NOT 将 Raw 输入原地修改为 `{equals: 1000}`

#### Scenario: 旧 maxDurationMs 字段不再支持
- **WHEN** YAML 中任一 target 配置 `expect.maxDurationMs`
- **THEN** 系统 SHALL 在启动期配置校验失败，提示该字段未知，并要求使用 `expect.durationMs`

#### Scenario: 旧 match 字段不再支持
- **WHEN** YAML 中任一 matcher 或内容 expectation 配置 `match`
- **THEN** 系统 SHALL 在启动期配置校验失败，提示该字段未知或不支持，并要求使用 `regex`

#### Scenario: durationMs matcher 配置
- **WHEN** YAML 中任一 target 配置 `expect.durationMs: {lte: 1000}`
- **THEN** 系统 SHALL 接受该配置并在运行期使用完整执行耗时进行 matcher 校验

#### Scenario: 动态 headers 字段允许
- **WHEN** YAML 中 `http.headers`、`defaults.http.headers`、`llm.headers`、`defaults.llm.headers` 或 `expect.headers` 包含任意 header 名称，且对应值符合契约
- **THEN** 系统 SHALL 接受这些动态 header 名称

#### Scenario: ContentExpectations 字段必须为数组
- **WHEN** YAML 中任一内容类 expect 字段配置为非数组
- **THEN** 系统 SHALL 在启动期配置校验失败，提示该字段必须为 expectation 数组

#### Scenario: regex 字段非法
- **WHEN** YAML 中任一 `regex` 不是字符串、不是可编译正则或存在 ReDoS 风险
- **THEN** 系统 SHALL 在启动期配置校验失败，提示对应 regex 不合法

#### Scenario: Store 持久化 Raw expect
- **WHEN** 系统同步已解析 target 到 targets 表
- **THEN** `targets.expect` SHALL 存储变量替换后的 Raw expect JSON，而不是包含 `kind` 或 resolved matcher 的运行期执行计划

### Requirement: 数据保留配置字段
配置 schema 的 `server.storage` 段 SHALL 支持 `retention` 字段，类型为字符串，格式为 `<数字><单位>`（单位：`d` 天、`h` 小时、`m` 分钟），用于指定历史数据保留时长。

#### Scenario: retention 字段校验通过
- **WHEN** 配置文件中 `server.storage.retention` 为合法格式（如 `"7d"`、`"24h"`、`"30m"`）
- **THEN** 配置校验 SHALL 通过

#### Scenario: retention 字段格式非法
- **WHEN** 配置文件中 `server.storage.retention` 为非法格式（如 `"abc"`、`"7x"`、`""`）
- **THEN** 配置校验 SHALL 失败并报告格式错误

#### Scenario: retention 字段缺省
- **WHEN** 配置文件中未指定 `server.storage.retention`
- **THEN** 系统 SHALL 使用默认值 `"7d"`

### Requirement: 数据目录路径解析
配置加载流程 SHALL 将 `server.storage.dataDir` 相对路径基于配置文件所在目录（configDir）解析为绝对路径。绝对路径 SHALL 保持不变。

#### Scenario: dataDir 为相对路径
- **WHEN** 配置文件位于 `/opt/dial/probes.yaml`，且 `server.storage.dataDir` 配置为 `./data`
- **THEN** 系统 SHALL 将 dataDir 解析为 `/opt/dial/data`，而非依赖进程 cwd

#### Scenario: dataDir 为绝对路径
- **WHEN** `server.storage.dataDir` 配置为 `/var/lib/dial/data`
- **THEN** 系统 SHALL 直接使用该绝对路径，不做额外解析

#### Scenario: dataDir 使用默认值
- **WHEN** 未配置 `server.storage.dataDir`（使用默认值 `./data`）
- **THEN** 系统 SHALL 将默认值 `./data` 基于 configDir 解析为绝对路径

### Requirement: target 通用元信息字段约束
系统 SHALL 在 YAML target 通用字段中支持 `description` 字段，并对 `id`、`name` 和 `description` 执行契约校验。`id` MUST 为 1 到 30 个字符。`name` MUST 为 null 或 1 到 30 个字符的字符串，且语义校验 SHALL 拒绝仅包含空白字符的 name。`description` MUST 为 null 或不超过 500 个字符的字符串，且 MAY 为空字符串。

#### Scenario: description 字段解析
- **WHEN** 系统读取包含 `description: "检查生产 API 健康状态"` 的 target
- **THEN** 系统 SHALL 将该字段解析为 target 的目标说明

#### Scenario: name 为 null 通过校验
- **WHEN** 系统读取包含 `name: null` 或省略 `name` 的 target
- **THEN** 系统 SHALL 接受该配置

#### Scenario: name 仅包含空白字符报错
- **WHEN** 系统读取包含 `name: "   "` 的 target
- **THEN** 系统 SHALL 以配置错误退出，提示 name 不能为空白

#### Scenario: description 为空字符串
- **WHEN** 系统读取包含 `description: ""` 的 target
- **THEN** 系统 SHALL 接受该配置，且不触发长度错误

#### Scenario: description 为 null 通过校验
- **WHEN** 系统读取包含 `description: null` 或省略 `description` 的 target
- **THEN** 系统 SHALL 接受该配置

#### Scenario: description 类型非法
- **WHEN** YAML 中某个 target 的 `description` 字段不是字符串也不是 null
- **THEN** 系统 SHALL 以错误退出，提示 description 字段类型错误

#### Scenario: description 超过最大长度
- **WHEN** YAML 中某个 target 的 `description` 字段超过 500 个字符
- **THEN** 系统 SHALL 以错误退出，提示 description 字段长度错误

#### Scenario: id 超过最大长度
- **WHEN** YAML 中某个 target 的 `id` 字段超过 30 个字符
- **THEN** 系统 SHALL 以错误退出，提示 id 字段长度错误

#### Scenario: name 超过最大长度
- **WHEN** YAML 中某个 target 的 `name` 字段超过 30 个字符
- **THEN** 系统 SHALL 以错误退出，提示 name 字段长度错误

#### Scenario: 变量替换后 description 超长
- **WHEN** target 的 `description` 通过变量替换后超过 500 个字符
- **THEN** 系统 SHALL 在契约校验阶段以错误退出，提示 description 字段长度错误

### Requirement: 配置 schema 导出包含 target 元信息约束
系统 SHALL 在导出的 `probe-config.schema.json` 中包含 target `id`、`name` 和 `description` 的长度约束和可空类型，用于编辑器提示和外部校验。

#### Scenario: schema 导出 description
- **WHEN** 系统导出 `probe-config.schema.json`
- **THEN** target schema SHALL 包含可选的 `description` 字段，类型为 string 或 null，字符串最大长度为 500

#### Scenario: schema 导出 id 和 name
- **WHEN** 系统导出 `probe-config.schema.json`
- **THEN** target schema SHALL 声明 `id` 的 minLength 为 1、maxLength 为 30，并声明 `name` 为可选字段，类型为 string 或 null，字符串的 minLength 为 1、maxLength 为 30

### Requirement: TCP 配置校验
系统 SHALL 在启动期对 tcp checker 的配置契约和语义执行严格校验。Tcp target 的 `tcp` 分组 SHALL 只允许 `host`、`port`、`readBanner`、`bannerReadTimeout` 和 `maxBannerBytes` 字段；Tcp expect SHALL 只允许 `connected`、`durationMs` 和 `banner` 字段。`banner` MUST 为 `RawContentExpectations` 数组，`durationMs` SHALL 为 `RawValueExpectation`。未知字段、非法类型、非法端口、非法 size、非法 ContentExpectations 和不可编译正则 MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw tcp expect 输入。

#### Scenario: tcp host 类型非法
- **WHEN** YAML 中 tcp target 的 `tcp.host` 不是非空字符串
- **THEN** 系统 SHALL 以配置错误退出，提示 tcp.host 必须为非空字符串

#### Scenario: tcp port 类型非法
- **WHEN** YAML 中 tcp target 的 `tcp.port` 不是整数
- **THEN** 系统 SHALL 以配置错误退出，提示 tcp.port 必须为整数端口

#### Scenario: tcp readBanner 类型非法
- **WHEN** YAML 中 tcp target 的 `tcp.readBanner` 不是布尔值
- **THEN** 系统 SHALL 以配置错误退出，提示 tcp.readBanner 必须为布尔值

#### Scenario: tcp bannerReadTimeout 非法
- **WHEN** YAML 中 tcp target 或 defaults.tcp 的 `bannerReadTimeout` 不是非负有限数字
- **THEN** 系统 SHALL 以配置错误退出，提示 bannerReadTimeout 格式错误

#### Scenario: tcp maxBannerBytes 非法
- **WHEN** YAML 中 tcp target 或 defaults.tcp 的 `maxBannerBytes` 不是合法 size 值
- **THEN** 系统 SHALL 以配置错误退出，提示 maxBannerBytes 格式错误

#### Scenario: tcp expect connected 类型非法
- **WHEN** YAML 中 tcp target 的 `expect.connected` 不是布尔值
- **THEN** 系统 SHALL 以配置错误退出，提示 expect.connected 必须为布尔值

#### Scenario: tcp expect banner 非法
- **WHEN** YAML 中 tcp target 的 `expect.banner` 不是合法 `RawContentExpectations` 数组
- **THEN** 系统 SHALL 以配置错误退出，提示 expect.banner 格式错误

#### Scenario: tcp expect banner regex 正则非法
- **WHEN** YAML 中 tcp target 配置 `expect.banner: [{ regex: "[invalid" }]`
- **THEN** 系统 SHALL 在启动期配置校验失败，而不是延迟到运行期抛错

#### Scenario: tcp 分组未知字段失败
- **WHEN** YAML 中 tcp target 的 `tcp` 分组包含 `tls: true` 等未知字段
- **THEN** 系统 SHALL 以配置错误退出，提示 tcp 分组包含未知字段

#### Scenario: defaults.tcp 未知字段失败
- **WHEN** YAML 中 defaults.tcp 包含 `host` 或其他非默认字段
- **THEN** 系统 SHALL 以配置错误退出，提示 defaults.tcp 包含未知字段

### Requirement: LLM 配置校验
系统 SHALL 在启动期对 llm checker 的配置契约和语义执行严格校验。LLM target 的 `llm` 分组 SHALL 只允许 `provider`、`url`、`model`、`prompt`、`mode`、`key`、`authToken`、`headers`、`ignoreSSL`、`options` 和 `providerOptions` 字段。`defaults.llm` 分组 SHALL 只允许 `mode`、`headers`、`ignoreSSL`、`options` 和 `providerOptions` 字段。LLM expect SHALL 只允许 `status`、`headers`、`output`、`finishReason`、`rawFinishReason`、`usage`、`stream` 和 `durationMs` 字段。`expect.output` MUST 为 `RawContentExpectations` 数组。`expect.finishReason`、`expect.rawFinishReason`、`expect.usage.*`、`expect.stream.firstTokenMs` 和 `expect.durationMs` SHALL 使用 `RawValueExpectation`。未知字段、非法 provider、非法 URL、非法 mode、非法认证组合、非法 options、非法 output expectation 和 `mode: http` 下配置 `expect.stream` MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw llm expect 输入。

#### Scenario: llm provider 非法
- **WHEN** YAML 中 llm target 的 `llm.provider` 不是 `openai`、`openai-responses` 或 `anthropic`
- **THEN** 系统 SHALL 以配置错误退出，提示 llm.provider 不合法

#### Scenario: llm url 非法
- **WHEN** YAML 中 llm target 的 `llm.url` 不是 `http://` 或 `https://` URL
- **THEN** 系统 SHALL 以配置错误退出，提示 llm.url 格式不合法

#### Scenario: llm model 为空
- **WHEN** YAML 中 llm target 的 `llm.model` 不是非空字符串
- **THEN** 系统 SHALL 以配置错误退出，提示 llm.model 必须为非空字符串

#### Scenario: llm prompt 为空
- **WHEN** YAML 中 llm target 的 `llm.prompt` 不是非空字符串
- **THEN** 系统 SHALL 以配置错误退出，提示 llm.prompt 必须为非空字符串

#### Scenario: llm mode 非法
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `mode` 不是 `http` 或 `stream`
- **THEN** 系统 SHALL 以配置错误退出，提示 llm.mode 不合法

#### Scenario: llm headers 类型非法
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `headers` 不是对象，或任一 header 值不是字符串
- **THEN** 系统 SHALL 以配置错误退出，提示 llm.headers 格式错误

#### Scenario: llm ignoreSSL 类型非法
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `ignoreSSL` 不是布尔值
- **THEN** 系统 SHALL 以配置错误退出，提示 llm.ignoreSSL 必须为布尔值

#### Scenario: llm authToken provider 非法
- **WHEN** YAML 中 `provider: openai` 或 `provider: openai-responses` 的 llm target 配置 `authToken`
- **THEN** 系统 SHALL 以配置错误退出，提示 authToken 仅支持 anthropic provider

#### Scenario: Anthropic key 与 authToken 冲突
- **WHEN** YAML 中 `provider: anthropic` 的 llm target 同时配置非空 `key` 和非空 `authToken`
- **THEN** 系统 SHALL 以配置错误退出，提示 key 与 authToken 不能同时配置

#### Scenario: llm options 非法
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `options.maxOutputTokens` 不是正整数，`options.temperature`/`topP`/`topK`/`presencePenalty`/`frequencyPenalty`/`seed` 类型不合法，或 `options.stopSequences` 不是字符串数组
- **THEN** 系统 SHALL 以配置错误退出，提示 llm.options 格式错误

#### Scenario: llm providerOptions 非法
- **WHEN** YAML 中 llm target 或 defaults.llm 的 `providerOptions` 不是 JSON object
- **THEN** 系统 SHALL 以配置错误退出，提示 llm.providerOptions 格式错误

#### Scenario: llm 禁止字段失败
- **WHEN** YAML 中 llm target 配置 `api`、`providerName`、`baseURL`、`apiKey`、`messages`、`maxRetries`、`request`、`maxBodyBytes` 或 `maxStreamBytes`
- **THEN** 系统 SHALL 以配置错误退出，提示 llm 分组包含未知字段

#### Scenario: llm output expectation 缺少支持字段
- **WHEN** YAML 中 llm target 的 `expect.output` 数组项未包含任何合法 ValueMatcher 字段或 extractor
- **THEN** 系统 SHALL 以配置错误退出，提示 output expectation 缺少支持的 expectation 类型

#### Scenario: llm output expectation 同时配置多个 extractor
- **WHEN** YAML 中 llm target 的同一条 output expectation 同时包含 json、css、xpath 中的多个 extractor
- **THEN** 系统 SHALL 以配置错误退出，提示每条 output expectation 只能配置一种 extractor

#### Scenario: llm output regex 非法
- **WHEN** YAML 中 llm target 的 output regex expectation 不是字符串、不是可编译正则表达式或存在 ReDoS 风险
- **THEN** 系统 SHALL 以配置错误退出，提示该 output regex 不合法

#### Scenario: llm output json path 非法
- **WHEN** YAML 中 llm target 的 output json expectation 缺少 path，或 path 不符合系统支持的 JSONPath 子集
- **THEN** 系统 SHALL 以配置错误退出，提示该 output json path 不合法

#### Scenario: llm expect usage 非法
- **WHEN** YAML 中 llm target 的 `expect.usage.inputTokens`、`expect.usage.outputTokens` 或 `expect.usage.totalTokens` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出，提示 expect.usage 格式错误

#### Scenario: llm expect stream 仅允许 stream mode
- **WHEN** YAML 中 llm target 配置 `llm.mode: http` 且配置 `expect.stream`
- **THEN** 系统 SHALL 以配置错误退出，提示 expect.stream 仅支持 stream mode

#### Scenario: llm expect stream firstTokenMs 非法
- **WHEN** YAML 中 llm target 的 `expect.stream.firstTokenMs` 不是合法 `RawValueExpectation`
- **THEN** 系统 SHALL 以配置错误退出，提示 expect.stream.firstTokenMs 格式错误

### Requirement: 日志配置格式
系统 SHALL 支持可选的 `server.logging` 配置，用于定义运行时日志等级、命令行日志等级、文件日志等级、文件路径和滚动策略。`server.logging` 未配置时 SHALL 使用内置默认值。系统 SHALL NOT 支持 `server.logging.console.enabled`、`server.logging.console.format`、`server.logging.file.enabled`、`server.logging.file.format` 或 `server.logging.file.rotation.enabled` 字段。

#### Scenario: 未配置 logging 使用默认值
- **WHEN** 配置文件未声明 `server.logging`
- **THEN** 系统 SHALL 使用 `server.logging.level=info`、`server.logging.console.level=info`、`server.logging.file.level=info`、`server.logging.file.path=<resolved dataDir>/logs/dial.log`、`server.logging.file.rotation.size=50MB`、`server.logging.file.rotation.frequency=daily` 和 `server.logging.file.rotation.maxFiles=14`

#### Scenario: console 和 file level 继承全局 level
- **WHEN** 配置声明 `server.logging.level: warn` 且未声明 `server.logging.console.level` 和 `server.logging.file.level`
- **THEN** 系统 SHALL 将 console 和 file 的日志等级均解析为 `warn`

#### Scenario: 显式配置文件日志路径
- **WHEN** 配置声明 `server.logging.file.path`
- **THEN** 系统 SHALL 使用该路径作为文件日志路径，而不是默认 `<resolved dataDir>/logs/dial.log`

#### Scenario: 相对日志路径
- **WHEN** `server.logging.file.path` 是相对路径
- **THEN** 系统 SHALL 基于配置文件所在目录解析为绝对路径

#### Scenario: 绝对日志路径
- **WHEN** `server.logging.file.path` 是绝对路径
- **THEN** 系统 SHALL 原样使用该绝对路径，并允许该路径位于 `dataDir` 之外

#### Scenario: 不支持日志开关和格式字段
- **WHEN** 配置声明 `server.logging.console.enabled`、`server.logging.console.format`、`server.logging.file.enabled`、`server.logging.file.format` 或 `server.logging.file.rotation.enabled`
- **THEN** 系统 SHALL 以配置错误退出并提示存在未知字段

### Requirement: 日志配置校验
系统 SHALL 在启动期校验 `server.logging` 配置。日志等级 SHALL 只能是 `trace`、`debug`、`info`、`warn`、`error` 或 `fatal`。`rotation.size` SHALL 使用有效 size 格式且解析为正整数字节数。`rotation.frequency` SHALL 只能是 `hourly`、`daily` 或 `weekly`。`rotation.maxFiles` SHALL 是正整数。

#### Scenario: 非法日志等级
- **WHEN** 配置声明 `server.logging.level: verbose`
- **THEN** 系统 SHALL 以配置错误退出并提示日志等级非法

#### Scenario: 非法滚动大小
- **WHEN** 配置声明 `server.logging.file.rotation.size: "large"`
- **THEN** 系统 SHALL 以配置错误退出并提示 size 格式非法

#### Scenario: 非法滚动频率
- **WHEN** 配置声明 `server.logging.file.rotation.frequency: monthly`
- **THEN** 系统 SHALL 以配置错误退出并提示 frequency 非法

#### Scenario: 非法归档数量
- **WHEN** 配置声明 `server.logging.file.rotation.maxFiles: 0`
- **THEN** 系统 SHALL 以配置错误退出并提示 maxFiles 必须为正整数

#### Scenario: 非法日志路径
- **WHEN** 配置声明 `server.logging.file.path` 为空字符串或空白字符串
- **THEN** 系统 SHALL 以配置错误退出并提示日志路径非法
