# SQL Catalog MVP V1 设计文档

版本：V1.0  
目标读者：实施 Agent / 后端工程师 / 前端工程师 / 产品协作者  
范围：TiDB Cloud SQL 稳定性治理工具 MVP，覆盖 SQL 发现、建档、分诊、分析、治理、客户反馈、观察和报告闭环。

---

## 0. 设计结论摘要

MVP V1 只保留少量清晰概念：

| 实体 | 类比 | 定位 |
|---|---|---|
| Slow Log | 化验单 / 检查明细 | 单次慢查询事实，是证据，不是治理对象。 |
| Finding | 症状发现 | 外部系统或人工输入的一次“发现”。 |
| SQL Catalog Item | 病历档案 | 系统一等公民，表示一条登记在册、需要关注的 SQL。 |
| Case / Ticket | 一次诊疗过程 | 一次分析、优化、客户跟进、观察和关闭过程。 |
| Record | 病历记录 / 诊断 / 处方 | 基于模板填写的 Markdown 记录，可表达建议、无建议、反馈、观察结果等。 |
| Group | 分组视角 | 分为 Cluster Group 和 SQL Group，分别用于集群观察和 SQL 治理分组。 |
| Relation | 关联关系 | 支持任意实体之间灵活关联。 |
| Timeline Event | 时间线事件 | 支持 SQL Catalog Item 和 Case 的历史追踪。 |

核心原则：

1. **SQL Catalog Item 是一等公民**，主时间线、指标趋势、Findings、Records、Cases 都围绕它组织。
2. **SQL Catalog Item 的唯一身份是 `org_id + cluster_id + sql_digest`**。
3. **DB、plan digest、resource group、user 都是观测维度，不进入 SQL Catalog Item identity**。
4. **Finding 是对外标准输入**。外部巡检、人工创建、Slow Log Explorer 标记等都创建 Finding。
5. **首次 Finding 到达时，系统自动创建并关联 SQL Catalog Item**，默认状态为 `waiting_triage`。
6. **Case 是一次治理生命周期，不是 SQL 本体**。一个 Case 可以关联一个或多个 SQL Catalog Item、Finding、外部 Ticket、Record 或其他 Case。
7. **Recommendation 不作为独立核心实体**，统一用 Record 表达。Record 可以是分析记录、优化建议、暂不处理结论、客户反馈、观察结果等。
8. **多集群横向对比通过 Cluster Group + Digest Comparison View 实现，不改变 SQL Catalog Item identity**。
9. **digest 不同但优化方式相同的 SQL，通过 SQL Group 手工组织**。MVP 不做不可靠的自动相似 SQL 合并。
10. **所有客户可见内容必须显式标记 `visibility = customer_visible`**；默认全部为 `internal`。

---

## 1. 设计目标和非目标

### 1.1 MVP 目标

MVP V1 需要实现以下闭环：

```text
Finding 输入
  → 自动创建 / 关联 SQL Catalog Item
  → Catalog Item 等待分诊
  → 人工创建或关联 Case
  → 基于模板填写 Record
  → 同步客户 / 记录反馈
  → 观察优化后表现
  → 关闭 Case
  → SQL Catalog Item 保留完整历史
  → 生成周报 / 客户报告
```

系统需要回答：

1. 这条 SQL 是什么时候第一次被发现的？
2. 最近一周它出现了多少次？集中在哪些 DB？有哪些 plan digest？
3. 它是否是新检出、持续检出、复发或已经进入治理中？
4. 目前有没有 Case？Case 推进到哪一步？
5. 针对它做过哪些分析、结论、建议、客户反馈和观察？
6. 如果客户有多套横向扩展集群，同一 digest 在不同集群上的表现差异如何？
7. 哪些 SQL 可以作为一组一起治理、一起出报告？

### 1.2 MVP 非目标

MVP V1 明确不做：

1. 不自动合并不同 digest 的 SQL。
2. 不基于 normalized SQL、AST 或 embedding 自动判断相似 SQL。
3. 不把 plan digest 作为 SQL identity。
4. 不把 DB name 作为 SQL identity。
5. 不自动执行优化动作，例如自动加索引、自动绑定 plan。
6. 不要求接管全量 Slow Log 存储。MVP 只保存与 Finding / Case 相关的 Slow Log 样本、摘要或外部引用。
7. 不做复杂 AI 自动诊断。后续可以把 AI 作为 Record 草稿生成器，但 V1 以人工模板化记录为主。

---

## 2. 核心实体定义

## 2.1 Slow Log

### 2.1.1 定位

Slow Log 表示一次单独的慢查询事实。

它是证据，不是治理对象。

一条 Slow Log 描述的是：

```text
某个时间点，在某个 org / cluster / DB 中，某条 SQL 的一次慢执行。
```

### 2.1.2 存储策略

MVP 不承担全量慢日志仓库职责。

Slow Log 实体用于保存以下内容之一：

1. Finding 携带的慢日志样本。
2. 人工分析时手动关联的 Slow Log 样本。
3. 外部 Slow Log Explorer 中某条慢日志的引用。
4. 重要 Case 中需要沉淀的关键慢日志证据。

全量慢日志仍然保留在现有日志系统或 Slow Log Explorer 中，本系统通过外部链接或引用访问。

### 2.1.3 核心字段

```text
id
source
external_log_id
org_id
project_id
cluster_id
db_name
sql_digest
plan_digest
normalized_sql
sample_sql_redacted
start_time
duration_ms
process_time_ms
wait_time_ms
scan_rows
scan_keys
result_rows
memory_bytes
disk_bytes
resource_group
user_name
plan_summary
raw_ref_url
raw_payload_json
created_at
```

---

## 2.2 Finding

### 2.2.1 定位

Finding 是系统对外标准输入。

外部巡检、人工发现、客户提单、Slow Log Explorer 标记、定时任务和内部工具都必须通过 Finding 进入系统。

Finding 表示：

```text
在某个时间窗口内，某个来源发现某条 SQL 有值得关注的表现。
```

Finding 是事实输入，不等于 Case，也不等于治理结论。

### 2.2.2 输入约束

MVP 中，一个 Finding 只描述一个 `org_id + cluster_id + sql_digest`。

如果外部系统一次检出多条 SQL，应拆成多个 Finding 提交。

Finding 必须提供：

```text
source
source_event_id
org_id
cluster_id
sql_digest
time_window_start
time_window_end
```

`normalized_sql` 强烈建议提供。若未提供，SQL Catalog Item 仍可创建，但 UI 只能展示 digest。

`source_event_id` 必须全局可幂等。对于人工创建，前端生成 `manual:<uuid>`。

### 2.2.3 核心字段

```text
id
source
source_event_id
org_id
project_id
cluster_id
sql_digest
normalized_sql
time_window_start
time_window_end
severity
reason_codes
metrics_json
dimensions_json
created_by
created_at
```

`dimensions_json` 用于保存观测维度，例如：

```json
{
  "db_names": ["db_order", "db_order_01"],
  "plan_digests": ["plan_a", "plan_b"],
  "resource_groups": ["rg_default"]
}
```

`metrics_json` 用于保存指标摘要，例如：

```json
{
  "slow_count": 32,
  "avg_latency_ms": 850,
  "max_latency_ms": 6200,
  "total_latency_ms": 27200,
  "avg_scan_rows": 120000,
  "max_scan_rows": 900000,
  "avg_memory_bytes": 104857600,
  "max_memory_bytes": 536870912
}
```

---

## 2.3 SQL Catalog Item

### 2.3.1 定位

SQL Catalog Item 是系统一等公民。

它表示：

```text
某个 org 下某个 cluster 中，一条登记在册、需要关注的 SQL Digest。
```

它类似一个人的病历档案。Finding 是症状发现，Case 是一次诊疗过程，Record 是病历记录。

### 2.3.2 Identity

SQL Catalog Item 的唯一身份固定为：

```text
org_id + cluster_id + sql_digest
```

`project_id` 是属性，不进入唯一身份。

原因：

1. `cluster_id` 已经能定位具体集群。
2. SQL 的治理对象在实际使用中以 org、cluster、digest 最容易解释。
3. project 变化或迁移不应该改变 Catalog Item 的历史档案。

### 2.3.3 不进入 Identity 的字段

以下字段不参与 SQL Catalog Item identity：

```text
db_name
plan_digest
resource_group
user_name
sample_sql parameters
```

它们都是观测维度。

### 2.3.4 状态

SQL Catalog Item 有自己的状态 `status`：

| status | 含义 |
|---|---|
| waiting_triage | 默认状态。首次发现后等待判断是否需要建 Case 或关联已有 Case。 |
| reviewing | 有人在分诊或分析，但还未进入正式 Case。 |
| in_governance | 已关联至少一个 open Case，正在治理中。 |
| watching | 暂不建 Case，但继续观察。 |
| no_action_needed | 明确判断暂不处理。必须有 Record 说明原因。 |
| snoozed | 暂缓处理，到期后重新进入待分诊或观察。 |
| archived | 归档，不进入日常队列。 |

首次创建时：

```text
status = waiting_triage
```

### 2.3.5 核心字段

```text
id
org_id
project_id
cluster_id
sql_digest
normalized_sql
status
owner_id
priority
severity
first_seen_at
last_seen_at
latest_finding_at
created_from_finding_id
created_at
updated_at
```

---

## 2.4 Case / Ticket

### 2.4.1 定位

Case 是一次分析和优化生命周期。

它对应医疗类比里的：

```text
一次挂号、诊断、治疗、复查和结案。
```

Case 可以关联：

1. 一个或多个 SQL Catalog Item。
2. 一个或多个 Finding。
3. 一个或多个 Slow Log。
4. 一个 SQL Group。
5. 外部 Support Ticket / Jira / 文档 / Slack Thread。
6. 其他 Case。

### 2.4.2 状态模型

Case 有三个核心状态字段：

```text
state
stage
resolution
```

`state` 只有两个值：

```text
open
closed
```

`stage` 表示当前治理阶段：

| stage | 含义 |
|---|---|
| triage | 初步分诊。 |
| analyzing | 分析中。 |
| conclusion_ready | 已有结论或方案，待 review 或同步。 |
| customer_pending | 等待客户确认、反馈或执行。 |
| observing | 客户已执行或已有动作，正在观察效果。 |
| resolved | 数据上看已解决，等待最终关闭。 |

`resolution` 只在 `state = closed` 时填写：

| resolution | 含义 |
|---|---|
| fixed | 已优化并验证解决。 |
| no_action_needed | 不需要处理。 |
| not_optimizable | 当前没有合理优化方案。 |
| accepted_risk | 客户或内部接受风险。 |
| false_positive | 误报。 |
| duplicate | 重复 Case。 |
| obsolete | 已过期，例如业务下线。 |

### 2.4.3 核心字段

```text
id
case_no
title
description
state
stage
resolution
owner_id
priority
severity
created_by
created_at
updated_at
closed_at
```

---

## 2.5 Record

### 2.5.1 定位

Record 是模板化 Markdown 记录。

它取代狭义 Recommendation，因为治理过程中不一定总是有“建议”。Record 可以表达：

1. 分诊记录。
2. 分析记录。
3. 优化方案。
4. 暂不处理结论。
5. 无法优化结论。
6. 客户同步记录。
7. 客户反馈。
8. 观察结果。
9. 复盘总结。

Record 可以挂在：

```text
SQL Catalog Item
Case
Finding
SQL Group
```

### 2.5.2 Markdown + 结构化字段

Record 的正文使用 Markdown。

同时把重要字段冗余为结构化字段，方便筛选、统计和报告生成。

例如：

```text
conclusion_type
action_type
risk_level
confidence
customer_action_required
customer_action_status
review_status
visibility
```

### 2.5.3 Record 类型

MVP 支持以下 `record_type`：

| record_type | 含义 |
|---|---|
| triage_record | 分诊记录。 |
| analysis_record | SQL 分析记录。 |
| proposal_record | 治理方案 / 优化建议。 |
| no_action_record | 暂不处理 / 无法优化记录。 |
| customer_sync | 客户同步记录。 |
| customer_feedback | 客户反馈记录。 |
| observation_record | 观察结果。 |
| postmortem_record | 复盘记录。 |
| comment | 普通评论。 |

### 2.5.4 结构化枚举

`conclusion_type`：

```text
optimizable
not_optimizable
no_action_needed
need_more_info
accepted_risk
false_positive
fixed
inconclusive
```

`action_type`：

```text
add_index
modify_index
drop_index
rewrite_sql
update_stats
plan_binding
schema_change
app_change
config_change
data_cleanup
customer_confirm
no_action
other
```

`review_status`：

```text
draft
ready
reviewed
rejected
obsolete
```

`visibility`：

```text
internal
customer_visible
```

默认：

```text
visibility = internal
```

---

## 2.6 Group

### 2.6.1 定位

Group 是分组视角。MVP 中产品上明确分成两类：

```text
Cluster Group
SQL Group
```

底层可以共用一张 `groups` 表，但 UI、文案和行为必须分开。

---

### 2.6.2 Cluster Group

Cluster Group 表示：

```text
一组共同承担某项业务、需要一起观察的集群。
```

成员类型：

```text
cluster
```

用途：

1. 在 Catalog 列表中筛选这组集群下的 SQL Catalog Items。
2. 使用 Digest Comparison View 查看同一 digest 在多个集群之间的表现差异。
3. 生成某个业务集群组的治理周报。
4. 发现同组集群中某个集群表现异常。

重要规则：

```text
Cluster Group 不改变 SQL Catalog Item identity。
```

SQL Catalog Item 仍然是：

```text
org_id + cluster_id + sql_digest
```

Cluster Group 只是观察和筛选视角。

---

### 2.6.3 SQL Group

SQL Group 表示：

```text
一组需要一起分析、一起治理、一起出报告的 SQL 或 Case。
```

成员类型可以是：

```text
sql_catalog_item
case
finding
record
```

用途：

1. 把 digest 不同但优化方式相同的 SQL 放在一起。
2. 把某个客户本周要治理的一批 SQL 放在一起。
3. 把一次专项治理活动中的 SQL 放在一起。
4. 给一组 SQL 写统一 Record。
5. 基于一组 SQL 生成报告。

添加 SQL Group 成员时必须填写 `reason`。

示例 reason：

```text
same optimization: add index on user_id, status
same business module: order-service
same customer ticket: ABC-123
same report scope: 2026-W22 customer report
same incident
```

---

## 2.7 Relation

### 2.7.1 定位

Relation 用于表达任意实体之间的灵活关系。

支持对象：

```text
slow_log
finding
sql_catalog_item
case
record
group
external_link
report
```

### 2.7.2 常用关系类型

```text
discovered
created_from
evidence_of
handled_by_case
linked_to
related_to
duplicate_of
same_issue
same_solution
belongs_to_group
mentioned_in_report
supersedes
blocked_by
```

### 2.7.3 关系规则

1. Finding 创建后，系统自动建立：

```text
Finding --discovered--> SQL Catalog Item
```

2. 如果 SQL Catalog Item 有 open Case，系统自动建立：

```text
Finding --evidence_of--> Case
```

3. Case 关联 SQL Catalog Item 时建立：

```text
Case --handled_by_case--> SQL Catalog Item
```

4. Slow Log 作为证据时建立：

```text
Slow Log --evidence_of--> Finding / Case / SQL Catalog Item
```

5. SQL Group 成员关系使用 `group_members` 表承接，不强制写入 `relations`。如需要表达额外语义，可额外写 relation。

---

## 2.8 Timeline Event

### 2.8.1 定位

Timeline Event 用于展示历史事件。

SQL Catalog Item 和 Case 都有时间线。

### 2.8.2 展示原则

SQL Catalog Item Timeline 是主线，展示这条 SQL 的完整历史：

1. Finding 和指标信号。
2. Catalog Item 状态变化。
3. Record 创建与更新。
4. 关联 Case 的关键里程碑。
5. 外部链接、Group 变更、报告生成等。

Case Timeline 是子线，展示一次治理过程的详细推进：

1. Case 创建。
2. 分析、评论、Record。
3. 客户同步、客户反馈。
4. 观察开始和结束。
5. 关闭和重开。

Catalog Item Timeline 不默认展示 Case 中所有评论，只展示 Case 关键里程碑。用户可以点击展开 Case 子时间线。

---

## 3. 生命周期设计

## 3.1 Finding 输入生命周期

```text
外部系统 / 人工创建 Finding
  ↓
幂等检查 source + source_event_id
  ↓
创建 Finding
  ↓
根据 org_id + cluster_id + sql_digest 查找 SQL Catalog Item
  ↓
如果不存在，创建 SQL Catalog Item，status = waiting_triage
  ↓
建立 Finding → SQL Catalog Item 关系
  ↓
如果 Finding 携带 Slow Log 样本，创建 Slow Log 并建立证据关系
  ↓
写入 SQL Catalog Item Timeline
  ↓
更新 metrics rollup
  ↓
如果 SQL Catalog Item 有 open Case，自动把 Finding 关联到 Case
```

## 3.2 SQL Catalog Item 生命周期

```text
waiting_triage
  → reviewing
  → in_governance
  → watching
  → no_action_needed
  → snoozed
  → archived
```

### 状态变更规则

1. 首次创建：

```text
waiting_triage
```

2. 人工开始分诊：

```text
waiting_triage → reviewing
```

3. 创建或关联 open Case：

```text
任意非 archived 状态 → in_governance
```

4. 所有关联 open Case 关闭后：

- 如果最新 Case resolution = `fixed`：

```text
in_governance → watching
```

- 如果 resolution = `no_action_needed` / `not_optimizable` / `accepted_risk`：

```text
in_governance → no_action_needed
```

- 如果 resolution = `false_positive`：

```text
in_governance → no_action_needed
```

- 如果 resolution = `obsolete`：

```text
in_governance → archived
```

5. `snoozed` 到期：

```text
snoozed → waiting_triage
```

6. `archived` 不自动恢复。若新 Finding 进入 archived item，系统记录事件并提醒 owner，由人工决定是否恢复。

## 3.3 Case 生命周期

默认主流程：

```text
open / triage
  → open / analyzing
  → open / conclusion_ready
  → open / customer_pending
  → open / observing
  → open / resolved
  → closed / fixed
```

旁路流程：

```text
open / triage
  → closed / false_positive

open / analyzing
  → closed / no_action_needed

open / analyzing
  → closed / not_optimizable

open / customer_pending
  → closed / accepted_risk

任意 open 状态
  → closed / duplicate
```

Case 关闭后，如果相关 SQL Catalog Item 又持续产生高优先级 Finding，系统不自动重开 Case。系统创建 Timeline Event 和提醒，由 owner 选择：

1. Reopen 原 Case。
2. 创建新 Case。
3. 维持 watching。
4. 标记 no_action_needed。

## 3.4 Record 生命周期

```text
draft
  → ready
  → reviewed
  → obsolete
```

规则：

1. `customer_visible` Record 必须先进入 `reviewed`，才允许进入客户报告。
2. `proposal_record` 如果 `customer_action_required = true`，Case 一般应进入 `customer_pending`。
3. `observation_record` 如果 `conclusion_type = fixed`，Case 可以进入 `resolved`。

---

## 4. 数据模型

### 4.1 ID 规范

所有一等实体使用字符串 ID，推荐 ULID：

```text
VARCHAR(32)
```

原因：

1. 避免分布式环境下自增 ID 冲突。
2. 便于日志、事件和外部系统引用。
3. 前端 URL 可读性较好。

### 4.2 slow_logs

```sql
CREATE TABLE slow_logs (
  id VARCHAR(32) PRIMARY KEY,

  source VARCHAR(64) NOT NULL,
  external_log_id VARCHAR(256),

  org_id VARCHAR(64) NOT NULL,
  project_id VARCHAR(64),
  cluster_id VARCHAR(64) NOT NULL,

  db_name VARCHAR(256),
  sql_digest VARCHAR(128) NOT NULL,
  plan_digest VARCHAR(128),

  normalized_sql TEXT,
  sample_sql_redacted MEDIUMTEXT,

  start_time DATETIME(3),
  duration_ms BIGINT,
  process_time_ms BIGINT,
  wait_time_ms BIGINT,

  scan_rows BIGINT,
  scan_keys BIGINT,
  result_rows BIGINT,

  memory_bytes BIGINT,
  disk_bytes BIGINT,

  resource_group VARCHAR(128),
  user_name VARCHAR(128),

  plan_summary MEDIUMTEXT,
  raw_ref_url TEXT,
  raw_payload_json JSON,

  created_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_external_log (source, external_log_id),
  KEY idx_digest_time (org_id, cluster_id, sql_digest, start_time),
  KEY idx_plan (plan_digest)
);
```

实现要求：

- 如果 `external_log_id` 不存在，创建时由系统生成。
- `sample_sql_redacted` 必须是脱敏 SQL，不能保存未脱敏参数。

### 4.3 findings

```sql
CREATE TABLE findings (
  id VARCHAR(32) PRIMARY KEY,

  source VARCHAR(64) NOT NULL,
  source_event_id VARCHAR(256) NOT NULL,

  org_id VARCHAR(64) NOT NULL,
  project_id VARCHAR(64),
  cluster_id VARCHAR(64) NOT NULL,

  sql_digest VARCHAR(128) NOT NULL,
  normalized_sql TEXT,

  time_window_start DATETIME(3) NOT NULL,
  time_window_end DATETIME(3) NOT NULL,

  severity VARCHAR(32),
  reason_codes JSON,
  metrics_json JSON,
  dimensions_json JSON,

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_source_event (source, source_event_id),
  KEY idx_digest_time (org_id, cluster_id, sql_digest, time_window_start, time_window_end),
  KEY idx_cluster_time (org_id, cluster_id, time_window_start),
  KEY idx_created_at (created_at)
);
```

### 4.4 sql_catalog_items

```sql
CREATE TABLE sql_catalog_items (
  id VARCHAR(32) PRIMARY KEY,

  org_id VARCHAR(64) NOT NULL,
  project_id VARCHAR(64),
  cluster_id VARCHAR(64) NOT NULL,

  sql_digest VARCHAR(128) NOT NULL,
  normalized_sql TEXT,

  status VARCHAR(64) NOT NULL DEFAULT 'waiting_triage',
  owner_id VARCHAR(128),
  priority VARCHAR(32),
  severity VARCHAR(32),

  first_seen_at DATETIME(3),
  last_seen_at DATETIME(3),
  latest_finding_at DATETIME(3),

  created_from_finding_id VARCHAR(32),

  created_at DATETIME(3) NOT NULL,
  updated_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_item_identity (org_id, cluster_id, sql_digest),
  KEY idx_status (status),
  KEY idx_owner (owner_id),
  KEY idx_last_seen (last_seen_at),
  KEY idx_org_cluster (org_id, cluster_id)
);
```

### 4.5 cases

```sql
CREATE TABLE cases (
  id VARCHAR(32) PRIMARY KEY,

  case_no VARCHAR(64) NOT NULL,
  title VARCHAR(512) NOT NULL,
  description MEDIUMTEXT,

  state VARCHAR(32) NOT NULL DEFAULT 'open',
  stage VARCHAR(64) NOT NULL DEFAULT 'triage',
  resolution VARCHAR(64),

  owner_id VARCHAR(128),
  priority VARCHAR(32),
  severity VARCHAR(32),

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,
  updated_at DATETIME(3) NOT NULL,
  closed_at DATETIME(3),

  UNIQUE KEY uk_case_no (case_no),
  KEY idx_state_stage (state, stage),
  KEY idx_owner (owner_id),
  KEY idx_updated_at (updated_at)
);
```

### 4.6 records

```sql
CREATE TABLE records (
  id VARCHAR(32) PRIMARY KEY,

  record_type VARCHAR(64) NOT NULL,
  template_id VARCHAR(128),

  target_type VARCHAR(64) NOT NULL,
  target_id VARCHAR(32) NOT NULL,

  title VARCHAR(512),
  markdown_body MEDIUMTEXT NOT NULL,

  visibility VARCHAR(32) NOT NULL DEFAULT 'internal',

  conclusion_type VARCHAR(64),
  action_type VARCHAR(64),

  risk_level VARCHAR(32),
  confidence VARCHAR(32),

  customer_action_required BOOLEAN,
  customer_action_status VARCHAR(64),

  review_status VARCHAR(64) DEFAULT 'draft',

  metadata_json JSON,

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,
  updated_at DATETIME(3) NOT NULL,

  KEY idx_target (target_type, target_id),
  KEY idx_record_type (record_type),
  KEY idx_visibility (visibility),
  KEY idx_conclusion (conclusion_type),
  KEY idx_action (action_type),
  KEY idx_review_status (review_status)
);
```

### 4.7 groups

```sql
CREATE TABLE groups (
  id VARCHAR(32) PRIMARY KEY,

  group_type VARCHAR(64) NOT NULL,
  name VARCHAR(256) NOT NULL,
  description TEXT,

  org_id VARCHAR(64),
  project_id VARCHAR(64),

  owner_id VARCHAR(128),
  status VARCHAR(64),

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,
  updated_at DATETIME(3) NOT NULL,

  KEY idx_group_type (group_type),
  KEY idx_org (org_id),
  KEY idx_owner (owner_id)
);
```

`group_type` 只能是：

```text
cluster_group
sql_group
```

### 4.8 group_members

```sql
CREATE TABLE group_members (
  id VARCHAR(32) PRIMARY KEY,

  group_id VARCHAR(32) NOT NULL,

  member_type VARCHAR(64) NOT NULL,
  member_id VARCHAR(128) NOT NULL,

  reason TEXT NOT NULL,

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_group_member (group_id, member_type, member_id),
  KEY idx_member (member_type, member_id)
);
```

规则：

- Cluster Group 只允许 `member_type = cluster`。
- SQL Group 允许 `member_type = sql_catalog_item | case | finding | record`。
- `reason` 必填。

### 4.9 relations

```sql
CREATE TABLE relations (
  id VARCHAR(32) PRIMARY KEY,

  from_type VARCHAR(64) NOT NULL,
  from_id VARCHAR(32) NOT NULL,

  to_type VARCHAR(64) NOT NULL,
  to_id VARCHAR(32) NOT NULL,

  relation_type VARCHAR(64) NOT NULL,
  role VARCHAR(64),
  reason TEXT,

  visibility VARCHAR(32) NOT NULL DEFAULT 'internal',

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,

  KEY idx_from (from_type, from_id),
  KEY idx_to (to_type, to_id),
  KEY idx_relation_type (relation_type)
);
```

### 4.10 timeline_events

```sql
CREATE TABLE timeline_events (
  id VARCHAR(32) PRIMARY KEY,

  event_type VARCHAR(64) NOT NULL,

  actor_type VARCHAR(64) NOT NULL,
  actor_id VARCHAR(128),

  title VARCHAR(512),
  payload_json JSON,

  visibility VARCHAR(32) NOT NULL DEFAULT 'internal',

  created_at DATETIME(3) NOT NULL,

  KEY idx_event_type (event_type),
  KEY idx_created_at (created_at)
);
```

### 4.11 timeline_event_targets

```sql
CREATE TABLE timeline_event_targets (
  id VARCHAR(32) PRIMARY KEY,

  event_id VARCHAR(32) NOT NULL,

  target_type VARCHAR(64) NOT NULL,
  target_id VARCHAR(32) NOT NULL,

  target_role VARCHAR(64),

  created_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_event_target (event_id, target_type, target_id, target_role),
  KEY idx_target (target_type, target_id)
);
```

`target_role` 示例：

```text
primary
secondary
case_milestone
signal
```

### 4.12 metric rollups

#### sql_item_metric_rollups

```sql
CREATE TABLE sql_item_metric_rollups (
  id VARCHAR(32) PRIMARY KEY,

  sql_catalog_item_id VARCHAR(32) NOT NULL,

  window_granularity VARCHAR(16) NOT NULL,
  window_start DATETIME(3) NOT NULL,
  window_end DATETIME(3) NOT NULL,

  source VARCHAR(64),

  slow_count BIGINT DEFAULT 0,
  total_latency_ms BIGINT DEFAULT 0,
  avg_latency_ms BIGINT,
  max_latency_ms BIGINT,
  p95_latency_ms BIGINT,

  avg_scan_rows BIGINT,
  max_scan_rows BIGINT,
  avg_memory_bytes BIGINT,
  max_memory_bytes BIGINT,

  sample_count BIGINT DEFAULT 0,

  updated_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_rollup (sql_catalog_item_id, window_granularity, window_start, source),
  KEY idx_item_window (sql_catalog_item_id, window_start, window_end)
);
```

#### sql_item_db_rollups

```sql
CREATE TABLE sql_item_db_rollups (
  id VARCHAR(32) PRIMARY KEY,

  sql_catalog_item_id VARCHAR(32) NOT NULL,

  window_granularity VARCHAR(16) NOT NULL,
  window_start DATETIME(3) NOT NULL,
  window_end DATETIME(3) NOT NULL,

  db_name VARCHAR(256) NOT NULL,

  slow_count BIGINT DEFAULT 0,
  total_latency_ms BIGINT DEFAULT 0,
  avg_latency_ms BIGINT,
  max_latency_ms BIGINT,

  updated_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_db_rollup (sql_catalog_item_id, window_granularity, window_start, db_name),
  KEY idx_item_window (sql_catalog_item_id, window_start, window_end)
);
```

#### sql_item_plan_rollups

```sql
CREATE TABLE sql_item_plan_rollups (
  id VARCHAR(32) PRIMARY KEY,

  sql_catalog_item_id VARCHAR(32) NOT NULL,

  window_granularity VARCHAR(16) NOT NULL,
  window_start DATETIME(3) NOT NULL,
  window_end DATETIME(3) NOT NULL,

  plan_digest VARCHAR(128) NOT NULL,

  slow_count BIGINT DEFAULT 0,
  total_latency_ms BIGINT DEFAULT 0,
  avg_latency_ms BIGINT,
  max_latency_ms BIGINT,

  updated_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_plan_rollup (sql_catalog_item_id, window_granularity, window_start, plan_digest),
  KEY idx_item_window (sql_catalog_item_id, window_start, window_end),
  KEY idx_plan_digest (plan_digest)
);
```

### 4.13 labels

```sql
CREATE TABLE labels (
  id VARCHAR(32) PRIMARY KEY,

  org_id VARCHAR(64),
  name VARCHAR(128) NOT NULL,
  description TEXT,
  color VARCHAR(32),

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_org_label (org_id, name)
);
```

```sql
CREATE TABLE object_labels (
  id VARCHAR(32) PRIMARY KEY,

  object_type VARCHAR(64) NOT NULL,
  object_id VARCHAR(32) NOT NULL,
  label_id VARCHAR(32) NOT NULL,

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,

  UNIQUE KEY uk_object_label (object_type, object_id, label_id),
  KEY idx_object (object_type, object_id)
);
```

推荐 label 命名：

```text
source:inspection
source:manual
symptom:high-latency
symptom:large-scan
symptom:plan-change
action:add-index
action:rewrite-sql
status:need-review
status:blocked
```

### 4.14 external_links

```sql
CREATE TABLE external_links (
  id VARCHAR(32) PRIMARY KEY,

  link_type VARCHAR(64) NOT NULL,
  title VARCHAR(512) NOT NULL,
  url TEXT NOT NULL,

  visibility VARCHAR(32) NOT NULL DEFAULT 'internal',

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,

  KEY idx_link_type (link_type)
);
```

`link_type`：

```text
support_ticket
jira
slack_thread
doc
slow_log_explorer
dashboard
inspection_report
customer_email
meeting_note
other
```

外部链接通过 `relations` 关联到 Catalog Item、Case、Finding 或 Record。

### 4.15 templates

```sql
CREATE TABLE templates (
  id VARCHAR(128) PRIMARY KEY,

  name VARCHAR(256) NOT NULL,
  record_type VARCHAR(64) NOT NULL,
  target_types JSON NOT NULL,

  markdown_template MEDIUMTEXT NOT NULL,
  field_schema_json JSON,

  default_visibility VARCHAR(32) NOT NULL DEFAULT 'internal',
  default_labels JSON,

  active BOOLEAN NOT NULL DEFAULT TRUE,

  created_by VARCHAR(128),
  created_at DATETIME(3) NOT NULL,
  updated_at DATETIME(3) NOT NULL
);
```

### 4.16 reports

```sql
CREATE TABLE reports (
  id VARCHAR(32) PRIMARY KEY,

  report_type VARCHAR(64) NOT NULL,
  title VARCHAR(512) NOT NULL,

  org_id VARCHAR(64),
  project_id VARCHAR(64),

  time_window_start DATETIME(3),
  time_window_end DATETIME(3),

  visibility VARCHAR(32) NOT NULL DEFAULT 'internal',

  status VARCHAR(64) NOT NULL DEFAULT 'generated',

  snapshot_json JSON,

  generated_by VARCHAR(128),
  generated_at DATETIME(3) NOT NULL,

  KEY idx_report_type (report_type),
  KEY idx_org_time (org_id, time_window_start, time_window_end)
);
```

```sql
CREATE TABLE report_items (
  id VARCHAR(32) PRIMARY KEY,

  report_id VARCHAR(32) NOT NULL,

  item_type VARCHAR(64) NOT NULL,
  item_id VARCHAR(32) NOT NULL,

  snapshot_json JSON,

  created_at DATETIME(3) NOT NULL,

  KEY idx_report (report_id),
  KEY idx_item (item_type, item_id)
);
```

---

## 5. Finding Ingestion API

## 5.1 创建 Finding

```http
POST /api/v1/findings
```

### Request

```json
{
  "source": "inspection",
  "source_event_id": "inspection-20260601-cluster-a-digest-x-window-00",

  "org_id": "org_1",
  "project_id": "project_1",
  "cluster_id": "cluster_a",

  "sql_digest": "digest_xxx",
  "normalized_sql": "select * from orders where user_id = ? and status = ?",

  "time_window_start": "2026-06-01T00:00:00Z",
  "time_window_end": "2026-06-01T01:00:00Z",

  "severity": "high",
  "reason_codes": ["high_latency", "large_scan"],

  "metrics": {
    "slow_count": 32,
    "avg_latency_ms": 850,
    "max_latency_ms": 6200,
    "total_latency_ms": 27200,
    "avg_scan_rows": 120000,
    "max_scan_rows": 900000
  },

  "dimensions": {
    "db_names": ["db_order"],
    "plan_digests": ["plan_abc"]
  },

  "slow_logs": [
    {
      "external_log_id": "slowlog-001",
      "db_name": "db_order",
      "plan_digest": "plan_abc",
      "start_time": "2026-06-01T00:12:31Z",
      "duration_ms": 6200,
      "scan_rows": 900000,
      "memory_bytes": 536870912,
      "raw_ref_url": "https://example.internal/slowlogs/slowlog-001"
    }
  ]
}
```

### Response

```json
{
  "finding_id": "f_01H...",
  "catalog_item_id": "sci_01H...",
  "catalog_item_created": true,
  "linked_case_ids": []
}
```

## 5.2 Ingestion 事务逻辑

同步事务内必须完成：

1. 幂等检查。
2. 创建 Finding。
3. 创建或查找 SQL Catalog Item。
4. 创建 Finding → SQL Catalog Item relation。
5. 更新 SQL Catalog Item 的 `first_seen_at`、`last_seen_at`、`latest_finding_at`。
6. 创建 Timeline Event。

异步任务完成：

1. metric rollup 更新。
2. label 自动生成。
3. Digest Comparison View 所需缓存更新。
4. 报告统计缓存更新。
5. 通知发送。

### 伪代码

```python
def ingest_finding(payload):
    with transaction():
        existing = find_finding(payload.source, payload.source_event_id)
        if existing:
            return existing.response

        finding = create_finding(payload)

        item = find_catalog_item(
            org_id=payload.org_id,
            cluster_id=payload.cluster_id,
            sql_digest=payload.sql_digest,
        )

        item_created = False

        if not item:
            item = create_catalog_item(
                org_id=payload.org_id,
                project_id=payload.project_id,
                cluster_id=payload.cluster_id,
                sql_digest=payload.sql_digest,
                normalized_sql=payload.normalized_sql,
                status="waiting_triage",
                first_seen_at=payload.time_window_start,
                last_seen_at=payload.time_window_end,
                latest_finding_at=payload.time_window_end,
                created_from_finding_id=finding.id,
            )
            item_created = True
            create_timeline_event(
                targets=[item],
                event_type="catalog_item_created",
                actor="system",
                payload={"finding_id": finding.id},
            )
        else:
            update_catalog_item_seen_time(item, payload.time_window_start, payload.time_window_end)
            if not item.normalized_sql and payload.normalized_sql:
                update_normalized_sql(item, payload.normalized_sql)

        create_relation(
            from_obj=finding,
            to_obj=item,
            relation_type="discovered",
            role="primary",
        )

        slow_log_ids = []
        for slow_log_payload in payload.slow_logs:
            slow_log = upsert_slow_log(slow_log_payload, payload)
            slow_log_ids.append(slow_log.id)
            create_relation(slow_log, finding, "evidence_of")
            create_relation(slow_log, item, "evidence_of")

        create_timeline_event(
            targets=[item, finding],
            event_type="finding_ingested",
            actor="system",
            payload=summarize_finding(payload, slow_log_ids),
        )

        open_cases = find_open_cases_linked_to_catalog_item(item.id)
        for case in open_cases:
            create_relation(finding, case, "evidence_of")
            create_timeline_event(
                targets=[case],
                event_type="related_finding_ingested",
                actor="system",
                payload=summarize_finding(payload, slow_log_ids),
            )
            create_timeline_event(
                targets=[item],
                event_type="case_milestone",
                actor="system",
                payload={
                    "case_id": case.id,
                    "milestone": "new_related_finding",
                    "finding_id": finding.id
                },
            )

    enqueue_rollup_job(finding.id)
    enqueue_auto_label_job(finding.id)

    return response
```

---

## 6. 自动标签和分类规则

### 6.1 自动标签

根据 Finding source 自动打标签：

```text
source:inspection
source:manual
source:slow-log
source:customer-ticket
```

根据 reason_codes 自动打标签：

```text
symptom:high-latency
symptom:high-total-latency
symptom:large-scan
symptom:high-memory
symptom:plan-change
symptom:lock-wait
```

自动标签可以被人工移除，但系统应记录 Timeline Event。

### 6.2 New / Recurring / Quiet / Reappeared

这些不是状态，而是列表页和报告中的派生标签。

默认统计窗口：7 天。可配置。

#### New

```text
Catalog Item.first_seen_at 在当前统计窗口内。
```

#### Recurring

```text
当前窗口内有 Finding，且前一个同长度窗口也有 Finding。
```

#### Quiet

```text
前一个窗口有 Finding，当前窗口没有 Finding。
```

#### Reappeared

```text
过去 N 天无 Finding，当前窗口重新出现 Finding。
```

默认 N = 14。

#### Reopened Case

```text
Case 已 closed 后被人工 reopen。
```

系统不自动 reopen Case，只提醒 owner。

---

## 7. 页面信息架构

顶部导航：

```text
Workbench
Catalog
Cases
Groups
Reports
Templates
```

---

## 7.1 Workbench

Workbench 是日常工作入口。

### 7.1.1 队列

1. 待分诊 SQL：`status = waiting_triage`。
2. 我的 SQL：owner 是当前用户的 SQL Catalog Items。
3. 我的 Cases：owner 或 assignee 是当前用户的 Cases。
4. 客户待跟进：`stage = customer_pending` 的 Cases。
5. 观察到期：`stage = observing` 且 observation due 的 Cases。
6. 最近重新出现：派生标签 `reappeared` 的 SQL Catalog Items。

### 7.1.2 待分诊 SQL 列表字段

```text
SQL Digest
SQL Snippet
Org
Cluster
Last Seen
7d Slow Count
7d Max Latency
Affected DBs
Plan Count
Severity
Owner
Linked Open Cases
Status
```

### 7.1.3 快捷动作

```text
Create Case
Link Existing Case
Assign Owner
Add Triage Record
Mark Reviewing
Mark Watching
Mark No Action Needed
Snooze
Add to SQL Group
```

---

## 7.2 Catalog

Catalog 是 SQL Catalog Item 资产库。

### 7.2.1 筛选条件

```text
Org
Project
Cluster
Cluster Group
SQL Group
Digest
Status
Owner
Label
Severity
Last Seen Range
Has Open Case
Source
New / Recurring / Reappeared
```

### 7.2.2 视图

#### Item View

每行是一条 SQL Catalog Item：

```text
org_id + cluster_id + sql_digest
```

字段：

```text
Digest
SQL Snippet
Org
Cluster
Status
Owner
Priority
Severity
First Seen
Last Seen
7d Slow Count
7d Total Latency
7d Max Latency
Affected DB Count
Plan Count
Open Case Count
Latest Record
```

#### Digest Comparison View

当用户选择 Org 或 Cluster Group 时可用。

每行按 digest 折叠，展示该 digest 在多个集群中的表现：

```text
Digest
SQL Snippet
Clusters Hit
Total 7d Slow Count
Worst Cluster
Worst Avg Latency
Worst Max Latency
Open Cases
```

展开后显示：

| Cluster | Item Status | Slow Count 7d | Avg Latency | Max Latency | Top DB | Dominant Plan | Open Case |
|---|---|---:|---:|---:|---|---|---|
| cluster-a | in_governance | 120 | 850ms | 8.2s | db_order | plan_a | #123 |
| cluster-b | watching | 15 | 210ms | 1.1s | db_order_01 | plan_a | - |
| cluster-c | - | 0 | - | - | - | - | - |

注意：Digest Comparison View 是查询视图，不创建新实体。

---

## 7.3 SQL Catalog Item Detail

这是系统核心页面。

### 7.3.1 Header

```text
SQL Digest
Normalized SQL
Org / Project / Cluster
Status
Owner
Priority
Severity
Labels
First Seen
Last Seen
Open Cases
```

操作按钮：

```text
Create Case
Link Case
Add Record
Add Finding
Add Slow Log Evidence
Add External Link
Add to SQL Group
Mark Reviewing
Mark Watching
Mark No Action Needed
Snooze
Archive
```

### 7.3.2 Overview Cards

展示：

```text
Last 24h / 7d / 30d
slow_count
avg_latency
p95_latency
max_latency
total_latency
avg_scan_rows
max_memory
```

### 7.3.3 Distribution Cards

展示：

```text
DB Distribution
Plan Digest Distribution
Source Distribution
Reason Code Distribution
```

plan digest 在这里是观测属性。

### 7.3.4 Related Cluster Performance

如果当前 cluster 属于一个或多个 Cluster Group，则展示同一 digest 在同组其他集群的表现。

字段：

```text
Cluster
Status
Slow Count 7d
Avg Latency
Max Latency
Top DB
Dominant Plan
Last Seen
Open Case
```

### 7.3.5 Tabs

#### Timeline

默认主时间线。支持过滤：

```text
All
Signals
Governance
Records
Cases
Comments
System
```

#### Metrics

展示趋势：

```text
slow_count trend
latency trend
total_latency trend
DB distribution over time
plan distribution over time
```

#### Findings

字段：

```text
Source
Time Window
Severity
Reason Codes
Metrics
Dimensions
Linked Cases
Created At
```

操作：

```text
View Raw Payload
Link to Case
Link to SQL Group
Mark Related Finding
```

#### Slow Logs

展示关联 Slow Log 样本：

```text
Start Time
DB
Duration
Plan Digest
Scan Rows
Memory
External Link
```

#### Records

展示挂在当前 Catalog Item 上的 Records，以及可选展示关联 Case 的 Records。

字段：

```text
Record Type
Title
Conclusion
Action
Visibility
Review Status
Created By
Updated At
```

#### Cases

展示关联 Cases：

```text
Case No
Title
State
Stage
Resolution
Owner
Updated At
```

支持：

```text
Create Case
Link Existing Case
Unlink Case
Expand Case Timeline
```

#### Relations

展示所有关联对象：

```text
Findings
Cases
Records
Groups
External Links
Reports
Related SQL Catalog Items
Related Cases
```

---

## 7.4 Cases

Cases 页面类似 GitHub Issues。

### 7.4.1 Case List

字段：

```text
Case No
Title
State
Stage
Resolution
Owner
Priority
Severity
Linked SQL Count
Customer Pending
Observation Due
Updated At
```

筛选：

```text
State
Stage
Owner
Priority
Label
Org
Cluster
Linked SQL Group
Updated Range
```

### 7.4.2 Case Detail

页面区域：

```text
Header
Description
Linked SQL Catalog Items
Records
Timeline
Customer Feedback
Observation Summary
Relations
```

Case 页面重点回答：

```text
这一次治理推进到哪里了？
结论是什么？
客户是否同步？
客户是否执行？
观察结果如何？
```

---

## 7.5 Groups

Groups 页面分两个入口：

```text
Cluster Groups
SQL Groups
```

### 7.5.1 Cluster Group Detail

展示：

```text
Group Name
成员 Clusters
Top SQL Catalog Items
Digest Comparison Matrix
Active Cases
Weekly Summary
```

Digest Comparison Matrix 按 digest 展示各集群表现。

### 7.5.2 SQL Group Detail

展示：

```text
Group Name
成员 SQL Catalog Items
成员 Cases
成员 Findings
Group-level Records
Reports
```

SQL Group 支持直接创建：

```text
Group-level Record
Group-level Case
Customer Report
```

---

## 7.6 Reports

报告类型：

```text
Internal Weekly Report
Customer SQL Governance Report
Cluster Group Report
SQL Group Report
Case Summary Report
```

报告必须保存 snapshot。

客户报告只能使用：

```text
visibility = customer_visible
review_status = reviewed
```

的 Records 和 Timeline Events。

---

## 7.7 Templates

Templates 用于管理 Record 模板。

模板字段：

```text
name
record_type
target_types
markdown_template
field_schema_json
default_visibility
default_labels
active
```

---

## 8. Record 模板

## 8.1 Triage Record

适用对象：SQL Catalog Item / Finding

```markdown
## 现象

## 影响范围

## 初步判断

## 是否需要创建 Case

## 下一步

## 备注
```

结构化字段：

```text
conclusion_type: need_more_info | optimizable | no_action_needed | false_positive
action_type: customer_confirm | no_action | other
risk_level
confidence
```

## 8.2 Analysis Record

适用对象：SQL Catalog Item / Case

```markdown
## SQL 概况

## 观测指标

## DB 分布

## Plan Digest 分布

## 执行计划观察

## 初步诊断

## 待确认问题
```

## 8.3 Proposal Record

适用对象：Case / SQL Group / SQL Catalog Item

```markdown
## 问题摘要

## 证据

## 建议动作

## 客户侧待办

## 风险

## 回滚方式

## 验证方式
```

结构化字段：

```text
conclusion_type = optimizable
action_type
customer_action_required
risk_level
confidence
review_status
```

## 8.4 No Action Record

适用对象：SQL Catalog Item / Case

```markdown
## 结论

## 原因

## 证据

## 后续观察条件

## 重新处理条件
```

结构化字段：

```text
conclusion_type = no_action_needed | not_optimizable | accepted_risk | false_positive
action_type = no_action
```

## 8.5 Customer Sync Record

适用对象：Case

```markdown
## 同步时间

## 同步渠道

## 同步对象

## 同步内容摘要

## 客户侧待办

## 下一次跟进时间
```

## 8.6 Customer Feedback Record

适用对象：Case / SQL Catalog Item

```markdown
## 反馈来源

## 客户反馈内容

## 客户计划

## 预计执行时间

## 阻塞点

## 下一步
```

## 8.7 Observation Record

适用对象：Case / SQL Catalog Item

```markdown
## Baseline 时间窗口

## Observation 时间窗口

## 优化前指标

## 优化后指标

## 流量是否可比

## 观察结论

## 后续动作
```

结构化字段：

```text
conclusion_type = fixed | inconclusive | not_optimizable | need_more_info
customer_action_status = applied | unknown
```

---

## 9. Timeline 事件类型

MVP 支持以下事件类型：

```text
catalog_item_created
finding_ingested
slow_log_attached
status_changed
owner_changed
priority_changed
severity_changed
label_added
label_removed
record_created
record_updated
case_created
case_linked
case_unlinked
case_stage_changed
case_closed
case_reopened
group_member_added
group_member_removed
external_link_added
report_generated
comment_added
metric_signal_detected
```

### 9.1 Catalog Item Timeline 展示规则

Catalog Item Timeline 包含：

1. target 为当前 SQL Catalog Item 的所有事件。
2. 关联 Case 的里程碑事件。
3. 关联 Group、Report、External Link 的事件。

默认不展示：

1. 每条 Slow Log 明细。
2. Case 中所有普通评论。
3. 与当前 SQL 无关的 Group 事件。

### 9.2 Case Timeline 展示规则

Case Timeline 包含：

1. target 为当前 Case 的所有事件。
2. 关联 SQL Catalog Item 的关键 signal 摘要。
3. 当前 Case 相关 Records、Customer Feedback、Observation。

---

## 10. API 草案

## 10.1 Findings

```http
POST /api/v1/findings
GET  /api/v1/findings/{id}
GET  /api/v1/findings?org_id=&cluster_id=&sql_digest=&source=&from=&to=
POST /api/v1/findings/{id}/relations
```

## 10.2 SQL Catalog Items

```http
GET   /api/v1/catalog-items
GET   /api/v1/catalog-items/{id}
PATCH /api/v1/catalog-items/{id}
GET   /api/v1/catalog-items/{id}/timeline
GET   /api/v1/catalog-items/{id}/metrics
GET   /api/v1/catalog-items/{id}/findings
GET   /api/v1/catalog-items/{id}/slow-logs
GET   /api/v1/catalog-items/{id}/records
GET   /api/v1/catalog-items/{id}/cases
POST  /api/v1/catalog-items/{id}/cases
POST  /api/v1/catalog-items/{id}/records
POST  /api/v1/catalog-items/{id}/relations
POST  /api/v1/catalog-items/{id}/external-links
POST  /api/v1/catalog-items/{id}/labels
DELETE /api/v1/catalog-items/{id}/labels/{label_id}
```

## 10.3 Digest Comparison

```http
GET /api/v1/catalog/digest-comparison?org_id=&cluster_group_id=&from=&to=&digest=
```

返回按 digest 折叠后的跨集群表现。

## 10.4 Cases

```http
GET   /api/v1/cases
POST  /api/v1/cases
GET   /api/v1/cases/{id}
PATCH /api/v1/cases/{id}
GET   /api/v1/cases/{id}/timeline
GET   /api/v1/cases/{id}/records
POST  /api/v1/cases/{id}/records
POST  /api/v1/cases/{id}/relations
POST  /api/v1/cases/{id}/external-links
POST  /api/v1/cases/{id}/close
POST  /api/v1/cases/{id}/reopen
```

## 10.5 Records

```http
POST  /api/v1/records
GET   /api/v1/records/{id}
PATCH /api/v1/records/{id}
POST  /api/v1/records/{id}/review
POST  /api/v1/records/{id}/relations
```

## 10.6 Groups

```http
GET   /api/v1/groups?group_type=
POST  /api/v1/groups
GET   /api/v1/groups/{id}
PATCH /api/v1/groups/{id}
GET   /api/v1/groups/{id}/members
POST  /api/v1/groups/{id}/members
DELETE /api/v1/groups/{id}/members/{member_id}
GET   /api/v1/groups/{id}/digest-comparison
POST  /api/v1/groups/{id}/records
POST  /api/v1/groups/{id}/cases
```

## 10.7 Templates

```http
GET   /api/v1/templates
POST  /api/v1/templates
GET   /api/v1/templates/{id}
PATCH /api/v1/templates/{id}
```

## 10.8 Reports

```http
POST /api/v1/reports
GET  /api/v1/reports
GET  /api/v1/reports/{id}
GET  /api/v1/reports/{id}/items
```

---

## 11. 报告设计

## 11.1 Internal Weekly Report

统计窗口默认为自然周。

内容：

```text
本周 Finding 数
本周新 SQL Catalog Items 数
本周 recurring SQL 数
本周 reappeared SQL 数
新建 Cases 数
关闭 Cases 数
新增 Records 数
客户已同步数
客户待跟进数
观察中数
观察完成数
```

Top 列表：

```text
Top SQL by slow_count
Top SQL by total_latency
Top SQL by max_latency
Top recurring SQL
Top reappeared SQL
Top customer_pending Cases
Top observing Cases
```

## 11.2 Customer SQL Governance Report

只允许使用：

```text
visibility = customer_visible
review_status = reviewed
```

的 Record 和事件。

结构：

```text
1. 本期概览
2. 重点 SQL 列表
3. 每条 SQL 的影响摘要
4. 建议动作
5. 风险说明
6. 客户侧待办
7. 观察结果
8. 下一步计划
```

## 11.3 Snapshot 规则

报告生成后必须保存：

```text
report.snapshot_json
report_items.snapshot_json
```

报告打开时展示 snapshot，不实时重新计算历史内容。

---

## 12. 权限、可见性和安全

### 12.1 可见性

所有用户输入内容默认：

```text
visibility = internal
```

只有明确标记为：

```text
visibility = customer_visible
```

且 review 通过的 Record，才可进入客户报告。

### 12.2 SQL 脱敏

默认保存：

```text
normalized_sql
```

不默认保存带参数的原始 SQL。

如需保存样本 SQL，只能保存：

```text
sample_sql_redacted
```

### 12.3 权限边界

MVP 至少支持：

```text
org 级别访问控制
只读角色
编辑角色
管理员角色
```

跨 org 的 Relation 不允许由普通用户创建。

### 12.4 审计

以下行为必须写 Timeline Event：

```text
创建 Finding
创建 Catalog Item
创建 / 关闭 / 重开 Case
修改 Catalog Item status
创建 / 更新 Record
修改 Record visibility
生成报告
添加外部链接
添加 Group 成员
```

---

## 13. 实施顺序

## Phase 1：Catalog + Finding 主干

必须交付：

1. `findings` API。
2. `sql_catalog_items` 自动创建。
3. `relations`。
4. Catalog List。
5. Catalog Item Detail 基础页。
6. Timeline 基础能力。
7. Workbench 待分诊队列。

验收标准：

```text
提交一个 Finding 后，系统能幂等创建 Finding；
如果 SQL Catalog Item 不存在，自动创建；
Catalog Item 状态为 waiting_triage；
Catalog Item 页面能看到 Finding 事件和基础信息。
```

## Phase 2：Case + Record

必须交付：

1. Case 创建、关联、状态流转。
2. Record 创建和模板选择。
3. Catalog Item 与 Case 关联展示。
4. Case Timeline。
5. 客户可见 / 内部可见隔离。

验收标准：

```text
用户可以从 Catalog Item 创建 Case；
Case 自动关联 Catalog Item；
用户可以在 Case 中填写 Proposal Record；
Record 可标记 customer_visible；
Case 关键里程碑出现在 Catalog Item Timeline 中。
```

## Phase 3：Metrics + Groups

必须交付：

1. metric rollup。
2. DB distribution。
3. Plan digest distribution。
4. Cluster Group。
5. SQL Group。
6. Digest Comparison View。

验收标准：

```text
用户可以创建 Cluster Group；
Catalog 可按 Cluster Group 筛选；
Digest Comparison View 能展示同一 digest 在多个集群之间的表现；
用户可以创建 SQL Group 并添加多个 SQL Catalog Items，且 reason 必填。
```

## Phase 4：Reports + Notifications

必须交付：

1. Internal Weekly Report。
2. Customer SQL Governance Report。
3. Report Snapshot。
4. Observation due 提醒。
5. Customer pending 超时提醒。

验收标准：

```text
系统能生成某 org / Cluster Group / SQL Group 的周报；
客户报告只包含 customer_visible 且 reviewed 的内容；
历史报告打开时展示 snapshot。
```

---

## 14. 关键验收用例

### 用例 1：第一次发现 SQL

输入：一个 inspection Finding。  
期望：

```text
Finding 创建成功；
SQL Catalog Item 自动创建；
status = waiting_triage；
Finding 关联到 SQL Catalog Item；
Catalog Item Timeline 出现 catalog_item_created 和 finding_ingested。
```

### 用例 2：重复输入同一 Finding

输入：相同 source + source_event_id。  
期望：

```text
不重复创建 Finding；
不重复创建 Timeline Event；
返回已有 finding_id 和 catalog_item_id。
```

### 用例 3：同一 SQL 再次检出

输入：同 org + cluster + digest，新的 source_event_id。  
期望：

```text
创建新 Finding；
关联到已有 SQL Catalog Item；
更新 last_seen_at；
Timeline 增加 finding_ingested。
```

### 用例 4：从 Catalog Item 创建 Case

操作：用户点击 Create Case。  
期望：

```text
Case 创建；
Case 与 SQL Catalog Item 建立 handled_by_case relation；
SQL Catalog Item status 自动变为 in_governance；
Catalog Item Timeline 出现 case_created / case_linked。
```

### 用例 5：Case 治理结束

操作：Case closed / fixed。  
期望：

```text
Case state = closed；
resolution = fixed；
SQL Catalog Item status 从 in_governance 变为 watching；
Catalog Item Timeline 出现 case_closed。
```

### 用例 6：Cluster Group 横向对比

前置：cluster-a、cluster-b 属于同一个 Cluster Group。  
输入：两个 cluster 均有相同 digest 的 SQL Catalog Item。  
期望：

```text
Catalog 按 Cluster Group 筛选时出现两条 Item；
Digest Comparison View 按 digest 折叠；
展开后能看到 cluster-a 和 cluster-b 的指标差异。
```

### 用例 7：SQL Group 批量治理

操作：用户创建 SQL Group，把多个 SQL Catalog Items 加入，并填写 reason。  
期望：

```text
SQL Group 成员创建成功；
reason 必填；
Group Detail 能展示成员；
用户可以在 Group 上创建 Group-level Record 或 Case。
```

### 用例 8：客户报告可见性

前置：Case 中有 internal Record 和 customer_visible Record。  
操作：生成客户报告。  
期望：

```text
报告只包含 customer_visible 且 reviewed 的 Record；
internal 内容不出现在报告 snapshot 中。
```

---

## 15. 设计自检清单

实施时必须遵守以下约束：

1. 不创建 SQL Anchor、Catalog Scope 等额外概念。
2. SQL Catalog Item 唯一键必须是 `org_id + cluster_id + sql_digest`。
3. `project_id` 不进入 SQL Catalog Item 唯一键。
4. `db_name` 不进入 SQL Catalog Item 唯一键。
5. `plan_digest` 不进入 SQL Catalog Item 唯一键。
6. Finding 是外部标准输入，首次发现必须自动创建 SQL Catalog Item。
7. SQL Catalog Item 默认状态必须是 `waiting_triage`。
8. Case 不是必须存在；Catalog Item 可以只处于 watching / no_action_needed。
9. Case 和 Catalog Item 必须通过 Relation 关联，不写死一对一。
10. Finding、Case、Catalog Item、Record、Slow Log、Group、External Link、Report 之间必须支持灵活 Relation。
11. Recommendation 不作为核心实体实现，统一由 Record 表达。
12. Record 必须同时支持 Markdown 正文和结构化字段。
13. Cluster Group 只用于筛选和横向对比，不改变 Catalog Item identity。
14. SQL Group 用于人工治理分组，添加成员必须填写 reason。
15. Catalog Item Timeline 是主线；Case Timeline 是治理子线。
16. 客户报告只能使用 `visibility = customer_visible` 且 `review_status = reviewed` 的内容。
17. 系统不自动合并不同 digest 的 SQL。
18. 系统不自动 reopen Case，只提醒 owner。
19. Slow Log 是证据实体，MVP 不要求存储全量慢日志。
20. 所有关键状态变更都必须写 Timeline Event。
