# Evidence Query UT Taxonomy 设计草案

## 背景

这份文档用于整理 `evidence collect` 相关 UT 的设计边界。

本轮讨论的目标已经明确收敛为：

- 不验证诊断结论是否正确
- 不验证 agent 是否完成了根因推断
- 只验证在一次真实诊断提问中，系统能否把用户需要的数据查到、查准、并按预期结构返回

为了让 UT 尽量覆盖真实 support 场景，本轮设计参考了飞书 wiki 下大量历史客户 case review / RCA / incident report，但这些历史 case 的作用不是提炼根因知识，而是反向枚举：

- 用户在 incident 中真实会查哪些数据
- 这些数据通常来自哪些观测入口
- 这些查询的对象粒度、时间范围、返回形态是什么

## 问题收敛

如果只看用户表述，UT 很容易演化成一堆自然语言模板列表。
如果只看实现入口，UT 又会退化成 collector / connector contract test。

本轮讨论最终得到的核心判断是：

- 不适合继续把当前设计压成一棵单轴 taxonomy
- 不管单轴是按“数据源”还是按“query intent”，都会把多个维度混在一起
- 更稳的设计是把 `UT 主 contract`、`用户查询模板`、`历史 case 覆盖标签` 分开建模

在此基础上，进一步收敛出的阶段性策略是：

- phase 1 先做 `atomic-first`
- 如果一个复杂 query template 会拆成多个 atomic query，就给每个 atomic query 一个独立的 `sub query template`
- 复杂 query template 先只保留为父模板或组合说明
- orchestration / multiple query 作为后续单独覆盖层补充

## 原始分类

在遍历历史 case 时，先得到了一组比较自然的“诊断主题分类”。这组分类对 coverage 很有帮助：

1. `SQL / 执行计划`
2. `TiDB / TiKV 运行指标`
3. `热点 / region / store`
4. `CDC / TiFlash / PD / etcd 专项`
5. `日志 / profile / 系统`
6. `上下文 / 变更`

这组分类保留价值很高，因为它们对应了历史 case 中反复出现的问题族。

但它们不适合直接作为 UT 主 taxonomy，原因在于这 6 类已经混入了不同维度：

- 有些是诊断语义主题，例如 `热点 / region / store`
- 有些更像数据通道，例如 `日志 / profile / 系统`
- 有些是组件专题打包，例如 `CDC / TiFlash / PD / etcd 专项`
- 有些是关联背景，例如 `上下文 / 变更`

因此，原始分类更适合在后续设计里扮演 `coverage tags`，而不是 `UT 主 contract`。

## 当前实现里的既有边界

在进入新 taxonomy 设计之前，还需要注意当前代码里已经存在两类边界：

- `channel` 视角
  - 例如 `logs`、`query_or_metric`、`events`、`ticket_context`
- `artifact / evidence bundle` 视角
  - 例如 `metrics`、`workload`、`resource_control`、`component_health`、`queries`、`logs`、`links`、`gate_checks`、`collection_errors`

这意味着系统内部其实已经天然存在：

1. 采集通道
2. 落盘产物
3. 诊断语义主题

如果在 UT taxonomy 中继续把这三者压成一层，就会出现下面的混淆：

- “热点 / region / store” 这样的语义主题会和 `metrics`、`logs` 这样的采集入口并列
- “组件健康” 这样的 artifact bucket 会和 “执行计划问题” 这样的诊断主题并列
- 同一条 UT 同时承担“采集 contract”“用户问法”“历史问题语义”三种职责

因此，本轮三层设计有一个隐含原则：

- 保持当前实现中的采集 / artifact 边界
- 单独引入用户查询模板层
- 再单独保留历史 case 的语义覆盖标签

## 为什么不继续做单轴降维

### 按数据源单轴分类的问题

如果只按数据源分类，优点是容易贴近实现边界，但会出现两个问题：

- 用户并不会按 source 提问
- 同一数据源往往支持多种完全不同的查询动作

例如同一份 metrics：

- 可以用来查某实例的 CPU 曲线
- 也可以用来查某时间窗 CPU 最高的 TiKV 排名

这两条 UT 的断言重点完全不同，不能因为“都来自 metrics”就视为同一类。

### 按 query intent 单轴分类的问题

如果只按 query intent 分类，优点是贴近用户问法，但也会出现两个问题：

- 同一个 intent 背后可能要走多个观测入口
- intent 名称天然会夹带语义，容易把“查数”和“推断”混在一起

例如：

- “查热点实例上的重 SQL”

这其实至少隐含了两步：

1. 先定位热点实例
2. 再在该实例范围内查 TopSQL

它不是一个天然原子的查询 contract。

### 本轮结论

因此，UT 不应该继续追求“一棵更漂亮的树”，而应该拆成三层：

1. `UT 主 contract facets`
2. `用户查询模板`
3. `历史 case coverage tags`

## 三层设计

### 第一层：UT 主 contract facets

这层是 UT 的底座，用来定义一条 UT 在测什么能力、如何断言、以及如何去重。

这一层不承载诊断主题，不承载自然语言问法，只承载可组合、可断言、可复用的原子查询能力。

建议最少拆成 5 个正交轴：

| 轴 | 含义 | 例子 |
| --- | --- | --- |
| `channel` | 数据从哪里来 | `metrics`、`workload_query`、`logs`、`profile`、`topology_state`、`task_state`、`change_event`、`system_infra` |
| `scope` | 查的对象是谁 | `cluster`、`instance`、`sql_digest`、`plan_digest`、`table/index`、`store`、`region`、`changefeed/task`、`node` |
| `operator` | 本次查询动作是什么 | `series_window`、`topk_rank`、`detail_lookup`、`decode_plan`、`log_slice`、`profile_snapshot`、`event_list` |
| `time_contract` | 时间如何约束 | `point`、`window`、`event_window`、`aligned_compare` |
| `return_shape` | 返回结构长什么样 | `timeseries`、`table_rows`、`structured_blob`、`log_records`、`artifact_link`、`partial_with_error` |

几条典型的 atomic contract 例子：

- `metrics / instance / topk_rank / window / table_rows`
- `metrics / instance / series_window / window / timeseries`
- `workload_query / sql_digest / detail_lookup / window / table_rows`
- `workload_query / plan_digest / decode_plan / window / structured_blob`
- `logs / instance / log_slice / window / log_records`
- `change_event / cluster / event_list / window / table_rows`

这一层决定：

- 测试实现应该怎么发起
- 测试断言应该检查哪些字段
- 两条测试是不是本质重复

### 第二层：用户查询模板

这层的目标不是做主 taxonomy，而是让 UT 长得像真实用户请求。

这一层内部建议再拆成两种角色：

- `sub query template`
- `parent query template`

也就是说，用户查询模板层决定：

- 用户在 incident 中可能怎么问
- 一条请求应该映射到哪些 atomic query
- 哪些 facet 组合在真实场景里是高频且值得保留的

#### `sub query template` 的定位

`sub query template` 是 atomic contract 的测试入口或别名，不是新的主抽象。

它的职责是：

- 给一条 atomic case 提供接近真实支持问法的题面
- 让 atomic case 可以作为独立测试入口运行
- 让多个父模板复用同一个 canonical atomic query

它**不应该**承担下面这些职责：

- 不应该成为独立 taxonomy
- 不应该和 facet 并列成为第二套 contract
- 不应该承载过多实现语义，例如默认缩窗规则、复杂排序逻辑、跨步绑定规则

换句话说：

- `facet / query_contract` 是主键
- `sub query template` 是入口

#### `parent query template` 的定位

`parent query template` 用来描述复杂查询如何由多个 atomic query 组成。

它的职责是：

- 记录一个复杂问题会拆成哪些 `sub query template`
- 记录这些子查询之间的关系，例如 `refine / enrich / fanout / fallback`
- 作为未来 orchestration test 的组合说明

在 phase 1 中，`parent query template` 不作为主 coverage 单元，也不应被算作“已测通过”。

为了避免把 template 做成无限膨胀的自然语言枚举，建议将其组织成一个简单的二层结构：

#### 一级：查询动作

- `read_metric`
- `rank_entities`
- `inspect_object`
- `search_events`
- `list_changes`

#### 二级：查询对象

- `service_symptom`
- `sql_workload`
- `plan_stats`
- `component_instance`
- `region_store_hotspot`
- `replication_task`
- `infra_host`
- `ops_change`

几个例子：

- “查某时间窗 CPU 最高的 TiKV 实例”
  - `rank_entities x component_instance`
- “查这条 SQL 的执行计划和 estRows / actRows”
  - `inspect_object x plan_stats`
- “查该时间窗是否发生过 OOM / restart / panic”
  - `search_events x infra_host`
- “查该时间窗是否有 DDL / analyze / scale in”
  - `list_changes x ops_change`

这一层的职责是：

- 作为 UT 的运行入口
- 保证题面贴近真实 support 问法
- 帮助控制模板是否值得保留

这里需要补一条硬约束：

- 每个 `sub query template` 只能对应一个 atomic contract
- 一个复杂 `parent query template` 可以引用多个 `sub query template`
- 但不能反过来让 `sub query template` 自己再长成一套组合语义

### 第三层：历史 case coverage tags

这层不参与运行，也不参与断言逻辑，只参与 coverage 审计。

它回答的是：

- 这条 UT 是从哪类真实 incident 反推出来的
- 某一类历史问题族是否已经被采样到
- 是否还有明显缺口

这层建议直接复用原始分类，并允许附带 case ID：

- `SQL / 执行计划`
- `TiDB / TiKV 运行指标`
- `热点 / region / store`
- `CDC / TiFlash / PD / etcd 专项`
- `日志 / profile / 系统`
- `上下文 / 变更`

例如某条 UT 可以同时带：

- `热点 / region / store`
- `TiDB / TiKV 运行指标`
- `APID-12473`
- `ST-1824`

## 三层之间的关系

三层不是三个并列 taxonomy，而是三个不同职责的索引：

- `facets`：定义测试能力主键
- `query templates`：定义测试题面和运行入口
- `coverage tags`：定义测试覆盖了哪些真实问题族

可以简单理解为：

- `facets` 是骨架
- `query template` 是题面
- `coverage tags` 是出处和覆盖标记

关系如下：

```text
历史 case
  -> 提炼出 coverage tags
  -> 提炼出 query templates

sub query template
  -> 映射到 1 个 atomic contract

parent query template
  -> 引用 1..N 个 sub query template

atomic contract
  -> 决定测试实现和断言结构
```

## 运行建议

本轮讨论更倾向于采用：

- 运行入口按 `sub query template`
- 断言按 `facet / query_contract`
- coverage 报表按 `coverage tags`

这意味着：

- 不能只按 facet 跑，否则会偏实现
- 也不能只按 template 跑，否则会失去能力级可断言性

更合适的运行模型是：

1. 用 `sub query template` 发起 atomic 请求
2. 绑定预期 `facet / query_contract`
3. 按 facet 断言是否命中了正确的数据入口、对象粒度、时间约束和返回结构
4. 最后按 tag 统计 coverage

### 两张 coverage 报表

建议从一开始就把 coverage 拆成两张，而不是混算：

#### `atomic coverage`

用于回答：

- 哪些 atomic contract 已经有独立入口和独立断言
- 哪些高频 `sub query template` 已经覆盖
- `channel x scope x operator x time_contract x return_shape` 的哪些组合已经有测试

#### `orchestration coverage`

用于回答：

- 哪些复杂 `parent query template` 已经被测
- 哪些 step relation 已覆盖，例如 `refine / enrich / fanout / fallback`
- 哪些跨步 wiring 已验证，例如：
  - `metrics -> workload`
  - `workload -> plan`
  - `task_state -> logs`
  - `event -> anchored window -> workload`

phase 1 的主目标是先把 `atomic coverage` 建立起来，但不能用它代替 `orchestration coverage`。

## 与现有 eval / evidence 结构的对齐

从当前仓库结构看，`atomic-first + sub query template` 和现有实现是“半顺手对齐”的：

- 顺手的部分
  - 现有 runner 本来就是“一条 case 对应一个 prompt”的运行模型
  - evidence 落盘已经按 artifact bucket 稳定分好类
- 不顺手的部分
  - 当前 `execution_contract / evidence_collection` 粒度偏粗，更偏 diagnosis，而不是原子查询语义
  - 当前 channel 也不足以直接表达更细的 atomic contract

因此，更自然的落点不是改 evidence 文件布局，而是在 `case.yaml / judge.yaml` 这层补一个更细的 `query_contract`。

### 建议的落点

- 保持现有 evidence artifacts 不动
- `case.yaml` 增加轻量的 `sub_query_template`
- `judge.yaml` 增加一个新的 `query_contract`
- 现有 `execution_contract` 和 `evidence_collection` 继续只做底层保障，不承载 atomic query 语义

### `query_contract` 草模

下面是一份建议中的最小草模：

```yaml
case:
  prompt:
    text: "查 2026-03-01 00:00 到 02:30 CPU 最高的 TiKV 实例"
  sub_query_template:
    id: rank_tikv_cpu_in_window
    intent: rank_entities
    object: component_instance

judge:
  query_contract:
    channel: metrics
    scope: instance
    operator: topk_rank
    time_contract: window
    return_shape: table_rows
    required_filters:
      - cluster_id
      - time_range
    required_fields:
      - instance_id
      - metric_name
      - value
```

对于 atomic case，建议以 `query_contract` 为准；`sub_query_template` 只是它的用户侧入口。

## Atomic Query 的样例

Atomic UT 的要求是：

- 一条 `sub query template` 只对应一个 atomic contract
- 不依赖前一步查询结果
- 可以独立运行和独立断言

### 样例 1：查某时间窗 CPU 最高的 TiKV 实例

```yaml
name: rank_tikv_cpu_in_window

sub_query_template:
  id: rank_tikv_cpu_in_window
  intent: rank_entities
  object: component_instance
  prompt_shape: "查 2026-03-01 00:00 到 02:30 CPU 最高的 TiKV 实例"

query_contract:
  channel: metrics
  scope: instance
  operator: topk_rank
  time_contract: window
  return_shape: table_rows

coverage_tags:
  - TiDB / TiKV 运行指标
  - 热点 / region / store
```

主要断言点：

- 使用的是 metrics 渠道
- 时间窗正确
- 结果粒度是 instance
- 返回的是 top-k 列表，而不是曲线或自然语言总结

### 样例 2：查某条 SQL 的执行计划

```yaml
name: inspect_sql_plan

sub_query_template:
  id: inspect_sql_plan_by_digest
  intent: inspect_object
  object: plan_stats
  prompt_shape: "查 digest=<sql_digest> 在指定时间窗的执行计划和 estRows/actRows"

query_contract:
  channel: workload_query
  scope: plan_digest
  operator: decode_plan
  time_contract: window
  return_shape: structured_blob

coverage_tags:
  - SQL / 执行计划
```

主要断言点：

- 能正确按 digest / plan digest 定位对象
- 能返回结构化 plan 信息
- 返回里包含 estRows / actRows 或等价字段

### 样例 3：查某时间窗是否发生过 DDL / analyze

```yaml
name: list_schema_changes_in_window

sub_query_template:
  id: list_schema_changes_in_window
  intent: list_changes
  object: ops_change
  prompt_shape: "查指定时间窗内是否发生过 DDL 或 analyze"

query_contract:
  channel: change_event
  scope: cluster
  operator: event_list
  time_contract: window
  return_shape: table_rows

coverage_tags:
  - 上下文 / 变更
```

## Multiple Query 的样例

当一个 `parent query template` 需要多个 atomic query 时，不应把它视作“一个大 facet”，而应定义成一个显式 `query plan`。

也就是说：

- 每个 step 仍然是一个 atomic contract
- steps 之间的关系、参数传递和时间传播要显式定义
- 不依赖模型临场自由发挥

### 允许的串联关系

建议只允许 4 种：

- `refine`：前一步缩小下一步的查询范围
- `enrich`：前一步找到对象，下一步补详情
- `fanout`：前一步返回多个对象，对每个对象做同类查询
- `fallback`：主路径拿不到结果时，走备选路径

### Step 之间建议显式定义的字段

- `depends_on`
- `relation`
- `bindings`
- `selector`
- `time_propagation`
- `fanout_limit`
- `allow_partial`
- `required`

### 样例 4：查热点 TiKV 上最重的 SQL 及其执行计划

```yaml
parent_query_template:
  id: hot_tikv_heavy_sql_and_plan
  intent: inspect_hot_sql_plan
  object: component_instance
  prompt_shape: "查 2026-03-01 00:00 到 02:30 最热点 TiKV 上最重的 SQL 和它的执行计划"

steps:
  - id: s1_hot_instance
    sub_query_template: rank_tikv_cpu_in_window

  - id: s2_top_sql
    depends_on: [s1_hot_instance]
    relation: refine
    bindings:
      instance_id: "${s1_hot_instance.rows[0].instance_id}"
    sub_query_template: rank_instance_topsql_in_window

  - id: s3_plan_detail
    depends_on: [s2_top_sql]
    relation: enrich
    bindings:
      sql_digest: "${s2_top_sql.rows[0].sql_digest}"
    sub_query_template: inspect_sql_plan_by_digest

coverage_tags:
  - TiDB / TiKV 运行指标
  - 热点 / region / store
  - SQL / 执行计划
```

这里真正被测的是两部分：

- 每个 atomic step 是否正确查数
- step 与 step 之间的 wiring 是否正确

### 样例 5：查 lag 最高的 changefeed 及其错误日志

```yaml
parent_query_template:
  id: max_lag_changefeed_and_logs
  intent: inspect_lagging_changefeed
  object: replication_task
  prompt_shape: "查指定时间窗 lag 最高的 changefeed，以及对应错误日志"

steps:
  - id: s1_rank_changefeed
    sub_query_template: rank_changefeed_lag_in_window

  - id: s2_logs
    depends_on: [s1_rank_changefeed]
    relation: refine
    bindings:
      changefeed_id: "${s1_rank_changefeed.rows[0].changefeed_id}"
    sub_query_template: inspect_changefeed_logs_in_window

coverage_tags:
  - CDC / TiFlash / PD / etcd 专项
  - 日志 / profile / 系统
```

## 什么时候该拆成 Atomic，什么时候保留 Multiple

建议原则：

- 如果后一步的 selector 依赖前一步输出对象，则可以定义成 multiple
- 如果两步之间只共享时间窗，不共享对象或锚点，则不应硬串

例如：

适合做 multiple：

- `top instance -> logs of that instance`
- `top sql digest -> plan of that digest`
- `lagging changefeed -> logs of that changefeed`

不适合硬串：

- `top sql`
- `ddl history`

因为后者虽然都在同一 incident 中有价值，但彼此没有天然对象依赖，更适合保留为两条 atomic UT。

## 当前主要结论

1. 历史 case 更适合提供 `coverage 样本空间`，不适合直接变成一维 taxonomy。
2. 原始 6 类诊断主题应保留，但应降级为 `coverage tags`。
3. `UT 主 contract` 应使用 faceted 设计，而不是继续压缩成单轴 tree。
4. `sub query template` 应作为 atomic contract 的入口或 alias，而不是第二套主 contract。
5. `parent query template` 在 phase 1 只作为组合说明，不应直接算作主 coverage 单元。
6. `query_contract` 更适合作为 atomic UT 的判断主键；现有 `execution_contract / evidence_collection` 继续只做底层保障。
7. atomic UT 应该是主力；multiple UT 只用于那些确实存在显式对象依赖的真实查询场景。

## 当前仍需继续推敲的问题

1. `facet` 五个轴是否已经足够，还是需要单独拆出 `cardinality`。
2. `topology_state`、`task_state`、`system_infra` 这几个 channel 的边界是否要进一步收紧。
3. multiple query 的 `time_propagation` 是否需要更严格限制，避免 scenario 过度自由。
4. 是否需要额外引入一层 `orchestration contract`，专门断言 multi-step wiring，而不是把它混进 atomic UT 的断言里。
5. 是否需要把 `sub query template` 进一步弱化成纯 alias，避免和 `query_contract` 形成双重真相。
6. 最终的 UT 编排数量如何控制，避免 faceted matrix 做成完整笛卡尔积。

## 建议的下一步

后续如果进入实现或评审阶段，建议直接产出一份矩阵，而不是继续扩写 taxonomy 名词：

```text
channel x scope x operator x time_contract x return_shape
```

然后为每个被选中的组合补：

- 典型 query template
- 对应 coverage tags
- atomic / multiple 标记
- 主要断言点

这样设计会比单树更严格，也更适合后续真正落到 UT 代码里。
