# Clinic API 向 Data Proxy 迁移方案

## 文档信息

| 项目 | 值 |
| --- | --- |
| 状态 | Active（Metadata/Activity/Components staging Pass；Top RU staging Partial Pass） |
| 最后更新 | 2026-07-21 |
| Owner | TBD（迁移 DRI；提议 2026-07-24 前指派） |
| Required reviewers | Data Proxy backend、API Gateway/RBAC、Security、SRE/Deploy、Clinic UI/Investibook consumer owner（具体人员 TBD） |
| Phase 0 决策截止 | 提议 2026-07-24；Critical Phase 0e owner/处置方案提议 2026-07-20 前确认 |
| 设计文档 PR | [pingcap-inc/clinic#1572](https://github.com/pingcap-inc/clinic/pull/1572)（已合并） |
| 实现 PR | [pingcap-inc/clinic#1572](https://github.com/pingcap-inc/clinic/pull/1572)（历史 direct metadata 实现，已合并）、[#1591](https://github.com/pingcap-inc/clinic/pull/1591)（历史 direct Activity 实现，已合并）、[#1594](https://github.com/pingcap-inc/clinic/pull/1594)（Clinic facade，已合并）、[#1596](https://github.com/pingcap-inc/clinic/pull/1596)（Top RU，已合并） |
| 精简验收文档 | [Data Proxy API 迁移与验收状态](../data-proxy-api-migration-acceptance.md) |
| 关联仓库 | `clinic`、`clinic-ui`、`tidb-dashboard`、`investibook`、`nutshell-skills`、`app-delivery`/运行时 Gateway 配置仓库 |

状态拆分为六个维度，避免把“是否纳入”“契约是否批准”“代码是否完成”“环境是否验证”“caller 是否切流”和“旧路由是否退役”混在一起：

| 维度 | 可用值 | 含义 |
| --- | --- | --- |
| Scope | `Required` / `Candidate` / `Out of Scope` | 本轮必须迁移、已记录但暂缓且不进入当前 OpenAPI/DoD、或明确不迁移 |
| Contract | `Proposed` / `Approved` | path、参数和响应仍待评审，或已经冻结 |
| Delivery | `Deferred` / `Planned` / `In Progress` / `Implemented` | 候选暂缓、尚未开始、已有实现 PR、代码已完成 |
| Verification | `Not Run` / `Pass` / `Fail` / `Blocked` | 验证未执行、通过、失败或被外部条件阻塞 |
| Cutover | `Not Started` / `Canary` / `Complete` / `Rolled Back` | caller 尚未切换、灰度中、全部切换或已经回滚 |
| Legacy | `Active` / `Deprecated` / `Exempt` / `Removed` | 旧路由正常服务、已弃用、保留书面豁免或已删除 |

本文件保留详细设计、调查和排障记录；面向 reviewer 的 API 映射与环境验收状态统一维护在精简验收文档。每个实现 PR 必须同步更新两者；没有证据时不得把验证项标记为通过。

## 1. 背景

Data Proxy（代码目录为 `clinic_data_service`）正在成为 Clinic 面向外部调用方，尤其是 Investibook，提供数据访问能力的统一入口。后续认证、授权和审计也会在 Data Proxy 边界收敛。

当前部分能力仍通过 Clinic Server 或 NGM 暴露：调用方先查询 Cluster metadata，再把 `provider`、`region`、`projectID`、`deployType`、`tenantID`、甚至 Premium 的 parent/resource-pool ID 作为请求 Header 传给 NGM；NGM 再代理到 O11y internal diagnosis API 或直接访问对象存储。这有三个问题：

1. 路由元数据由调用方拼装，Header 可被伪造，难以形成清晰的租户隔离边界。
2. 不同能力需要不同的数据所有权边界：SQL/Top RU 等数据面能力适合由 Data Proxy provider/resolver 访问 O11y/OLAP；metadata、Activity、components 等 Clinic 控制面能力若在 Data Proxy 重写，会形成两套复杂业务逻辑并持续漂移。
3. Clinic internal org ID、TiDB Cloud org/tenant ID、逻辑 cluster ID、Premium parent ID 混在同一调用链中，调用方容易传错。

因此，本次迁移不是简单把旧 Handler 再注册一遍，而是把公开契约、鉴权作用域、Cluster context 解析和权威数据源访问一起收敛到 Data Proxy。

## 2. 目标与非目标

### 2.1 目标

- 在 Data Proxy 提供本文件列出的 Cluster metadata、Topology/components、Continuous Profiling、Plan Replayer/OOM、Activity Events、SQL Statements 和 Top RU API。
- 对外统一使用 versioned、cluster-scoped path；调用方不再传 placement/routing Header。
- `{orgID}` 与 `{clusterID}` 在 path 中参与鉴权和租户隔离，服务端校验两者关联关系。
- 按能力归属选择实现模式：数据面能力由 Data Proxy provider/resolver 访问 O11y/OLAP；Clinic-owned 控制面能力由 Clinic 暴露 canonical internal API，Data Proxy 只做外部契约、统一鉴权边界和安全转发。
- 新请求链不回调旧 Clinic public API 或 NGM compatibility API；允许调用为 Data Proxy 专门设计、仅在内部网络暴露的 Clinic canonical API。
- 第一阶段尽量保持旧成功响应语义，使旧、新接口可以进行自动化对比；明确记录所有有意差异。
- 为 JSON、分页、流式下载、短期 Token、多 tier/provider 路由建立可重复的验证矩阵。
- 迁移期间旧接口可回滚；完成调用方切换和观察期后再删除旧依赖。

### 2.2 非目标

- 本文既是迁移设计，也是实现期间的进度与验证台账；每批代码只实现 tracker 中明确进入 `In Progress` 的范围，不因规划完整性一次性扩大实现范围。
- 不在第一批同时重构已有 `/data-proxy/query`、`/data-proxy/schema`、metrics、serverless-parent metrics、Loki 和 system logs path；这些 path 的进一步收敛另立计划，但需做回归 smoke 防止新 Router 影响它们。
- 不把旧 Clinic Handler 当作新 Data Proxy 的运行时 fallback。旧、新对比只能发生在测试、影子请求或离线验证中。
- 不默认把 Clinic UI 中所有关闭的 feature、所有 ActivityHub 写接口、所有 TiDB Dashboard 可选接口纳入本轮；候选项单独列出并等待确认。

## 3. 已确认的现状

### 3.1 服务边界

- Data Service 是独立的嵌套 Go module：`clinic_data_service/go.mod`，不能把“直接 import 根模块 Handler”当作默认复用方案。
- Data Service 当前 Router 位于 `clinic_data_service/pkg/server/router.go`。现有公开 Data Proxy 路由包括 `/data-proxy/query|batch-query|schema|info`、`/data-proxy/metrics/{clusterID}/api/v1/...`、serverless-parent metrics、Loki/system logs。META-01/02/04 的 direct-DB 版本曾由 [#1572](https://github.com/pingcap-inc/clinic/pull/1572) 在 staging 验收，ACT-01 的 direct-ActivityHub 版本由 [#1591](https://github.com/pingcap-inc/clinic/pull/1591) 合并；二者现已进入 Clinic-owned facade 重构，旧 staging 结果只作为响应 parity 基线，不能替代重构后的 post-deploy E2E。
- “Data Proxy 能否访问 Clinic DB”需要按进程和数据源区分。Data Service 早已有 `database.mysql`、GORM 连接池、通用 DAO 和 `clusters` 单表查询，并被 metrics/query/S3 等旧流程使用；但 app-delivery 的 staging/prod 配置明确把这条连接标为 analysis results 数据库，实际指向 `gateway01...:4000` 的 `staging`/`slowquery` schema，而不是 Clinic control-plane RDS。该 legacy `clusters` 模型是 Data Service 自己的扁平 schema，没有 `organizations.dbaas_org_id` 联表和 canonical metadata 完整字段，不能直接作为本批 org-scoped API 的权威数据源。
- Clinic 根服务和 API Gateway 是另一组进程，已经连接 `mysql-clinic...:3306` 的 canonical Clinic DB。Open PR [#1571](https://github.com/pingcap-inc/clinic/pull/1571) 在 Gateway `dbaas_auth` 中复用根模块 DB model 校验 org/cluster，并在 Data Service 增加新的 query route；它没有给 Data Service 增加 metadata repository、`organizations` 查询、三条 metadata GET route 或部署配置。因此准确结论不是“以前整个 Data Proxy 都无法访问 Clinic DB”，而是“Data Service 以前没有本批所需的 canonical metadata 数据源与 API；Gateway 的 DB 校验也不能替代 API payload 查询”。
- 固定版本 Data Proxy Skill 的 caller contract 是 `${CLINIC_ENDPOINT}/data-proxy/...` + `Authorization: Bearer ${CLINIC_API_KEY}`。该 key 当前按 internal access key 使用，dev/staging/prod endpoint 与 key 必须匹配；新 resource API 首批以同一 caller contract 验证。
- `clinic_data_service/pkg/server/middleware.go` 当前只有 recovery、tracing 和统一错误处理，没有认证/授权 middleware。
- 当前认证链实际在 API Gateway：API key 优先，其次 session，然后 RBAC；成功后把用户上下文注入下游 Header。新 path 必须同步更新 Gateway allow-list、resource mapping 和部署路由。
- 当前 app-delivery 环境已用 `/data-proxy` Prefix Ingress 转发到 Data Service，因此新 namespace 预期无需新增 Ingress path；仍需验证 Prefix route、Kong plugin、credential policy、RBAC resource map 及其部署 secret/config 证据。
- Data Service 当前已通过 `clinic_data_service/pkg/clinic-internal-api/client.go` 调用 Clinic internal server。该内部 service boundary 可以复用，但新 resource Handler 必须调用固定、typed、为 Data Proxy 设计的 canonical path；不能复用返回 raw URL bag 的 `/internal/clusters/{clusterID}/cluster-dataurls` 来实现 metadata、Activity 或 components。
- Data Service 自带的 O11y client 能力弱于根模块 resolver，并且对多条 cluster 结果的选择逻辑不完整。迁移前需要形成一个 Data Service 自己拥有、覆盖所有 tier 的 O11y target resolver。
- Data Service 的 Swagger 仍不是完整 API inventory。首次为本批重新生成时会顺带带出历史 metrics `series`，但该注释把 repeated `match[]` 错写成 CSV 且遗漏 runtime POST；为避免把无关且错误的契约混入 metadata PR，生成物和 Makefile 接线已从本批移除，留到 Phase 0 的独立 OpenAPI baseline 修复。根 CI 也仍未可靠覆盖嵌套 module 的 Go tests。

### 3.2 ID 语义

当前存在三套易混淆的 ID：

| 名称 | 当前语义 | 新契约建议 |
| --- | --- | --- |
| Clinic internal org ID | `/clinic/api/v1/orgs/{oid}` 使用的 Clinic DB organization 主键 | 不再暴露给新调用方 |
| TiDB Cloud org/tenant ID | NGM `x-org-id`、Cluster list 中的 `tenantID` | 已确认作为新 path 的 `{orgID}` |
| Logical cluster ID | 用户可见的 TiDB Cloud cluster/application ID | 新 path 的 `{clusterID}`，也是授权对象 |
| Effective data target ID | Premium ConProf 可能使用 parent/resource-pool ID；不同 tier 还有不同 O11y cluster ID | 仅由服务端 resolver 派生，不出现在公开 path/Header 中 |

已确认新 path 的 `{orgID}` 定义为 TiDB Cloud/DBAAS org ID。Data Proxy 必须用 `(orgID, clusterID)` 联合查询或等价方式校验归属，不能只按 cluster ID 查询后相信 caller。

### 3.3 当前调用方

- Investibook 至少有三条独立调用链：source preparer、标准 diagnosis sweep collector、targeted/ad-hoc `clinic-cli`。它们分别直接消费 Cluster list/detail、Activity、components、Continuous Profiling、Plan Replayer/OOM，并使用 internal API key；不能只更新 CLI 或 Skill 文档。
- Investibook 目前没有直接消费 `/statements/*` 或 `/topru/*`；其 SQL/TopSQL 证据主要走通用 `/data-proxy/query`。
- Clinic UI 的 SQL Repository 和嵌入式 SQL Statements 页面消费 Statements API。
- Clinic UI 的 Premium/Essential Top RU 页面实际消费 `summary`、`summaryplan` 和 `user_stats`；列表按 SQL digest 查询 summary，打开 SQL 详情时按需查询 plan breakdown。`config` 已定义但当前没有 live caller。
- Clinic UI 的 file 页面使用 list 响应里的 `download_url`，并不会自己选择 Plan/OOM 下载 path。
- Clinic UI/Investibook 当前都需要先拼装 NGM placement Header；Premium Continuous Profiling 还会把 logical cluster 映射为 parent/resource-pool target。

## 4. 目标架构

```mermaid
flowchart LR
    C["Investibook / Clinic UI"] --> G["Data Proxy edge: authentication + RBAC"]
    G --> H["Data Service versioned handlers"]
    H --> F["Clinic-owned facade client\nfixed internal paths, typed DTOs"]
    F --> I["Clinic internal server\nsingle business owner"]
    I --> M["Clinic metadata DB"]
    I --> A["ActivityHub"]
    I --> P["DBAAS Core / Mgmt / Resource Pool"]
    H --> R["Data Proxy provider / resolver"]
    R --> O["O11y internal APIs / OLAP"]
    R --> S["Object storage / query engines"]
```

API 按数据与业务逻辑归属分为三类，不能用同一种迁移方式机械实现：

1. **Clinic-owned facade**：metadata、Activity、components 的 ownership、数据源选择、兼容逻辑留在 Clinic；Clinic internal server 暴露固定 canonical API，Data Proxy 只负责公开 path、边缘鉴权契约、参数边界、状态映射和观测。
2. **Data-plane provider/resolver**：SQL Statements、Top RU 等优先复用 Data Proxy 既有 provider 模型，以 capability-specific O11y/OLAP 接口或查询引擎实现，不回调 Clinic 业务 API。
3. **Data Proxy-owned orchestration**：只有当能力天然跨数据源且不属于 Clinic 既有业务逻辑时，才在 Data Proxy 实现 resolver、token/download 等编排；必须先记录选择依据，避免复制复杂逻辑。

### 4.1 ClusterContext

所有 cluster-scoped Handler 先解析同一个服务端上下文，至少包含：

```text
cloudOrgID
logicalClusterID
clinicOrgID (仅内部兼容时需要)
provider / region / projectID
deployType / deployTypeV2
parentID / shootName / keyspaceID
status / deleted state
feature capabilities
```

约束：

- Cache key 必须包含 `orgID:clusterID`；不能只按 cluster ID 缓存授权结果。
- Cache 只缓存 metadata/target，不缓存 caller permission。
- Phase 0 冻结 TTL 和主动失效来源，至少覆盖 cluster 转 org、删除/恢复、parent/resource-pool 变化和 O11y placement 变化；授权所依赖的 org/cluster ownership 查询不得在 refresh 失败时用 stale-on-error 继续放行旧 owner。
- caller 提供的 `x-provider`、`x-region`、`x-project-id`、`x-deploy-type`、`x-cluster-id` 不参与目标选择；若为兼容临时接收，也只能忽略并记录 deprecated metric。
- logical cluster 是授权对象，effective target ID 只是下游路由结果。例如 Premium ConProf 即使读取 parent/resource-pool，仍先校验 caller 对 logical child cluster 的权限。
- Data Proxy 不调用旧 `/clinic/api/v1/...` public route 或 `/ngm/api/v1/...` compatibility route；Clinic-owned 能力只调用 `/clinic/api/v1/internal/data-proxy/v1/...` canonical route。

### 4.2 代码复用原则

Data Service 是独立 Go module，不能直接 import 根模块业务包。因此复用边界是内部 HTTP contract，而不是复制 Handler：

1. Clinic 根模块抽出 legacy/internal Handler 共用的 typed service/core；数据源访问、ownership 和 deploy-type 分派只维护一份。
2. Clinic internal server 注册 `/clinic/api/v1/internal/data-proxy/v1/...`，使用 DBAAS org ID；这些路由不经公网 Ingress，也不接受 caller credential。
3. Data Proxy 使用小型 typed client；只构造固定 GET path/query，不透传 caller `Authorization`、Cookie、CSRF、placement 或 legacy cursor Header，不跟随 redirect，不向外泄露内部错误 body。
4. Data Proxy 可以保留 `Repository`/client interface 以便 handler unit test，但实现只做 transport DTO 映射，不能再次包含 SQL、ActivityHub 协议或 component deploy-type 业务逻辑。
5. 长期由两个进程共同使用且不能自然归属于 Clinic 的纯算法，才考虑抽 shared module；本批不为此新增通用框架。

### 4.3 数据源选择优先级

实现优先级如下：

1. SQL Statements、Top RU 等 O11y/OLAP 数据面能力优先通过 Data Proxy capability-specific provider/resolver 实现；不把通用 raw URL bag 暴露给 Handler。
2. 既有复杂逻辑明确属于 Clinic 的能力，优先在 Clinic 形成 legacy/internal 共用 core，再由 Data Proxy 调 canonical internal API。
3. 只有 O11y 与 Clinic 都没有合适 owner、且能力确需跨对象存储或其他数据源编排时，才由 Data Proxy 直接实现；先完成 connectivity、credential、timeout/SLO 和最小权限设计。
4. 任何模式都不使用 caller 提供的 placement Header 选择目标，也不通过旧 Clinic public/NGM API fallback。

#### 第一批 facade 重构决策

META-01/02/04、ACT-01、TOPO-01 统一采用 Clinic-owned facade：

- Clinic internal server 是 facade owner，但 #1594 不新增 Metadata SQL/repository：META-01 直接适配迁移前已有的 `datasource.ListOrganizationAndCluster`，META-02/04 通过 `datasource.GetOrganizationByCloudID` 解析 DBAAS org 后分别复用 `datasource.ListClusters`/`datasource.GetCluster`。查询、排序、deleted/filter/pagination 和 ownership 语义继续由这些既有 datasource core 负责；internal 层只做参数、DTO 和错误状态适配。ActivityHub 协议与 empty-404 兼容、components 的 deploy-type/data-source 分派同样留在 Clinic 既有 core。
- Data Proxy 保持已发布的 public path/response contract，通过 `clinic_api_base` 调用固定 internal path。Data Proxy 不再配置 Clinic metadata DB 或 ActivityHub，也不再持有相应 credential。
- 复用现有 `clinic-internal-api` ClusterIP Service 和 `clinic_api_base: http://clinic-internal-api:80`；本批不新增 Ingress、Service、IAM、DB account 或 Secret。
- 历史 `cluster_metadata` 与 Data Service `activity_hub` 配置在新代码中不再读取；app-delivery 可另行移除这些冗余 section，但这只是可选清理，不是 facade 部署或验收前置。Secret 中已存在的旧 key 可在回滚观察窗后单独清理，不把 Secret 删除与代码合并绑定。
- internal path 仅在集群内暴露；外部 caller 仍只访问 `/data-proxy/api/v1/...` 并由 Gateway 鉴权。Clinic ownership 校验是 defense in depth，不替代 Gateway 的 caller permission。

## 5. API 设计原则

### 5.1 统一前缀与作用域

已确认公共服务根前缀沿用现有 Data Proxy Skill、Ingress 和 Router 契约：

```text
/data-proxy
```

契约基线是 `nutshell-skills` commit `443ac49324e52cc60d7f4eb9f9768ef5dbb954d1` 的 `skills/platform/clinic-api/data-proxy/SUBSKILL.md`：它统一描述 `${CLINIC_ENDPOINT}/data-proxy/...`，当前稳定接口包括 `/data-proxy/query`、`/data-proxy/schema` 和 `/data-proxy/serverless-parent-metrics/api/v1/...`。现有路径没有统一版本位置，因此保持原样，不在本轮重构。

本次新增的 first-party Clinic resource API 使用新的版本化 namespace：

```text
/data-proxy/api/v1
```

这是本次设计选择，不是对旧 Data Proxy 路径规范的描述。它仍符合 Skill 的 `/data-proxy/...` 公共根前缀，并与 Clinic/NGM 的 service-prefix + API-version 结构一致；Prometheus/Loki 等协议兼容 API 继续保留各自既有的 `/api/v1` 位置。

两种作用域：

```text
/data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/...
/data-proxy/api/v1/clusters/{clusterID}/...
```

- org-scoped path 是默认公开契约，适合普通租户授权。
- 不含 org 的 cluster path 只用于 internal/global credential 的精确发现类接口，必须有独立的 global read permission。
- `projectID` 不放在公开 path，也不由 caller Header 提供；由 ClusterContext 派生。
- path 使用 kebab-case；query 参数第一版尽量保持旧名，降低响应/参数同时变化的风险。
- 本文用 `{orgID}`、`{clusterID}` 表达业务概念；OpenAPI parameter name 使用 lowerCamelCase `orgId`、`clusterId`。cluster ID 按 opaque string 校验，只限制非空、最大长度和禁止 `/`/控制字符，不能复用现有 Data Proxy “只允许纯数字”的校验。
- 新 Data Proxy-native JSON 字段使用 lowerCamelCase；协议兼容或旧 query 参数按各 endpoint 原契约保留 snake_case。

### 5.2 Cluster list 的重新设计

旧 `/dashboard/clusters` 同时承担跨 org 搜索、精确 cluster 查询和分页列表。新接口按使用场景拆开：

| 使用场景 | 建议新接口 | 权限 |
| --- | --- | --- |
| 已知 cluster ID，Investibook 需要发现完整 context | `GET /data-proxy/api/v1/clusters/{clusterID}` | internal/global only |
| 已知 TiDB Cloud org，列出该 org clusters | `GET /data-proxy/api/v1/orgs/{orgID}/clusters` | org cluster read |
| 跨 org fuzzy search/运营看板 | `GET /data-proxy/api/v1/clusters?query=...` | internal/global only；Candidate，暂不实现 |

这样可以避免普通调用方为了获得 path 中的 org ID，先依赖一个暴露所有租户数据的 dashboard API。

### 5.3 Header、分页与错误

- 首批 caller contract 明确为 `Authorization: Bearer <internal CLINIC_API_KEY>`，并保留标准 tracing/content Header。Gateway 内部如何验证 key、注入 user context 和执行 RBAC 是服务端实现细节，不能要求 caller 传内部身份 Header。
- caller 提供的 placement/routing selector 无论位于 Header、query 还是 JSON body，都不能参与新 Handler 的 target 选择，包括 `x-provider`、`x-region`、`x-project-id`、`x-deploy-type`、`x-cluster-id`、`control_plane_info` 和 `options.dataSource/vendor/region`。
- Activity 新接口只接受 query `cursor`；legacy `cursor` Header 被忽略，query/Header 同时存在时也只有 query 生效。ACT-01 的所有 scalar query 参数最多只能出现一次。
- collection encoding 按 endpoint 冻结，不能制定全局规则：Statements 的 `stmt_types`、`plans` 使用 repeated query；已有 Schema 的 `tables` 保持 comma-separated；Prometheus 的 `match[]` 保持 repeated protocol parameter。
- Top RU 的空 `user=` 与“不传 user”语义不同，不能在参数清理时合并。
- 第一阶段成功响应默认保持旧 body shape；不额外套统一 success envelope。
- 仅本次新增 `/data-proxy/api/v1` resource API 的 Error 统一为 `code`、`message`、`requestId` 和可选 `details`，并正确映射 400/401/403/404/409/429/502/504。已有 `/data-proxy/query` 的 `{"error":"..."}` 等 legacy shape 不在本轮改变。为避免泄露，错误 org + cluster 组合对普通 caller 可统一返回 404。
- `requestId` 与响应 `X-Trace-ID` 使用同一个 canonical trace ID，日志、metrics exemplar 和上游 request 均关联该值，不生成第二套无法互查的 ID。
- 上游请求和响应 Header 都使用 allow-list。响应侧必须剥离 `Set-Cookie` 和 hop-by-hop Header，不能把上游内部认证/路由信息带回 caller。
- metadata/resolver 返回的 upstream URL 必须校验 scheme、host、port 和允许的 path prefix，不能把任意 metadata URL 当作代理目标。

### 5.4 下载与 Token

- Plan/OOM 下载不再接受 caller 提供的 `provider`、`region` 和裸 object key 作为授权依据。
- list 响应保留 lowerCamelCase `downloadUrl`，但 URL 指向 cluster-scoped Data Proxy path，并携带短期 opaque handle。本文推荐随机 handle + 服务端状态，而不是把 object key 放进可解码的自包含 Token。
- 服务端 Token 记录至少包含：version、purpose/operation、org、logical cluster、artifact category、target locator（仅服务端保存）、caller/credential binding、`issued_at/not_before/expires_at` 和 replay/download-count policy。Plan、OOM、top-memory、profile view、profile download 使用不同 purpose，不能互换。
- `target locator` 是 tagged union：direct storage 保存 provider/region/bucket/canonical key/object version；O11y 保存 effective target fingerprint 和加密后的 upstream token，或保存已经 canonicalize、可由内部接口直接执行的 typed locator。上游 token 不返回 caller，必须加密存储；handle expiry 不晚于 upstream token expiry。
- 签发时记录 ClusterContext/resolver revision。消费时重新校验 logical org/cluster ownership；若 current effective target fingerprint 与签发时不同，则拒绝并要求 caller 重新签发，不能静默读取旧 placement，也不能在授权查询失败时使用 stale locator。
- Phase 0 冻结最大 TTL、是否一次性、允许的并发/断点续传、caller binding、吊销和 key/record 轮换策略。数据库/缓存只保存 handle hash；URL 和响应不得暴露 canonical object key。
- 下载时重新校验 handle、caller、purpose 与 path context，并校验 object key 位于解析出的 cluster/category canonical prefix 下；文件名净化后才能写入 `Content-Disposition`。
- Continuous Profiling 新 action-token 不直接签 caller 提供的 raw `q`。提议改为 `POST .../action-tokens` + typed JSON：`purpose` 必须是 `view` 或 `download`，`profile` 包含 `ts/profileType/component/address/dataFormat`，`outputType` 只允许用于支持转换的 view target。服务端校验字段长度/枚举，并结合 ClusterContext/profile inventory 生成 canonical artifact identity；caller 若同时需要 view 和 download，分别请求两个 purpose handle。若为兼容暂时接收 `q`，也必须 decode、校验、canonicalize，不能把原字符串作为授权 claim。
- Continuous Profiling 的 action-token、download、single-profile view 必须成对切换。旧 token 使用代码内固定 HMAC key且没有可信 `iat`，任何人都能持续伪造任意 expiry，因此不能靠“停止签发后等待 24 小时”自然排空：最安全的方案是在 T0 立即关闭旧免鉴权 download/view，caller 重新获取新 Token；若业务必须保留旧链接，须先在 Phase 0 给旧 download/view 增加正常认证、cluster permission 和固定服务端 cutoff（`exp <= cutoff + 24h`），最多保留一个 TTL。
- Token query 必须在 Gateway/Ingress access log、APM span URL 和应用日志全链路脱敏；相关响应统一设置 `Cache-Control: no-store, private` 和 `Referrer-Policy: no-referrer`。不记录 Token、object key 全文、SQL 文本或 profile bytes；日志只记录脱敏后的 artifact ID、cluster、trace ID、状态和字节数。

## 6. API 迁移总表

以下 path 均为提议，需在 Phase 0 review 后冻结。`orgID` 表示 TiDB Cloud/DBAAS org ID；旧 path 中的 `clinicOrgID` 表示 Clinic internal org ID。

### 6.1 Cluster metadata、Topology 和 Activity

| ID | 能力 | 旧访问 | 建议新访问 | 主要参数/响应策略 | Phase | Scope | Contract |
| --- | --- | --- | --- | --- | --- | --- | --- |
| META-01 | 精确 Cluster metadata | `GET /clinic/api/v1/dashboard/clusters?cluster_id={clusterID}` | `GET /data-proxy/api/v1/clusters/{clusterID}` | Clinic-owned facade；internal/global；返回单个 canonical metadata，而不是 dashboard list envelope | 1 | Required | Approved |
| META-02 | Org cluster list | `GET /clinic/api/v1/dashboard/clusters` 或 `/orgs/{clinicOrgID}/clusters` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters` | Clinic-owned facade；保留分页、filter | 1 | Required | Approved |
| META-03 | 跨 org cluster search | `GET /clinic/api/v1/dashboard/clusters` | `GET /data-proxy/api/v1/clusters` | internal only；仅记录候选，本轮暂不实现 | Deferred | Candidate | Proposed |
| META-04 | Cluster detail | `GET /clinic/api/v1/orgs/{clinicOrgID}/clusters/{clusterID}` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}` | Clinic-owned facade；无 routing Header；Investibook-minimal DTO | 1 | Required | Approved |
| TOPO-01 | Components/topology | `GET /clinic/api/v1/orgs/{clinicOrgID}/clusters/{clusterID}/resource-pool/components` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/components` | Clinic-owned facade；保持 component array；不保留非浏览器请求伪造 TiCDC component 的旧行为 | 1 | Required | Approved |
| ACT-01 | Activity list | `GET /clinic/api/v1/activityhub/applications/{clinicOrgID}/targets/{clusterID}/activities` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/activities` | Clinic-owned facade；`begin_ts,end_ts` required；保留 snake_case item/filters；cursor 仅用 query | 1 | Required | Approved |

### 6.2 Continuous Profiling

| ID | 能力 | 旧访问 | 建议新访问 | 参数/兼容边界 | Phase | Scope | Contract |
| --- | --- | --- | --- | --- | --- | --- | --- |
| CP-01 | Group list | `GET /ngm/api/v1/continuous_profiling/group_profiles` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/continuous-profiling/group-profiles` | `begin_time,end_time`；保持 list body | 5a | Required | Proposed |
| CP-02 | Group detail | `GET /ngm/api/v1/continuous_profiling/group_profile/detail?ts=` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/continuous-profiling/group-profiles/{ts}` | 保持 detail body | 5a | Required | Proposed |
| CP-03 | Action token | `GET /ngm/api/v1/continuous_profiling/action_token?q=` | `POST /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/continuous-profiling/action-tokens` | body：`{purpose,profile:{ts,profileType,component,address,dataFormat},outputType?}`；每个 purpose 单独返回 `{token,expiresAt}`；`no-store, private`；有意 breaking change | 5b | Required | Proposed |
| CP-04 | Download | `GET /ngm/api/v1/continuous_profiling/download?token=` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/continuous-profiling/download?token=` | 流式 bytes、ZIP 和旧稳定 Header；checksum/ETag/Range 仅在明确作为新增能力后承诺 | 5b | Required | Proposed |
| CP-05 | Single profile view | `GET /ngm/api/v1/continuous_profiling/single_profile/view?token=&output_type=` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/continuous-profiling/single-profile/view?token=` | `outputType` 在 CP-03 view handle 签发时冻结，不能在消费时改写；仅部分 O11y target 支持转换，direct/local 使用 `dataFormat`；有意 breaking change | 5b | Required | Proposed |
| CP-06 | Config | 当前 NGM 未注册，但 embedded page 会请求 `/continuous_profiling/config` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/continuous-profiling/config` | 仅记录候选，本轮暂不实现且不进入 OpenAPI | Deferred | Candidate | Proposed |

用户原始列表中的 `/ngm/api/v1//...` 双斜杠按 typo 处理；真实旧 path 是单斜杠。

### 6.3 Plan Replayer 和 OOM

| ID | 能力 | 旧访问 | 建议新访问 | 参数/兼容边界 | Phase | Scope | Contract |
| --- | --- | --- | --- | --- | --- | --- | --- |
| FILE-PR-01 | Plan Replayer list | `GET /ngm/api/v1/plan_replayer/list` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/plan-replayer/files` | `start,end,instances?`；重写每个 `downloadUrl`；裸 `key/downloadPath` 字段处理待冻结 | 4a | Required | Proposed |
| FILE-PR-02 | Plan Replayer download | `GET /ngm/api/v1/plan_replayer/files?provider=&region=&key=` 已注册，但当前 list 实际返回 `/oom_record/files` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/plan-replayer/files/download?token=` | 必须修复 artifact type/path；无裸 provider/region/key | 4b | Required | Proposed |
| FILE-OOM-01 | OOM list | `GET /ngm/api/v1/oom_record/list` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/oom-records/files` | `start,end,instances?`；records/files 主体兼容，但裸 storage 字段处理待冻结 | 4a | Required | Proposed |
| FILE-OOM-02 | OOM download | `GET /ngm/api/v1/oom_record/files?provider=&region=&key=` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/oom-records/files/download?token=` | 流式下载；scope/prefix/checksum negative tests | 4b | Required | Proposed |
| FILE-OOM-03 | OOM top memory | `GET /ngm/api/v1/oom_record/top10_mem_usage?provider=&region=&key=` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/oom-records/files/top-memory-usage?token=` | 仅记录候选，本轮暂不实现且不进入 OpenAPI | Deferred | Candidate | Proposed |

旧 list schema 中可能包含裸 `key`、`download_path` 或带 provider/region 的旧 URL，这与 opaque handle 目标冲突。Phase 0 必须逐字段盘点 consumer：默认新 schema 删除裸 storage layout，只保留稳定 artifact metadata 和新 `downloadUrl`；若某字段必须暂时兼容，则标记 deprecated、脱敏且不得作为下载授权依据。

### 6.4 SQL Statements

| ID | 能力 | 旧访问 | 建议新访问 | 主要参数 | Phase | Scope | Contract |
| --- | --- | --- | --- | --- | --- | --- | --- |
| STMT-SQL | Premium persisted Statements 灵活 SQL 查询 | 无一对一旧 API | `POST /data-proxy/query`，逻辑表 `clinic_data_proxy.statements` | `clusterId`、SQL 中必填 `date = 'YYYY-MM-DD'`；source/provider/region 由服务端解析 | 6 | Required | Implemented / deploy + prod E2E Pending |
| STMT-01 | Available fields | `GET /ngm/api/v1/statements/available_fields` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/statements/available-fields` | 无 query | 6 | Required | Proposed |
| STMT-02 | Statement types | `GET /ngm/api/v1/statements/stmt_types` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/statements/statement-types` | 无 query | 6 | Required | Proposed |
| STMT-03 | Statement list | `GET /ngm/api/v1/statements/list` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/statements` | `begin_time,end_time,schemas[],stmt_types[],text,fields`；`resource_groups[]` 仅旧 proxy target 可能生效，待 Phase 0 决定是否成为统一契约 | 6 | Required | Proposed |
| STMT-04 | Plans | `GET /ngm/api/v1/statements/plans` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/statements/plans` | `begin_time,end_time,digest,schema_name?` | 6 | Required | Proposed |
| STMT-05 | Plan detail | `GET /ngm/api/v1/statements/plan/detail` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/statements/plan-detail` | `begin_time,end_time,digest,plans[],schema_name?` | 6 | Required | Proposed |

Statements 还有 legacy companion mount：`/api/v1/orgs/{org}/projects/{project}/clusters/{cluster}/ngm/statements/*`。即使最终无 live caller，也必须加入旧 QPS 审计，并在 Phase 0 明确标记为迁移、alias 还是 Out of Scope，不能静默遗漏。

`/statements/time_ranges` 会被一个 embedded controller 探测，但当前后端不存在且 caller 明确兼容 404；`config`、export/download 和 plan binding 等接口处于关闭、可选或 adapter 未实现状态，本轮默认不纳入。`resource_groups[]` 当前 local 实现忽略、proxy target 可能透传；是否把它纳入统一新契约单独决策。

旧 Swagger 把 `digest`、`plans[]` 标为 required，但 runtime 可以缺失；新契约若收紧必须记录为有意差异并验证所有 caller。`available_fields` 的 local 返回顺序来自 Go map，parity test 比较集合；若新 API 固定排序，也记录为有意差异。

### 6.5 Top RU

| ID | 能力 | 旧访问 | 建议新访问 | 主要参数 | Phase | Scope | Contract |
| --- | --- | --- | --- | --- | --- | --- | --- |
| TOPRU-01 | Config | `GET /ngm/api/v1/topru/config` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/top-ru/config` | 当前 UI 无 live caller；仅记录候选，本轮暂不实现 | Deferred | Candidate | Proposed |
| TOPRU-02 | Summary | `GET /ngm/api/v1/topru/summary` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/top-ru/summary` | `start,end,top` 必填；`order_by,user,data_source,step` 可选 | 2 | Required | Approved |
| TOPRU-04 | Summary by plan | `GET /ngm/api/v1/topru/summaryplan` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/top-ru/summary-plan` | `start,end,top` 必填；`digest,order_by,user,data_source,step` 可选 | 2 | Required | Approved |
| TOPRU-03 | User stats | `GET /ngm/api/v1/topru/user_stats` | `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/top-ru/user-stats` | `start,end` 必填；`top` 可选 | 2 | Required | Approved |

本批仅 plain logical Premium 与 plain logical Essential 使用这组 API；Resource Pool（包括 Premium/Essential resource-pool 变体）、Dedicated、Starter 和 BYOC 均不在本批支持或验收范围。当前 UI policy 是 plain Premium 最长 30 天、Top 100；plain Essential 最长 7 天、Top 20；默认最近 1 小时、Top 10。服务端和 O11y 是否也强制相同限制，需要在契约测试中确认。

### 6.6 邻接接口候选

以下接口确实存在或被完整页面使用，但不在用户给出的最小列表中。若目标是“迁移 Investibook 当前能力”，可后置；若目标是“迁移整个 Clinic 页面”，需要另行纳入：

- Activity detail：`GET .../activities/{activity_id}`。
- Activity group list/detail：`GET .../activitygroups`、`GET .../activitygroups/{context_id}`。
- ActivityHub file view/download。
- Cluster dashboard `clusters2` contract。
- Statement config/export/binding/resource-group 等当前关闭或条件能力。

## 7. 各领域迁移策略

### 7.1 Cluster metadata

当前 Cluster detail 混合了纯 metadata、Topology、Jira/on-call count、quota/data size、feature gates 和部分 realtime Mgmt 查询。直接复制整个 Handler 会把大量 Clinic 业务依赖带入 Data Service。

根据 Investibook 静态盘点，本轮采用 **Investibook-minimal** 作为工作契约，最终由 consumer contract tests 批准；不复制 Jira/on-call、quota/data size、feature gates、`ruVersion` 或 `controlPlaneArch`。Data Proxy-native JSON 使用 lowerCamelCase：

```json
{
  "clusterId": "...",
  "orgId": "...",
  "name": "...",
  "status": "...",
  "deleted": false,
  "version": "...",
  "provider": "aws",
  "region": "us-west-2",
  "deployType": "...",
  "deployTypeV2": "...",
  "createdAt": "2026-07-17T08:00:01Z",
  "diagnosticContext": {
    "projectId": "...",
    "parentId": "...",
    "shootName": "...",
    "keyspaceId": "..."
  }
}
```

字段边界：

- `orgId` 是 TiDB Cloud/DBAAS org ID，也是 path 授权对象；不再向 caller 暴露 Clinic internal org ID。Investibook adapter 把它映射到现有本地 `tenant_id` 语义。
- `createdAt` 由 `clusters.created_at` 读取并规范化为 UTC RFC 3339；Investibook source preparer 把它映射到现有 `cluster_meta.json` 的 `create_timestamp`，因此它属于第一批消费字段，不是纯展示 enrichment。
- `projectId` 是分阶段迁移旧 NGM consumer 的过渡字段；CP/files 全部切到新 API 后再评估删除。
- `parentId` 在 Premium metrics 仍由 caller 选择 parent scope 的期间是硬依赖；metrics 也改成 logical-cluster 服务端路由后再删除。
- `shootName` 只作为 Starter/Essential 诊断上下文；`keyspaceId` 若 metadata 权威源没有提供，继续由 metrics series 发现。两者均不是授权或新 Handler 的 routing 输入。
- `components` 继续由 TOPO-01 返回，不混入 detail。
- `tenantName` 等展示字段可选，不进入 Required minimum；旧 Clinic enrichment 继续只由旧 endpoint 提供。

Investibook 至少有三条独立调用链要做 contract test：source preparer、标准 diagnosis sweep collector、targeted/ad-hoc `clinic-cli`。其 `cluster_meta.json` 当前有固定本地字段集合，consumer PR 必须同步更新 adapter/schema/tests，不能只改 CLI。source preparer 还需用服务端 canonical `orgId` 校验 Jira tenant，mismatch 时失败或显式告警。

Org-scoped internal adapter 必须先用 TiDB Cloud org ID 解析 Clinic organization，再通过既有 datasource 用 org + cluster ID 查询，并对返回的 cluster ID、Clinic org ID 和真实 `TenantID` 做 fail-closed 归属校验；这能覆盖既有 detail datasource 在 DB miss 后的 realtime fallback，不能为此新增 SQL/repository。global exact 还要校验 dashboard row 的 Clinic org 与 cloud/DBAAS org 映射，只能通过 internal/global 权限访问。

当前 facade 状态（Metadata adapter 的既有 datasource 修正、focused tests/build、staging 部署和 live 验收均已完成；prod 与 consumer cutover 待执行）：

- META-01：`GET /data-proxy/api/v1/clusters/{clusterID}`，按 opaque cluster ID 精确返回单个 canonical metadata；staging 已按 internal/global Clinic key 完成验收，面向普通 credential 开放前仍需冻结 Gateway/RBAC policy。
- META-02：`GET /data-proxy/api/v1/orgs/{orgID}/clusters`，支持 1-based `page`、`pageSize`（默认 50，最大 200）、`query` 和 `showDeleted`；返回 `{items,page,pageSize,total}`。`query` 遵循既有 `clustersearch` policy（允许文本搜索时匹配 cluster ID/name，否则按既有受限策略处理），排序沿用 `updated_at DESC, id DESC`，`showDeleted=false` 映射为按 status 隐藏 `deleted`/`deletecomplete`/`cleared`，不另造 deleted/filter/pagination 语义；consumer contract 和真实数据仍需验证。
- META-04：`GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}`，Clinic internal API 解析 DBAAS org 后复用既有 detail datasource，并校验返回的 cluster ID、Clinic org ID 与真实 tenant ID；归属不匹配或 dashboard 也不存在时按 not found，已知 dashboard cluster 的 detail datasource 故障保留 5xx，避免把 DB/Mgmt outage 伪装成 404。
- 三个 Data Proxy public path/DTO 不变；Data Proxy repository 已替换为 Clinic typed client adapter。Clinic internal adapter 只把 public contract 的参数/DTO/状态映射到迁移前已有的 `ListOrganizationAndCluster`、`GetOrganizationByCloudID` + `ListClusters`/`GetCluster`，不复制这些 datasource 的 SQL 或业务语义。
- Data Proxy 删除独立 Clinic metadata MySQL repository/config；部署只依赖已有 `clinic_api_base` 与 `clinic-internal-api` Service。历史 staging direct-DB parity 作为响应基线保留；2026-07-21 已在重构后的正式 staging facade 上重新完成 public E2E，prod 仍待验收。

### 7.2 Activity Events

旧 `application_id` 在 list Handler 中实际没有参与上游查询。新 path 删除这个伪作用域；Data Proxy 把 DBAAS org/cluster 与 query 发送给 Clinic canonical internal API，由 Clinic 校验 ownership 并使用自身 ActivityHub credential 查询。

ACT-01 冻结契约如下：

- `begin_ts`、`end_ts` 是必填 Unix seconds，必须是非负整数且 `begin_ts <= end_ts`；为保持旧接口时间窗能力，首批不静默增加最大窗口。
- 可选 query 为 `catalog`、`type`、`name`、`severity`、`limit`、`cursor`。`limit` 默认 20、范围 20～1000；`severity` 默认 0、范围 -4～8；三个文本 filter 最长 255 bytes，opaque cursor 最长 4096 bytes 且不得包含控制字符。
- 新 path 只接受 query `cursor`，不兼容旧入站 `cursor` Header；Data Proxy 只转发 query，Clinic 将其转换为 ActivityHub 的 `x-activityhub-cursor`。caller 切换时必须同步修改，且 query/Header 冲突语义不进入新契约。
- 成功外层保持 `{activities,partial,cursor}`；Activity item 暂保留旧 snake_case（包括 `calibration_time` 等 Investibook 已读取字段），`tags`/任意 JSON `payload` 原样保留，空结果固定为 `activities: []`。
- ActivityHub 对空窗口可能返回 HTTP `404`；只有 body 能严格解析为既有固定 `status=error,msg="Activity not found."` 时才规范为 `200 + activities: []`，其他 upstream `404` 仍映射 `502`，不能与 ownership `404` 混淆。
- 先用 DBAAS org ID + logical cluster ID 做 ownership 查询；错误 org/cluster 均返回 `404 NotFound`。调用方 placement/routing Header 不参与也不向上游透传。
- 本批只实现 list；activity detail、groups、file view/download 和写接口继续保持 Candidate/旧链路。
- Data Proxy 不再读取 `activity_hub` 配置或持有 ActivityHub key；Clinic internal server 复用 Clinic 已有 ActivityHub 配置。Data Proxy 到 internal server 不可达时返回稳定 503/504/502，不能 fallback 到 direct ActivityHub。

Clinic-owned ActivityHub client 同时补齐：明确 timeout、context cancellation、URL encoding、上游非 2xx 检查、固定 Header allow-list 和错误映射。`429` 映射为可重试的 `429 ResourceExhausted`，上游超时映射 `504 DeadlineExceeded`，上游不可用/非法响应映射稳定的 `502/503`；错误与日志不得包含 credential 或完整 activity payload。Legacy Clinic route 保留原有宽松参数和统一 500 契约，只复用底层 search protocol core，避免重构改变既有 caller。

### 7.3 Top RU

Top RU 四个旧 Handler 都是 O11y thin proxy，是验证新 ClusterContext + O11y resolver 的最佳第一条业务链：

1. 用 logical org/cluster 做权限和 metadata 解析。
2. 服务端选择 O11y internal read target。
3. 只向 allowlisted `/internal/diagnosis/api/v1/topru/*` path 代理。
4. 保留 query string 和 body schema，不接受 caller placement Header。

TOPRU-02/03/04 的实现边界已经冻结：

- Clinic internal server 暴露 `GET /clinic/api/v1/internal/data-proxy/v1/orgs/{orgID}/clusters/{clusterID}/targets/top-ru`。它不是第二套 Top RU API，只复用 metadata 的 DBAAS org ownership helper 与既有 `ClusterURLsResolver.ResolveInternalReadTarget`，返回 scheme/host 级 target 和服务端生成的 allowlisted routing context。
- Data Service 只调用固定的 O11y `/internal/diagnosis/api/v1/topru/summary`、`/internal/diagnosis/api/v1/topru/summaryplan` 与 `/internal/diagnosis/api/v1/topru/user_stats`，不调用 NGM、不实现 SQL/RU 聚合，也不接受 caller target URL 或 placement Header。O11y 是聚合语义的唯一 owner。
- 成功 body 作为已验证的单个 JSON value 原样返回，以保留 O11y 未知字段；错误 body 不透传。summary 必填 `start,end,top`，可选 `order_by,user,data_source,step`；summary-plan 在此基础上额外允许可选 `digest`；user-stats 必填 `start,end`，可选 `top`。summary 与 summary-plan 保留 omitted/empty/named `user` 三态。
- 本批只承诺 plain logical Premium 与 plain logical Essential；Resource Pool（包括 Premium/Essential resource-pool 变体）、Dedicated、Starter 和 BYOC 均返回 unsupported 或不作为可用能力承诺，Top RU config 继续 Deferred。本批不照搬不一致的 `topRUEnabled`，也不发明新的 feature gate；后续找到权威 capability source 后再单独扩展 tier。
- 复用现有 `central-o11y`、`clinic_api_base`、Service 与网络链路，不新增 app-delivery config、Secret、IAM 或 Ingress。Top RU target client 与 O11y request 使用 Data Service 的 120 秒 query timeout。

### 7.4 Components/topology

Components 对不同 tier 使用不同权威数据源：Dedicated 依赖 DBAAS Core/config-center；Resource Pool、Premium、Essential V2、BYOC 依赖 TiDB Cloud Mgmt/ResourcePool/SharedPool/TiDBGroups。这些 client/config 与分派逻辑继续由 Clinic 持有。

Clinic 已抽出 legacy/internal 共用的 `ListResourcePoolComponents` core；internal Handler 使用 DBAAS org + cluster 联合 ownership，并返回真实 component array。Data Proxy 只调用该 internal API，不增加上述数据源 client/config。旧 Handler 对“非浏览器请求”追加 mocked TiCDC component 的兼容行为只保留在 legacy route，不进入 internal/public Data Proxy canonical contract。

### 7.5 Plan Replayer/OOM

先迁 list，再迁 download：

- 4a 建立按 cluster/category 的 storage prefix 解析和 list contract。
- 4b 引入短期 file token、prefix 校验、流式 response 和取消/超时处理。

旧 download 接受任意 `provider/region/key` 且不验证 key 属于当前 cluster，这是必须修复的安全边界，不作为兼容行为保留。Plan list 当前把下载 URL 指向 OOM route，也在新 list response 中修正，并作为有意差异记录。

4a 和 4b 可以分 PR 开发，但不能独立对 caller 发布：现有 UI 会立即使用旧 list 中的 `download_url`，新 adapter 会读取 `downloadUrl`。只有 4b 的 Token/download 已部署并通过验证后，才允许切换 4a list；两者共享一个 release gate 和回滚开关。

### 7.6 Continuous Profiling

Continuous Profiling 同时覆盖 O11y proxy、对象存储、Token 和不同输出格式，是高风险迁移：

- 当前 proxy target 是 `nextgen`、`premium`、`nextgen-host`、`byoc-host`；当前 direct/storage target 是 `dedicated`、`shared/serverless`、`byoc`。Resource Pool 必须先经 metadata 映射得到 effective deploy/target，不能预先归入某一类。
- Premium 还可能把 logical child cluster 映射为 parent/resource-pool target，并把 effective deploy type 归一为 `premium`。
- action token 的 `q` 内含 `ts/profile_type/component/address/data_format`，download/view 对 content type、ZIP、protobuf/text/svg/jeprof 有不同语义。
- 旧实现会记录 token，新实现不得复制该行为。

先做 Required JSON list/detail，再做 Token/view/download；config 保持 Deferred Candidate。两批共享同一 ClusterContext 和 target resolver。`outputType`/SVG 只按实际支持的 deploy target 验证，不承诺所有 direct/local tier 都具备 O11y conversion 能力。

### 7.7 SQL Statements

Statements 的旧 GET 契约是行为最复杂的一组，最后迁移。本期先增加 STMT-SQL：Premium 系列通过 `clinic_data_proxy.statements` 在 AWS Athena、GCP BigQuery 和 Azure Synapse 上灵活查询；AliCloud 保持 unsupported。这是增量能力，不替换下列旧契约：

- 当前 proxy target 是 `premium`、`nextgen-host`、`byoc`、`byoc-host`；当前 local target 是 `dedicated`、`shared`、`serverless` 和字面值 `nextgen`。实现时必须按实际 deploy type table 驱动，不能用“nextgen/BYOC 都 proxy”概括。
- Shared/serverless 等 local 路径先解析 storage location，再本地读取和聚合；Dedicated 使用 legacy bucket/prefix 和本地 parser/aggregation。
- Plan detail 还包含 binary plan decode 和额外字段处理。

不能把 STMT-SQL 当作现有 Statements API 的简单替换，否则会改变 aggregation、filter、field selection 和 binary plan 语义。五个 live GET contract 仍需先固化为 golden tests，再逐 tier 实现。

当前 Header validator 允许 project 缺失，但部分本地路径会直接解引用 project 字段；新 ClusterContext 必须明确“project 不存在”的合法 tier 和错误语义。

`resource_groups[]` 目前只有 proxy target 可能生效，local 实现忽略；Phase 0 必须选择“从统一契约删除”或“补齐 local parity”，不能在 OpenAPI 中无条件承诺。`digest`/`plans[]` 的 runtime 宽松行为也需先冻结后再写 required validation。

## 8. 鉴权、授权与租户隔离

### 8.1 边界

Phase 0 必须确认“统一鉴权”最终由 API Gateway 还是 Data Service 进程负责。当前实现是 Gateway 认证/RBAC，Data Service 信任下游请求；在这个前提下仍要求 defense in depth：

1. Gateway 认证 API key/session，生成可信 user context。
2. Gateway 根据 method + canonical path 执行 resource mapping/RBAC。
3. Data Service 使用 path 中的 org/cluster 做联合 metadata 查询，防止合法 org permission 被用于读取另一个 org 的 cluster。
4. Data Service 只把 allowlisted service credential 和 routing header 发往权威上游，不透传 `ClinicUserKey`、Cookie、Authorization 或任意入站 Header。
5. 结构化审计日志记录 caller identity（脱敏）、logical org/cluster、API operation、target kind、status、latency、bytes 和 trace ID。
6. 所有 resolver target 经过 scheme/host/port/path allow-list；下载、profile 转换和 SQL 聚合设置输入对象大小、CPU/内存、输出大小与并发限制。
7. 公开业务路由只能经 Gateway 到达 Data Service；Gateway 必须覆盖而不是保留 caller 提供的用户上下文 Header。Service/Ingress 使用 NetworkPolicy、mTLS 或等价来源验证阻止绕过 Gateway，Data Service 对直接访问和伪造 `ClinicUserKey` 的请求 deny-by-default。

未来若把认证逻辑移入 Data Service，公开 path、resource mapping 和 ClusterContext 校验原则保持不变。

### 8.2 Gateway/RBAC 变更清单

每个新 path 上线前必须完成：

- 验证现有 `/data-proxy` Prefix Ingress/Kong route 确实指向 `clinic-data-service` 并覆盖 `/data-proxy/api/v1`；预期无需增加新 Ingress path，但必须记录各环境 route/plugin 版本。
- 新 route deny-by-default；当前 permission checker 对部分 unmapped internal path 存在放行行为，必须修正或增加 canonical `/data-proxy/api/v1` fail-closed policy，不得用宽泛 whitelist 掩盖缺失的 method/path resource mapping。
- API key static URI allow-list 与 credential 类型检查。固定 Skill 使用 `Authorization: Bearer ${CLINIC_API_KEY}` 和 internal access key；dev/staging/prod 分别验证 internal key、普通 API credential、external/upload key、session 的允许/拒绝矩阵。
- resource map 覆盖所有 method/path。现有 org cluster read 可复用类似 `orgs/{org_id}/clusters/read` 的权限；global exact/search 需要独立 internal/global permission。
- Top RU 若开放给 external user，需要额外检查现有 `orgs/{org_id}/ngm/topru/read` 或其替代 resource。
- Statements、ConProf、files 当前主要是 internal-only；若本轮改变可见范围，必须单独安全评审，不能因为 path 迁移顺带放开。
- 401（未认证）、403（已认证但无权限）和 404（org/cluster 不匹配或资源不可见）的集成测试；若现有 Gateway 把 resource-map error 映射为 401，需要作为实现 gap 修正，不能只在文档声明。
- 直接访问 Service、伪造/重复用户上下文 Header、绕过 Gateway 来源验证的 negative test。

### 8.3 必须修复的旧安全行为

| 严重度 | 问题 | 新契约要求 |
| --- | --- | --- |
| Critical | ConProf JWT 使用代码内固定 HMAC key，24 小时有效；download 还绕过 NGM 鉴权 | 新接口使用高熵随机 opaque handle + 服务端状态、短 TTL 和吊销；download/view 必须经过 Data Proxy auth，并绑定 caller/purpose/path identity |
| Critical | 旧 ConProf 日志记录 token/完整 claims | 禁止记录 token 和完整 claims；添加敏感字段日志测试 |
| Critical | 旧 ConProf token 没有可信 `iat`，公开 key 可签任意 expiry，无法靠等待 TTL 排空 | T0 立即关闭旧免鉴权 download/view；若业务强制兼容，先补 auth + cluster permission + 固定服务端 cutoff，最多保留一个 TTL |
| High | Plan/OOM download 接受任意 provider/region/key，无 cluster prefix 校验 | provider/region 服务端解析；opaque handle + canonical prefix 校验；跨 cluster negative test |
| High | O11y proxy 转发几乎全部入站 Header | 仅构造 allowlisted upstream Header；不透传用户凭证/Cookie/内部 context |
| High | caller placement Header 决定数据目标 | 新公开契约完全移除这些 Header |
| High | metadata/upstream URL 可被代理直接使用 | 校验 HTTPS、允许的 host/port/path prefix，防止 SSRF |
| High | Token query 进入边缘日志、APM、浏览器历史/Referer | 全链路 query redaction；`no-store, private`；`no-referrer` |
| Medium | object key/文件名进入 `Content-Disposition` | 文件名 canonicalize/净化并使用安全编码，防止 header injection |

## 9. 迁移阶段与依赖顺序

### Phase 0：契约与基础设施

目标：没有业务 endpoint 上线，先把所有后续阶段共同依赖的边界做好。

- 冻结公共根 `/data-proxy` 下的新业务 namespace `/data-proxy/api/v1`、org/cluster ID 语义和 API mapping；旧 query/schema/protocol path 不变。
- 用三条 Investibook caller chain 的 contract tests 冻结 7.1 的 Investibook-minimal Cluster detail DTO，不做 full legacy parity。
- Required API 冻结 contract；ConProf config、OOM top-memory、跨 org search、Top RU config 等保留为 Deferred Candidate，不进入当前 OpenAPI、实现计划和 DoD。未来提升为 Required 必须先更新本文、依赖和 release gate。
- Statements legacy mount、Activity companion APIs 和 `clusters2` 记录旧流量/consumer；未进入 Required 范围的不得因为迁移主路径而被误删。
- 实现/设计 Data Service 原生 `ClusterContextRepository`，联合校验 TiDB Cloud org + logical cluster。
- 实现统一 cache/singleflight；cache key 使用 `orgID:clusterID`，定义 TTL、ownership/placement 失效和 no-stale-on-auth-error 规则。
- 实现 capability-specific O11y/storage target resolver 和双向 Header allow-listed proxy transport，覆盖 tier/deployTypeV2；校验 upstream scheme/host/port/path，防止 SSRF。每个能力优先验证 O11y，只有无等价接口或语义缺口时才新增直连 client。
- 冻结 opaque handle 的 direct-storage/O11y target locator、上游 token 加密与 expiry 上限、purpose 签发、最大 TTL、caller binding、replay、吊销和轮换契约，并验证 Gateway/Ingress/APM query redaction。
- 定义统一 error mapping、timeout、retry、cancellation 和 request-size/response-size 策略。
- 验证现有 `/data-proxy` Prefix Ingress 已捕获新 path（预期无需新增 Ingress path），完成 API-key static URI policy、RBAC resource mapping、未映射 `/data-proxy/api/v1` 对 internal caller 也 fail closed、正确 401/403、用户上下文 Header 覆盖、NetworkPolicy/mTLS 或等价来源验证，以及 auth-bypass integration tests。验收证据包含部署 secret/resource-map 的可审查版本，不能只写 app-delivery PR。
- 新 public v1 routes 使用独立、必需的 registration/composition；关键 resolver/config 缺失时 startup/readiness fail，不得因 optional config 加载失败而静默不注册、同时 `/healthz` 仍成功。
- 补齐 Data Service OpenAPI 生成/check target，并把 nested module unit/contract tests 加入 CI。OpenAPI check 除 Router inventory 外，还验证 authentication scheme、repeated query 编码、download content types/headers、错误 DTO 和 breaking-change diff。
- 对既有 `POST /data-proxy/query`、`GET /data-proxy/schema`、serverless-parent metrics GET/POST 以及 metrics/Loki 关键 route 增加回归 smoke，确保新 route group 和 middleware 不改变 Skill 已公开契约。
- 统一 metrics/log labels：operation、upstream、tier、provider、status_code、error_code、latency、bytes；禁止把 org/cluster 作为 Prometheus 高基数 label。
- 在新 ConProf 上线前必须决定旧免鉴权 download/view 的处置：推荐 T0 立即关闭；若业务不能接受，先补 authentication、cluster permission 和固定 cutoff。当前决定仍 Blocked，该安全项不能推迟到 Phase 5b。
- 建立所有旧 path 按 caller/credential 类型的动态流量基线，覆盖未在静态仓库中发现的脚本、旧客户端和临时调用；删除旧 route 前按相同维度复查。
- 为每个 Phase 在部署前冻结成功率、5xx、p95/p99、parity 容差、最短观察时间、旧 QPS 清零窗口和自动回滚阈值；具体数值写入端点验证记录，不能只写“观察一个窗口”。

退出条件：错误 org/cluster、伪造 placement Header、无权限 credential、SSRF target、跨 purpose Token 和 query 泄露的自动化测试通过；OpenAPI 与 Router inventory 一致；所有 Required contract 已冻结，Candidate 明确保持 Deferred 且未混入 OpenAPI。

### Phase 1：Cluster metadata + Activity list

- 上线 internal exact cluster lookup、org-scoped list/detail。
- Clinic internal server 通过迁移前已有的 metadata datasource core 向 Data Proxy 暴露 canonical typed API；Data Proxy 不直连 Clinic DB，#1594 不新增第二套 SQL/repository。
- Clinic internal server 统一访问 ActivityHub；Data Proxy 不持有 ActivityHub credential。
- Investibook 先切 cluster discovery 和 Activity，停止用 Clinic internal org ID 拼 path。

当前进度：[#1572](https://github.com/pingcap-inc/clinic/pull/1572) 与 [#1591](https://github.com/pingcap-inc/clinic/pull/1591) 的 direct 实现及其 staging 结果作为历史 parity 基线保留。#1594 已将 Data Proxy 改为 thin typed client，Metadata internal API 复用既有 datasource adapter；Metadata、Activity 与 Components 已于 2026-07-21 在正式 staging facade 完成验收。Activity cursor 因无 live 样本未覆盖；prod 与 consumer cutover 尚未完成，因此 Phase 1 整体仍未完成。

退出条件：old/new metadata 字段映射完成；source preparer 的 `cluster_meta.json` schema/tests、Jira tenant mismatch、sweep collector 和 clinic-cli contract tests 通过；Activity pagination/filter parity；global endpoint 只能被 internal/global credential 访问。

### Phase 2：Top RU

- Clinic capability target、Data Service safe O11y transport、summary/summary-plan/user-stats routes 与 focused tests 已实现；不复制 NGM/O11y 聚合逻辑。
- Local 已覆盖 plain Premium/Essential target、org ownership、三个固定 upstream path、query allowlist、omitted/empty/named `user` 三态、Header 隔离、unknown response fields、redirect/错误/超时边界和 Router registration。
- #1596 已部署 staging。Premium 的 legacy/new summary、user-stats、omitted/empty/named `user` 完整 JSON parity 和安全负向矩阵通过；Starter/Resource Pool 均 fail-closed。唯一 active Essential 样本在两条链都因 O11y registry 无 target 而失败，active Premium 也没有可用样本，因此保持 Partial Pass。
- capability 暂按支持 tier + resolver/O11y 结果；不引入未经证实的 feature gate。

退出条件：plain Premium/Essential staging parity 通过，所有 placement Header 在新接口上都不是必需且不能改变 target；其他 tier 保持不支持且不得误标可用。

### Phase 3：Components/topology

- Clinic legacy/internal Handler 复用同一个 component service core；不在 Data Service 新增 DBAAS Core、Mgmt、ResourcePool/SharedPool/TiDBGroups client/config。
- Data Proxy 通过 Clinic canonical internal API 暴露 org-scoped component array。
- 覆盖 Dedicated、Resource Pool、Premium、BYOC；明确 Essential/Starter 不支持 topology 的错误。
- mocked TiCDC 兼容行为仅保留在 legacy route，不进入 canonical internal/Data Proxy 响应。

退出条件：各支持 tier 的 component 数量、类型、资源规格、image/version 与权威上游一致。

### Phase 4a：Plan Replayer/OOM list

- 建立 category-specific storage prefix resolver。
- 保持 records/files 主体语义，生成新的 cluster-scoped `downloadUrl`。
- 修正 Plan Replayer download URL 指向 OOM route 的旧问题。
- 允许独立开发和合并，但不单独切 caller；在 4b 上线前，新 list 只用于集成验证。

退出条件：list schema、prefix resolver、handle issuance 和新 `downloadUrl` 格式通过 contract/security/unit tests；不要求 4b handler 已可端到端下载，caller 也尚未切流。

### Phase 4b：Plan Replayer/OOM download

- 引入短期 file token、prefix/canonical key 校验和流式 response。
- 覆盖旧稳定的 Content-Type、Content-Length、Content-Disposition、client cancellation 和 upstream 404/5xx；ETag、Range、checksum 若新增则单独定义语义，不能假设 ETag 等于 SHA-256。
- 对文件名、对象大小、top-memory 解析资源和输出大小设置限制。
- 4a list 和 4b download 同批发布/切流，共用 release gate；OOM top-memory 本轮保持 Deferred Candidate。

退出条件：4a/4b 所有 Required API 的 auth、跨 cluster/purpose、prefix、streaming、资源限制和 caller 回滚测试通过，并完成同批切流 sign-off。

### Phase 5a：Continuous Profiling list/detail

- 按已确认 deploy matrix 覆盖 direct storage 与 O11y target；Resource Pool 先解析 effective target 后分流。
- 固化最长时间范围、分钟对齐、空结果和 upstream error 语义。
- config 本轮保持 Deferred Candidate，不在实现阶段临时扩 scope。

退出条件：所有已承诺 deploy/tier 的 list/detail parity、时间边界和 target mapping 通过；Deferred config 未出现在 OpenAPI/Router。

### Phase 5b：Continuous Profiling token/view/download

- 实现 typed action-token request、opaque handle 服务端状态、purpose/caller/path binding；不兼容旧固定 HMAC key。
- action-token、single-profile view、download 与 consumer 原子切换。
- 覆盖 protobuf、ZIP、text、jeprof/collapsed，以及仅实际 O11y target 支持的 SVG/output conversion。
- 按 Phase 0 决策执行旧 route T0 立即关闭，或只在已经加固并有固定 cutoff 的前提下保留最多一个旧 TTL。

退出条件：CP-03/04/05 与 caller 原子切换；Token/query-redaction/auth/target/output tests 通过；旧 verifier 已关闭或处于有固定截止时间的加固状态。

### Phase 6：SQL Statements

- 先交付 STMT-SQL：Premium 系列复用 `/data-proxy/query`，服务端解析 `persisted_statements_summary` URL，AWS/GCP/Azure 实现后在 prod 做正向 E2E；staging 只做路由/错误 smoke。
- 按 actual runtime + live consumer 固化五个 GET contract。
- 依次覆盖 O11y proxy、shared/serverless storage target、dedicated legacy storage。
- 验证 repeated arrays、fields、排序/聚合、null、binary plan text。

退出条件：五个 Required API 覆盖 proxy/local target matrix，contract/golden/parity 测试通过；legacy mount 和可选参数均有明确最终状态。

### Phase 7：调用方切换与旧依赖清理

- 分别更新 Investibook source preparer、sweep collector、clinic-cli/targeted flow，以及 Clinic UI adapter/embedded dashboard；不能用其中一条链的成功代表全部 caller。
- 按调用方 feature flag 或配置切换，不在 Data Service request path 内双读。
- Required API 验证通过后更新 `nutshell-skills` 的 Data Proxy SUBSKILL、shell/Python helpers、相关 sub-skills/index/changelog，以及 Investibook builtin skill；在新 workspace 验证实际安装版本后才允许删除旧路由。
- 观察旧 path QPS、错误率和新旧 diff；达到退出条件后标记 deprecated。
- 删除 Data Service 对旧 Clinic public/NGM compatibility API、direct metadata DB 和 direct ActivityHub 的依赖；Clinic-owned facade 的 `clinic-internal-api` 依赖保留。随后删除已无调用方的旧公网路由和重复 resolver。

退出条件：旧 path 在约定窗口内按 caller/credential 维度 QPS 为零或只有书面豁免；新 API 达到已冻结 SLO；Skill PR 已发布且 workspace smoke 通过；consumer、运维和安全 owner sign-off；回滚演练完成。

Phase 1 之后，Phase 2～5 可按人力并行；但所有阶段都依赖 Phase 0，不允许每组 API 自己定义 ID/Header/错误规则。Statements 因语义最复杂，保持最后迁移。

## 10. PR 与进度跟踪

### 10.1 Phase/PR tracker

所有 Phase Owner 与 Required reviewer 具体人员必须在 Phase 0 kickoff 前（提议截止 2026-07-24）填入；没有 Owner 的 Phase 不得进入 `In Progress`。Critical 0e 使用更早的提议截止时间。

| Phase | Scope | Owner | Clinic/Data Service PR | Gateway/Deploy PR | Consumer PR | Release gate / 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| Plan | 本文、inventory、path proposal | TBD | [#1572](https://github.com/pingcap-inc/clinic/pull/1572)（已合并） | N/A | N/A | Metadata 实现与 staging 验收记录已落档；其余 Phase 决策继续维护 / Active Document |
| 0a | Data Service nested-module CI、OpenAPI/check | TBD | TBD（独立 PR） | N/A | N/A | metadata PR 不修改通用 workflow；首次生成发现历史 metrics `series` 契约漂移，需独立修 baseline 后再接 nested tests 与 OpenAPI generate/check / Planned |
| 0b | Clinic-owned facade / ClusterContext transport | TBD | [#1594](https://github.com/pingcap-inc/clinic/pull/1594) | staging image [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011)；[#74974](https://github.com/tidbcloud/app-delivery/pull/74974) 仅可选清理 | N/A | Metadata/Activity/Components facade 已实现并完成 staging E2E；Activity cursor、Dedicated Components 为 Not exercised；prod pending / Implemented |
| 0c | O11y/storage resolver、safe transport、opaque handle contract | @StinsonZhao（backend） | [#1596](https://github.com/pingcap-inc/clinic/pull/1596)；其余 capability TBD | N/A | N/A | Top RU capability target + safe transport 已部署，staging Partial Pass；storage/opaque handle 仍 Planned / In Progress |
| 0d | Gateway route、RBAC、network trust boundary | TBD | TBD | TBD | N/A | 所有公开 endpoint 上线依赖 / Planned |
| 0e | 旧 ConProf download/view 紧急加固或 T0 关闭方案 | TBD（提议 2026-07-20 前指派） | TBD | TBD | TBD | Critical；等待 owner + 处置决策 / Blocked |
| 1 | Metadata、Activity | @StinsonZhao（backend） | [#1572](https://github.com/pingcap-inc/clinic/pull/1572)/[#1591](https://github.com/pingcap-inc/clinic/pull/1591)（历史 direct 实现）；[#1594](https://github.com/pingcap-inc/clinic/pull/1594)（facade） | staging image [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011)；[#74974](https://github.com/tidbcloud/app-delivery/pull/74974) 仅可选清理 | TBD (Investibook) | Metadata 与 Activity staging Pass；Activity cursor Not exercised；prod、consumer pending / Implemented |
| 2 | Top RU | @StinsonZhao（backend） | [#1596](https://github.com/pingcap-inc/clinic/pull/1596) | staging 已部署 `internal_api` + `clinic/data-service` `112ae191` | TBD (Clinic UI) | Premium staging Pass；Essential O11y target blocked，整体 Partial Pass；prod/caller pending / In Progress |
| 3 | Components | TBD | [#1594](https://github.com/pingcap-inc/clinic/pull/1594) | staging image [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011)；无新增 Secret/Ingress | TBD | resource-pool/premium staging Pass；Dedicated Not exercised；prod、consumer pending / Implemented |
| 4a | Plan/OOM list | TBD | TBD | TBD | TBD | 只开发/集成，不独立切流 / Planned |
| 4b | Plan/OOM download | TBD | TBD | TBD | TBD | 与 4a 同批发布和回滚 / Planned |
| 5a | ConProf JSON APIs | TBD | TBD | TBD | TBD | 独立发布 / Planned |
| 5b | ConProf token/view/download | TBD | TBD | TBD | TBD | 三个 endpoint + caller 原子切换 / Planned |
| 6 | Statements | TBD | STMT-SQL 当前分支；旧 GET TBD | STMT-SQL app-delivery 当前分支 | TBD | STMT-SQL implemented，待部署/prod E2E；五个 live GET contract 仍 Planned / In Progress |
| 7a | Investibook/Clinic UI caller cutover | TBD | N/A | TBD | TBD | 所有独立 caller chain 验证 / Planned |
| 7b | `nutshell-skills` + builtin skill/helper 更新与 workspace 发布 | TBD | N/A | N/A | TBD | API Verified 后、旧路由删除前 / Planned |
| 7c | Deprecation、旧 QPS 审计、cleanup | TBD | TBD | TBD | TBD | 旧 QPS 清零或有效豁免 + sign-off / Planned |

### 10.2 API delivery tracker

本表是后续每个实现 PR 必须更新的端点级台账。`验证记录` 指向按 12.4 模板创建的同文件锚点或可长期访问的证据；不能只写聊天记录或本地临时文件。

| API ID | Owner | Implementation PR | Gateway/Deploy PR | Consumer PR | Delivery | Verification | 验证记录 / blocker / next action |
| --- | --- | --- | --- | --- | --- | --- | --- |
| META-01 | TBD | [#1572](https://github.com/pingcap-inc/clinic/pull/1572) + [#1594](https://github.com/pingcap-inc/clinic/pull/1594) | [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011) | TBD | Implemented | Local Pass / Staging Pass / Prod Pending | Clinic internal adapter 复用既有 `ListOrganizationAndCluster`；2026-07-21 正式 facade exact/detail/list parity 通过 |
| META-02 | TBD | [#1572](https://github.com/pingcap-inc/clinic/pull/1572) + [#1594](https://github.com/pingcap-inc/clinic/pull/1594) | [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011) | TBD | Implemented | Local Pass / Staging Pass / Prod Pending | 复用 `GetOrganizationByCloudID` + `ListClusters`；query/order/showDeleted 遵循既有 datasource 语义，正式 facade parity 通过 |
| META-03 | TBD | N/A | N/A | TBD | Deferred | Not Run | 暂缓；保留旧 CLI 行为直到单独决策 |
| META-04 | TBD | [#1572](https://github.com/pingcap-inc/clinic/pull/1572) + [#1594](https://github.com/pingcap-inc/clinic/pull/1594) | [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011) | TBD | Implemented | Local Pass / Staging Pass / Prod Pending | 复用 `GetOrganizationByCloudID` + `GetCluster` 保持 DBAAS org ownership；cross-org `404` 与正式 facade parity 通过 |
| TOPO-01 | TBD | [#1594](https://github.com/pingcap-inc/clinic/pull/1594) | [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011)；无新增 Secret/Ingress | TBD | Implemented | Local Pass / Staging Pass with gap / Prod Pending | resource-pool/premium 与 mocked TiCDC compatibility 通过；Dedicated 无 live 样本，Not exercised |
| ACT-01 | @StinsonZhao | [#1591](https://github.com/pingcap-inc/clinic/pull/1591) + [#1594](https://github.com/pingcap-inc/clinic/pull/1594) | [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011)；[#74974](https://github.com/tidbcloud/app-delivery/pull/74974) 仅可选清理 | TBD | Implemented | Local Pass / Staging Pass with gap / Prod Pending | nonempty/filter/empty parity 通过；16 个 90 天样本无 cursor，真实分页 Not exercised |
| TOPRU-01 | TBD | N/A | N/A | TBD | Deferred | Not Run | 暂缓；当前 UI 无 live caller |
| TOPRU-02 | @StinsonZhao | [#1596](https://github.com/pingcap-inc/clinic/pull/1596) | staging `112ae191`；无新增配置 | TBD | Implemented | Local Pass / Staging Partial Pass / Prod Pending | Premium omitted/empty/named user summary 完整 parity；Essential legacy/new 均无 O11y target，active Premium 未覆盖 |
| TOPRU-04 | @StinsonZhao | TBD | 无新增配置 | TBD | Implemented | Local Pass / Staging Pending / Prod Pending | 固定代理 O11y summaryplan；部署后与 NGM summaryplan 做完整 JSON parity，并覆盖同一 SQL 两个 plan |
| TOPRU-03 | @StinsonZhao | [#1596](https://github.com/pingcap-inc/clinic/pull/1596) | staging `112ae191`；无新增配置 | TBD | Implemented | Local Pass / Staging Partial Pass / Prod Pending | Premium user-stats 完整 parity；Essential legacy/new 均无 O11y target；安全负向矩阵通过 |
| FILE-PR-01 | TBD | TBD | TBD | TBD | Planned | Not Run | 与 FILE-PR-02 同批发布 |
| FILE-PR-02 | TBD | TBD | TBD | TBD | Planned | Not Run | 冻结 opaque handle/download contract |
| FILE-OOM-01 | TBD | TBD | TBD | TBD | Planned | Not Run | 与 FILE-OOM-02 同批发布 |
| FILE-OOM-02 | TBD | TBD | TBD | TBD | Planned | Not Run | 冻结 opaque handle/download contract |
| FILE-OOM-03 | TBD | N/A | N/A | TBD | Deferred | Not Run | 暂缓；不进入当前 OpenAPI |
| CP-01 | TBD | TBD | TBD | TBD | Planned | Not Run | 冻结 deploy target matrix |
| CP-02 | TBD | TBD | TBD | TBD | Planned | Not Run | 冻结 detail schema |
| CP-03 | TBD | TBD | TBD | TBD | Planned | Not Run | typed POST + Token contract review |
| CP-04 | TBD | TBD | TBD | TBD | Planned | Not Run | 与 CP-03/CP-05 原子切换 |
| CP-05 | TBD | TBD | TBD | TBD | Planned | Not Run | 与 CP-03/CP-04 原子切换 |
| CP-06 | TBD | N/A | N/A | TBD | Deferred | Not Run | 暂缓；不进入当前 OpenAPI |
| STMT-01 | TBD | TBD | TBD | TBD | Planned | Not Run | 响应排序/集合语义 |
| STMT-02 | TBD | TBD | TBD | TBD | Planned | Not Run | 冻结 response contract |
| STMT-03 | TBD | TBD | TBD | TBD | Planned | Not Run | 决定 resource_groups 行为 |
| STMT-04 | TBD | TBD | TBD | TBD | Planned | Not Run | 决定 digest required 行为 |
| STMT-05 | TBD | TBD | TBD | TBD | Planned | Not Run | 决定 plans[] required 行为 |

### 10.3 Caller cutover 与 legacy route tracker

同一 API 有多个 caller 时分行记录；动态流量基线发现新 caller 后立即补行。Candidate 只有变为 Required 后才进入本表。

完成判定有两条互斥路径：

- 正常路径：该 caller `Cutover=Complete`，对应非豁免旧 route `Legacy=Removed`，附 consumer sign-off、回滚演练和 removal PR。
- 豁免路径：该 caller/credential 可暂时不完成切流，但必须 `Legacy=Exempt`，记录审批人、原因、精确 caller/credential/path、补偿控制、QPS、owner、到期/复审日期和最终迁移计划。豁免必须由 Data Proxy、Security 和 consumer owner 共同批准；过期未续批即视为项目 verification 失败，不能用无限期豁免掩盖未完成迁移。

| API IDs | Caller | Owner | Cutover | Legacy | 旧 QPS/调用方证据 | 回滚演练 | Removal PR / exemption |
| --- | --- | --- | --- | --- | --- | --- | --- |
| META-01/02/04 | Investibook source-preparer + sweep collector | TBD | Not Started | Active | TBD | Not Run | TBD |
| META-01/02/04 | Investibook clinic-cli / targeted-ad-hoc | TBD | Not Started | Active | TBD | Not Run | TBD |
| ACT-01 | Investibook sweep collector | TBD | Not Started | Active | TBD | Not Run | TBD |
| ACT-01 | Investibook clinic-cli / targeted-ad-hoc | TBD | Not Started | Active | TBD | Not Run | TBD |
| TOPO-01 | Investibook clinic-cli / targeted-ad-hoc | TBD | Not Started | Active | TBD | Not Run | TBD |
| TOPRU-02/03/04 | Clinic UI | TBD | Not Started | Active | TBD | Not Run | TBD |
| FILE-PR-01/02、FILE-OOM-01/02 | Clinic UI | TBD | Not Started | Active | TBD | Not Run | TBD |
| FILE-PR-01/02、FILE-OOM-01/02 | Investibook clinic-cli / targeted-ad-hoc | TBD | Not Started | Active | TBD | Not Run | TBD |
| CP-01/02/03/04/05 | Clinic UI / embedded dashboard | TBD | Not Started | Active | TBD | Not Run | TBD |
| CP-01/02/03/04/05 | Investibook clinic-cli / targeted-ad-hoc | TBD | Not Started | Active | TBD | Not Run | TBD |
| STMT-01/02/03/04/05 | Clinic UI / embedded dashboard | TBD | Not Started | Active | TBD | Not Run | TBD |
| 所有旧 path | 动态流量基线发现的其他 caller | TBD | Not Started | Active | Phase 0 待采集 | Not Run | 不得在身份未确认时删除 |

### 10.4 Skill 与 Investibook 交付台账

基线：`nutshell-skills` commit `443ac49324e52cc60d7f4eb9f9768ef5dbb954d1`。Skill PR 在 Required API 的 contract/环境验证通过后编写，但必须在旧路由删除和项目完成前合并、发布并验证新 workspace 实际安装版本。

| 交付物 | 必须更新内容 | PR / Commit | 状态 | 验证 |
| --- | --- | --- | --- | --- |
| `data-proxy/SUBSKILL.md` | 新 namespace、全部 Required API、Bearer auth、lowerCamelCase DTO、下载/Token 示例、候选接口标记 | TBD | Planned | Not Run |
| `data-proxy/scripts/clinic-data-proxy.sh` | 新 API shell helpers；不再拼 placement Header、parent target、provider/region/key 或 raw `q` | TBD | Planned | Not Run |
| `scripts/clinic_api.py` | metadata/activity/topology/Top RU/files/ConProf/Statements typed methods、streaming download、error handling | TBD | Planned | Not Run |
| `scripts/clinic-shell.sh` | 加载并 export 新 helper；旧 helper 标记 deprecated/移除时序 | TBD | Planned | Not Run |
| 相关 sub-skills | 更新 `cluster-metadata`、`activity-hub`、`continuous-profiling`、`diagnostic-files`，删除 internal org ID 和 NGM placement 指导 | TBD | Planned | Not Run |
| Parent/index/changelog | 更新 `SKILL.md`、`SUBSKILLS_INDEX.md`、`changelogs.md` 的路由和能力说明 | TBD | Planned | Not Run |
| Investibook builtin | 更新 `clinic-cli`、source preparer、sweep collector、两套 skill 文档和固定 `cluster_meta.json` schema/tests | TBD | Planned | Not Run |
| Workspace package | 新建 workspace 中安装的 Skill/CLI commit 与 PR 一致，记录 workspace ID、镜像/skill 版本、trace ID 和脱敏 evidence | TBD | Planned | Not Run |

Skill 验收至少包括 shell/Python mock contract tests、`bash -n`/Python syntax、staging live smoke，以及确认请求不再发送 placement Header。Dedicated、Premium、Starter/Essential 至少各验证一个真实 workspace，必要时补 BYOC。仅修改 `SUBSKILL.md` 不算完成。

## 11. 发布、灰度与回滚

### 11.1 每组 API 的发布步骤

1. Unit、handler contract、upstream mock、Gateway auth 和 OpenAPI check 通过。
2. 在 dev 部署新 path；使用固定的测试 cluster/time window 对比旧、新接口。
3. 在 staging 覆盖该 API 支持的 tier/provider matrix。
4. consumer 通过配置或 feature flag 切到新 path；先 internal dogfood，再做受控 prod canary，然后扩大。
5. 按该端点已冻结的数值门槛观察 QPS、成功率、p95/p99、4xx/5xx、upstream error、download bytes/cancel 和 parity diff；触发自动回滚阈值立即停止扩大。
6. 文档记录验证证据和 sign-off，Delivery 保持 `Implemented`，Verification 从 `Not Run` 改为 `Pass`。
7. 旧 path 进入 deprecation，只有在旧 QPS 清零且过了兼容期后删除。

例外 release gate：Plan/OOM list + download 必须同批切流；ConProf action-token + view + download 必须与 caller 原子切换。Candidate API 不允许在开发阶段临时加入；提升为 Required 必须先更新本文并重新评审依赖与验证矩阵。

### 11.2 回滚

- 默认回滚方式是 consumer 保留 legacy client/config feature flag，或重新部署上一版 Data Service。新旧 path、org ID、DTO 和 placement Header 不同，Gateway 只有在已经实现并验证 compatibility adapter 时才能单独改 upstream 回滚。
- 新 Data Service 不在同一请求里 fallback 到旧服务，因此回滚不会形成循环依赖或掩盖错误。
- 数据读取 API 不做写入，回滚不需要数据修复；Token/download 需要按 endpoint pair 一起回滚。
- 若新 token 已签发，回滚期间保留相应新 download/view route 到 token TTL 结束，或让 caller 重新获取旧 token；不得把新 token 转发给旧 NGM。
- 旧 ConProf 公开 key 是安全例外：默认不得为了回滚重新开放旧免鉴权 download/view。只有旧 route 已补认证、cluster permission 和固定 cutoff 时，才可作为短期兼容回滚目标。

## 12. 测试与验证策略

### 12.1 自动化层次

| 层次 | 必测内容 |
| --- | --- |
| Unit | path/query validation、ClusterContext、tier mapping、cache key/失效、storage prefix、opaque handle record/binding、error mapping |
| Handler contract | method/path、repeated query、empty query、response body/header、cursor、streaming、client cancellation |
| Upstream mock | O11y/Mgmt/Core/ActivityHub/storage 的目标 URL、allowlisted Header、timeout、404/429/5xx |
| Gateway integration | credential 类型、401/403/404、resource mapping、global vs org-scoped permission、user context 注入 |
| Compatibility | 同一 cluster/time window 的旧、新 status/schema/关键字段 diff |
| Existing API regression | `/data-proxy/query`、schema、metrics、serverless-parent metrics、Loki/system logs 的 method/path/auth/response smoke |
| Consumer/Skill contract | `nutshell-skills` shell/Python helper mock、Investibook source preparer/sweep/clinic-cli、`cluster_meta.json` schema |
| Live smoke | dev/staging 的真实上游、trace ID、指标和结构化日志 |
| Installed workspace | 新 workspace 实际安装的 Skill/CLI revision、Dedicated/Premium/Starter 或 Essential 的端到端请求 |
| Rollback | caller 或 route 切回旧 path，Token endpoint pair 行为正确 |

### 12.2 特殊兼容测试

- Activity：`begin_ts/end_ts`、所有 filters、limit 最小值、query-only cursor；legacy cursor Header 被忽略，冲突时 query 唯一生效。
- Statements：重复的 `schemas`、`stmt_types`、`plans`，不能按错误的 CSV OpenAPI 描述实现；`fields` 虽控制计算但旧 JSON 仍包含零值字段。
- Top RU：不传 `user`、`user=name`、显式空 `user=` 三种情况；仅 plain Premium/Essential 的 history/top limit；Resource Pool、Dedicated、Starter、BYOC 验证为未支持且不得误路由。
- ConProf：typed action request；若暂时兼容 `q` 则覆盖双层 URL encoding、decode/枚举/长度/canonicalization；旧 token 伪造与固定 cutoff；Premium logical child 到 parent/resource-pool target；direct list 最大 3 小时和分钟对齐；output conversion 按 deploy target 验证。
- Plan/OOM：`instances` 的精确匹配规则需明确；旧实现只是 substring match，不应无意复制。
- Downloads：status、Content-Type、Content-Length、净化后的 Content-Disposition、ZIP entries、取消、超时、空文件、大文件、对象/解析/输出大小限制、上游 404/5xx；ETag/Range/checksum 只在新契约明确支持时验证。
- 安全：伪造 placement Header 不改变 target；错误 org + cluster；把 cluster A 的 file token 放到 cluster B path；跨 purpose、caller 不匹配、token 篡改/过期/replay；object key traversal/prefix escape；恶意文件名/header injection；resolver SSRF。
- 隐私：应用日志、Gateway/Ingress access log 和 APM 中不出现 token/query/object key；下载/view 响应包含 `no-store, private` 和 `no-referrer`。
- Statements：`available_fields` parity 比较集合而不是 map 迭代顺序；`digest/plans[]` 的 required 行为和 `resource_groups[]` 的 deploy 差异按冻结契约测试。

### 12.3 Tier/provider 验证矩阵

`N/A` 必须由产品/数据源契约确认，不能把“返回空”当作通过。

| API 领域 | Dedicated | Starter/Essential | Premium | Resource Pool | BYOC | Provider 重点 |
| --- | --- | --- | --- | --- | --- | --- |
| Metadata | Required | Required | Required | Required | Required | 全部已支持 provider |
| Components | Required | N/A/明确错误 | Required | Required | Required | Mgmt/Core 覆盖 |
| Activity | Required | Required | Required | Required | Required | ActivityHub |
| Top RU | 本批不支持 | 仅 plain Essential Required；Starter 不支持 | 仅 plain Premium Required | 本批不支持 | 本批不支持 | O11y internal read |
| Plan/OOM | Required | 当前未承诺 | 当前未承诺 | 当前未承诺 | 当前未承诺 | AWS/GCP storage，其他待确认 |
| ConProf | Required | 当前能力待确认 | Required（parent mapping） | Required | 能力待确认 | O11y + storage |
| Statements | Required | Required | Required | UI/能力待确认 | Required | O11y + storage |

### 12.4 单 API 验证记录模板

```md
### <API / Phase>

- API ID / Owner:
- Status: Not run | Pass | Fail | Blocked
- Implementation PR:
- Gateway / Deploy PR:
- Consumer PR:
- Contract / OpenAPI commit:
- Deployment version / commit:
- Environment:
- Legacy endpoint:
- New endpoint:
- Test clusters / tiers / providers:
- Success/error/latency/parity thresholds:
- Observation window / old-QPS-zero window:
- Rollback path:

| 层次 | 场景 | 预期 | 实际 | 结果 | 证据 |
| --- | --- | --- | --- | --- | --- |
| Unit | 参数和 target 选择 | ... | Not run | Not run | test name |
| Contract | method/query/header/body | ... | Not run | Not run | test name |
| Auth | no token / wrong org / no permission | 401/404/403 | Not run | Not run | trace ID |
| Parity | 旧新语义 diff | schema/关键字段一致 | Not run | Not run | sanitized diff |
| Live | dev/staging 真实上游 | ... | Not run | Not run | timestamp + trace ID |
| Observability | request/error/latency | 可定位 | Not run | Not run | dashboard/query |
| Rollback | 切回旧路由 | 恢复 | Not run | Not run | procedure |
```

### 12.5 可复现验证命令

以下是当前可用或 Phase 0 必须落地的最小命令入口；实现 PR 需要在验证记录中写实际命令、commit 和结果，不能只写“CI passed”。

```bash
(cd clinic_data_service && go test ./pkg/...)
go test ./pkg/api_gateway/... ./pkg/o11y/urls/... ./pkg/ngm/...
# [Planned, Phase 0] 新增并在 CI 执行；target 名称在 PR 中冻结
make check-data-service-swagger
```

`clinic_data_service/test/` 中依赖真实云资源或本地外部服务的测试必须通过显式 build tag 或环境变量启用，不直接并入普通 PR CI。Gateway focused tests、Data Service contract tests、根模块 resolver/NGM tests 和 Swagger breaking-change check 分开报告，便于定位失败。

Skill/consumer PR 还必须记录其仓库内实际执行的 shell syntax、Python syntax/unit、mock contract、Investibook source-preparer/collector/CLI tests 和 staging live smoke 命令。这里不预先伪造尚不存在的 target；命令名在对应 PR 冻结后回填 10.4 和单 API 验证记录。

## 13. 当前验证结果

Metadata、Activity 与 Components facade 的 focused tests/build 和 #1594 最终 CI 均通过；2026-07-21 又在正式 staging hostname 使用 staging Clinic key 完成 facade E2E 与 legacy 回归，因此 META-01/02/04 为 `Local Pass / Staging Pass / Prod Pending`，ACT-01 与 TOPO-01 为 `Local Pass / Staging Pass with known coverage gap / Prod Pending`。Activity cursor 和 Dedicated Components 因无 live 样本为 Not exercised；这两个缺口不能写成 Pass。

Top RU #1596 已以 `112ae191` 部署 staging。Premium summary/user-stats、user 三态及安全矩阵通过；唯一 active Essential 样本的 legacy/new 请求共同因 O11y registry `no o11y cluster found` 失败，可证明新链没有复制或改变该 resolver 失败，但不能替代成功数据 parity。当前严格标记为 `Local Pass / Staging Partial Pass / Prod Pending`。

以下 #1572/#1591 与 app-delivery 记录保留 direct 实现的历史验证基线，用于当前 facade 的响应 parity 和回归对照。当前 #1594 facade 已由 [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011) 部署并完成 staging 验收；历史 direct Pass/Pending 不再代表当前状态。2026-07-17 的 prod read-only smoke 中旧 API 基线通过、三条新 metadata API 因路由未部署失败，prod 结论仍保持 Pending。

初始静态盘点基线（`clinic` 和 `app-delivery` 于 2026-07-20 开始部署前 fetch；`app-delivery` 原本地工作区仍保留在 upstream 已删除的功能分支，没有被切换或覆盖，实际 staging 配置改动在独立 worktree/branch 完成。固定 Skill 使用用户指定的远端 revision；后续实现 PR 应替换为可审查的 permalink）：

| 仓库 | Commit |
| --- | --- |
| `clinic` | `a8d3ce77a03d18d77c36eb00122126a177ae3e6d`（`origin/master`；已合并到 #1572 分支） |
| `clinic-ui` | `540c889d5da2e9fa72fa25a53fd680a025e57041` |
| `investibook` | `4a38d6ee8d3796963f355bd444cd88b55fdab916` |
| `tidb-dashboard` | `90e5d71dc465e892232c52ed39076fed56b43ec6` |
| `app-delivery` | `9d3aa7cbeccda4c8c33457f09b311f423641130d`（`origin/main` 快照） |
| `nutshell-skills` | `443ac49324e52cc60d7f4eb9f9768ef5dbb954d1` |

| 验证项 | 结果 | 证据/说明 |
| --- | --- | --- |
| 旧 Clinic/NGM route inventory | Pass | 已核对 Router、Handler 和 NGM Swagger；用户列表的双斜杠为 typo |
| Data Service route inventory | Pass | 已确认 META-01/02/04 本地 Router、Handler 注释及 typed Clinic client 接线；当前生成 Swagger 尚未包含本批 route，Phase 0 baseline 修复后再生成 |
| 当前 facade verification | Local Pass / Staging Pass with known gaps | Clinic/Data Service focused tests、build 与 #1594 CI 通过；2026-07-21 正式 staging 上 Metadata exact/detail/list、Activity nonempty/filter/empty、Components resource-pool/premium、ownership/auth/Header isolation 和 legacy compatibility 通过。Activity cursor 与 Dedicated Components 为 Not exercised；三个 Deployment Ready、restart=`0` |
| Top RU staging verification | Partial Pass | #1596 merge `112ae191` 已部署；Premium legacy/new 完整 JSON parity、user 三态、鉴权/ownership/query/placement 隔离及 unsupported tier 矩阵通过。Essential 唯一 active 样本在两条链均无 O11y target，active Premium 亦无样本；两个新 Deployment Ready、restart=`0` |
| 固定 Data Proxy Skill contract audit | Pass | 已核对 `/data-proxy` 根前缀、Bearer internal access key、现有 query/schema/serverless-parent metrics 契约及 lowerCamelCase JSON 习惯 |
| app-delivery Ingress static inventory | Pass | 最新 `origin/main` 的 staging/prod base manifest 都保留 `/data-proxy` Prefix，可覆盖新 metadata namespace；prod-cloud/staging overlay 分别配置 Clinic auth。staging 另外新增 `/api/v1/orgs/{org}/clusters/{cluster}/data-proxy` 的 `dbaas-auth` Prefix，但其 resource map 当前只登记 query POST，不等于 metadata GET 已部署 |
| 2026-07-20 同事改动重叠审计 | Pass | Clinic `origin/master` 的 #1573–#1576 均未触碰 `clinic_data_service`；open #1571 只在 Gateway 增加 canonical DB ownership 校验并在 Data Service 增加 query route；app-delivery merged #74878/#74907 只增加 staging DBAAS Ingress/RBAC。所有最新源码和部署配置中均未发现 `cluster_metadata`、`CLINIC_METADATA_DB_*` 或 META-01/02/04 route 的替代实现 |
| Clinic UI/tidb-dashboard consumer inventory | Pass | 已识别 live、optional/404-tolerated 和 feature-disabled API |
| Investibook consumer inventory | Pass | 已识别 source preparer、标准 sweep collector、targeted/ad-hoc clinic-cli 三条独立链；当前直接依赖 metadata/activity/conprof/files，Statements/Top RU 无专用 caller |
| ID/Header 语义审计 | Pass | 已识别 internal org、tenant org、logical cluster、Premium parent 冲突 |
| 文档一致性检查 | Pass | endpoint ID/台账覆盖、Markdown table 列数、trailing whitespace 和 `git diff --no-index --check` 已检查 |
| 第一批 metadata `gofmt` | Pass | 已对当前 Go 改动执行 `gofmt`；这只证明格式化完成，不代表编译、unit/contract 或 live 验证通过 |
| 第一批 metadata direct repository tests | Historical Pass（superseded） | #1572 direct-DB repository 的历史证据；当前 Data Proxy 已删除 MySQL repository，#1594 也不在 Clinic internal API 迁移/复制等价 SQL，而是适配迁移前既有 datasource。修正后的 Clinic adapter 与 Data Proxy transport/status tests 已通过 |
| 第一批 metadata handler tests | Pass | focused file test 与完整 `pkg/handlers` package test 均通过；覆盖 opaque ID、org ownership、分页参数、503/504 |
| 第一批 metadata config tests | Pass | `GOCACHE=/private/tmp/clinic-go-build go test ./pkg/config -count=1`；覆盖 required/optional 配置、独立 loader/env expansion，以及 metadata 配置错误不禁用 warehouse routes |
| Data Service 相关完整 package compile/test | Pass | 2026-07-20 合并最新 Clinic master 后重跑 `go test -p=1 ./pkg/clustercontext ./pkg/config ./pkg/handlers ./pkg/server ./cmd/api -count=1`；四组有测试的 package 全部通过，`cmd/api` 成功编译（无测试文件） |
| ACT-01 direct local unit/contract/client/config/router | Historical Pass（superseded） | #1591 direct-ActivityHub 实现的历史证据；当前 ActivityHub protocol/credential 已归还 Clinic，Data Proxy 只保留 typed internal client/handler tests |
| ACT-01 staging Secret/config | No longer required by Data Proxy | `CLINIC_ACTIVITY_HUB_KEY` 与 Data Service `activity_hub` section 是 direct 实现遗留；facade 使用 Clinic 已有配置，app-delivery 只需删除冗余 section，Secret key 可在回滚观察窗后单独清理 |
| ACT-01 direct staging image/rollout（historical） | Historical Pass | Clinic CD [run 29729277123](https://github.com/pingcap-inc/clinic/actions/runs/29729277123) 构建 `feat-1a1e19372eb33f4681a808366090cd427460c07f`，app-delivery [#74976](https://github.com/tidbcloud/app-delivery/pull/74976) 合并历史 direct image；不计入当前 facade 验收 |
| ACT-01 isolated staging E2E（historical） | Historical Pass | 隔离 direct Deployment 的 nonempty/filter/empty、auth、ownership、Header isolation 通过；临时资源已删除，不计入当前 facade 验收 |
| ACT-01 cursor live sample（historical direct） | Blocked by data | 对 68 个历史 direct 样本未找到 cursor；当前 facade 又检查 16 个 90 天样本仍无 cursor，因此真实第二页继续为 Not exercised |
| ACT-01 staging logs/health/cleanup（historical） | Historical Pass | 历史 direct 隔离资源已清理；当前 facade 的运行状态见下一行 |
| ACT-01 formal staging current state | Pass with cursor gap | #1594 facade 经 [#75010](https://github.com/tidbcloud/app-delivery/pull/75010) / [#75011](https://github.com/tidbcloud/app-delivery/pull/75011) 部署；正式 hostname 的 nonempty/filter/future-empty、legacy parity、auth/negative/Header isolation 均通过。Clinic、clinic-internal-api、clinic-data-service Ready、restart=`0`；cursor 因无 live 样本为 Not exercised |
| Data Service 全 module `go test ./...` | Fail（existing baseline） | `clinic_data_service` 根 package 的既有 `cluster_compare_test.go` 仍按旧签名调用 `handlers.ClusterSqlDiff`，导致 build fail；`test/` 还默认依赖本机 `:8087`、Feishu 和完整云凭据。上述文件本批未修改；其余列出的 package（包括全部 `pkg/...`）已通过。本批在本地精确运行相关 package，不把 external integration tests 伪装成 hermetic unit tests |
| Clinic root `ci (test)` | CI conclusion Pass，但 baseline tests 实际 Fail | PR [#1572](https://github.com/pingcap-inc/clinic/pull/1572) run [29715164952](https://github.com/pingcap-inc/clinic/actions/runs/29715164952) 与同一基线 commit `a8d3ce77` 的 master run [29684118623](https://github.com/pingcap-inc/clinic/actions/runs/29684118623) 日志都包含完全相同的失败：`Test_ConverToTaskType`、`TestRegisterDiagnosisAPI`、`Test_Processed` 和 3 个 `pkg/db/model` tests。前两项是多年未随 enum/禁用 route 更新的 stale assertions；后四项涉及未初始化 DB、缺表、过长 fixture 和共享数据 ID。#1572 diff 只含 `clinic_data_service` nested module 与本文，不触碰这些 root packages。`Makefile` 的 `go test ... 2>&1 \| tee test_out.log` 未启用 `pipefail`，因此 `go test` 非零退出码被 `tee` 掩盖，两次 job 最终都显示 success；不能把该绿色 check 当成 root tests 通过证据 |
| Clinic root `ci (lint)` | Pass | 初始 PR run 29715164952 仅报告 `pkg/proxy/project_metric_proxy.go:73` 的 gofmt 和 `pkg/ngm/slowquery/slowquery_test.go:615` 的 gofumpt；同一基线 master run 29684118623 也失败。由于该 required check 会直接阻止 #1572 合入，经确认后 commit `30c07d36` 只应用这两处 formatter 输出（各一行、无语义变化）；最新 [run 29716681819](https://github.com/pingcap-inc/clinic/actions/runs/29716681819) 的 `ci (lint)` 已通过 |
| Metadata config fail-closed | Pass | `TestRegisterClusterMetadataRoutesFailsClosedOnConfigLoadError` 证明配置文件不可读/不可解析时启动失败，不会让 `/healthz` 保持成功而 metadata 永久 `503`；`TestRegisterClusterMetadataRoutesAllowsAbsentOptionalSection` 证明本地 optional 且 section 缺失时仍可显式返回 `503` |
| Data Service nested-module CI definition | Not implemented | 为保持 metadata PR 最小化，[#1572](https://github.com/pingcap-inc/clinic/pull/1572) 不修改通用 workflow；相关 package 已本地验证，nested-module CI 与 OpenAPI baseline 由 Phase 0a 独立 PR 补齐 |
| Data Service OpenAPI generate/check | Blocked | 首次生成可解析，但顺带加入与 runtime 不一致的历史 metrics `series`（错误 CSV repeated-query 编码且遗漏 POST）。该生成 churn 已从 metadata 批次移除；先在独立 baseline 修复旧注释，再生成 META-01/02/04 并接 CI，不能把“文件可解析”误记为契约通过 |
| Prod Clinic key / legacy metadata 基线 | Pass | 2026-07-17T09:07Z 前后，用用户提供且与 prod 匹配的 key 请求旧 dashboard cluster list，返回 `200` 和一个 active Starter / AWS `us-west-2` 样本；key 值和真实租户标识不写入文档。新 org path 使用旧响应中的 `tenantID`（DBAAS org ID），不使用 Clinic internal `orgID` |
| Prod Clinic key / existing Data Proxy baseline | Pass | 同一 key 请求 `GET /data-proxy/info` 返回 `200`、`service=Clinic Data Proxy`、`version=1.0.0`；证明 prod Kong、key 验证、`/data-proxy` Prefix Ingress 和现有 Data Service 请求链可用 |
| Prod Clinic key live smoke（META-01/02/04） | Fail | 对同一真实样本请求 global detail、org list、org detail 三条新 path，均返回 plain-text `404 page not found`；说明 prod Data Service 版本尚无这些 route。未返回 metadata，不能计为通过；部署 metadata DB Secret/config、网络和新镜像，并明确 internal-key-only Gateway contract 后必须原样重测 |
| Gateway runtime config 验证 | Partial pass | prod Ingress 已用 `/data-proxy` Prefix 指向 Data Service 并挂载 Clinic auth；provided internal key 可访问现有 Data Proxy。当前普通 key 的静态 URI policy 不含新 namespace，而 internal key 会绕过 URI/RBAC mapping；本批若先按 Investibook internal-key-only 发布可复用现状，但对普通 credential 开放前必须补 fail-closed、org RBAC 和 global internal-only policy |
| Staging metadata DB Secret/config | Pass（持久 desired state） | 复用现有 `clinic-readonly` 身份；凭据已通过 AWS Secrets Manager 内部复制加入 `staging-clinic-data-service`，ExternalSecret 为 `Ready=True`，只检查 key 名且未输出值。[app-delivery #74935](https://github.com/tidbcloud/app-delivery/pull/74935) 已合并为 `3d29948a10aa66fe8ca6ff7629dfa48431abc1fb`，Argo 已持续下发 `required: true` 配置，不再依赖临时 ConfigMap |
| Staging image/rollout | Pass（merged image + persistent config） | Clinic [#1572](https://github.com/pingcap-inc/clinic/pull/1572) merge commit `f6fc862d708e04172b6ed6ff4be9c223a5e44835` 已由 CD [run 29721241960](https://github.com/pingcap-inc/clinic/actions/runs/29721241960) 构建；app-delivery [#74956](https://github.com/tidbcloud/app-delivery/pull/74956) 已更新 master image。2026-07-20T07:25:46Z 复核 Deployment generation/observed=`58/58`、updated/ready/available=`1/1/1`、unavailable=`0`，Pod restart=`0`，挂载持久 ConfigMap `clinic-data-service-config-k7dm8fbb6k` |
| Staging backend live smoke（META-01/02/04） | Pass | 同 namespace 隔离 Deployment 使用 feature image、真实 staging Secret/Clinic DB 和目标配置。global detail、org list、org detail 均返回 `200`，list 命中同一 active 样本，两个 detail canonical 字段一致；错误 org/未知 cluster 返回 `404`，非法 org/page 返回 `400`。真实租户标识和响应已脱敏 |
| Staging DB connectivity/schema | Pass | 从 `clinic-staging` 网络边界完成 DNS、TCP、认证和只读查询；`organizations.dbaas_org_id` 与 `clusters` 联合查询得到真实 cloud cluster，并由 API 返回 tier/provider/region/status/project 等预期字段。未执行写入或 DDL |
| Staging public Gateway no-key | Pass | `GET https://staging-clinic.pingcap.com/data-proxy/info` 和新 metadata path 在无凭据时均返回 `401`，证明公网 `/data-proxy` Ingress 与 `clinic-auth` 拒绝链生效 |
| Staging public Gateway pre-merge smoke | Pass | 2026-07-20T03:36:52Z 前后使用临时应用后会被 Argo 恢复的目标 ConfigMap 完成首轮 smoke：`/data-proxy/info`、legacy dashboard list、global detail、org list、org detail 均 `200`；字段 parity、detail equality、list membership、错误 org/cluster、非法参数和 no-key 均通过。该轮只证明实现与环境兼容，不等于持久部署验收 |
| Staging post-merge E2E（META-01/02/04） | Pass（staging 迁移验收完成） | 2026-07-20T07:14:39Z 后在 merged image + app-delivery desired state 上验收；07:20:37Z 的公网矩阵 `overall=true`：legacy 与 `/data-proxy/info` 为 `200`，三条新 API 及定向 list 为 `200`，14 个 old/new canonical 与 diagnostic 字段 parity 全 true，global/org detail 和 list item 相同；错误 org/未知 cluster=`404`，非法 org/page/pageSize/showDeleted=`400`，无 key info/metadata=`401`，伪造 placement headers 不改变响应。system logs 无 metadata panic/fatal/init/query error；07:25:46Z Pod 仍 `1/1 Ready`、restart=`0`。key、真实 ID 和原始响应未记录 |
| Unit/contract tests | Partial pass | 第一批 repository、完整 handler/config/server package、API binary compile、真实 staging MySQL schema、backend route contract、公网 Router-through-Gateway 和本批 old/new DTO parity 已通过；prod live contract 和跨阶段 consumer contract 尚未完成 |
| Skill/consumer runtime smoke | Not run | merged image 与 metadata API 已在 staging 验收，但尚未切换 Investibook caller，也未更新/发布 Skill；Skill 必须在 API 合并和最终 path/response 冻结后更新 |
| Staging old/new parity | Pass | staging internal key 公网对照 legacy dashboard 样本：ID、DBAAS org、name/status/version/provider/region/deployType/deployTypeV2、project/parent fields 一致；list、跨 org、未知 cluster、非法参数和 no-key 均通过。dev 未单独发布，本批合并 gate 以 staging 为准 |
| 下载完整性/安全验证 | Not run | 尚未实现代码 |
| prod canary | Not run | 尚未实现代码 |

### 13.1 META-01/02/04 prod Clinic key smoke（2026-07-17）

- Environment: `prod` (`https://clinic.pingcap.com`)
- Auth: `Authorization: Bearer ${CLINIC_DATA_PROXY_API_KEY}`；仅从交互式 stdin 注入，命令参数、日志和本文均不保存 key 值
- Sample: 旧 API 返回的 active Starter、AWS `us-west-2` 集群；真实 org/cluster ID 在本文脱敏
- Org identity: 新 path 的 `{orgID}` 取旧响应 `tenantID`（TiDB Cloud/DBAAS org ID）
- Acceptance: HTTP `200` 且 body 能解析为预期 metadata/list DTO；仅 key 有效或路由存在都不算通过

| 请求 | 预期 | 实际 | 结果 |
| --- | --- | --- | --- |
| `GET /clinic/api/v1/dashboard/clusters?limit=1&page=1&show_deleted=false` | `200`，取得可供新接口校验的真实 cluster | `200`，返回 active cluster metadata | Pass（legacy baseline） |
| `GET /data-proxy/info` | `200`，确认同一 key 可进入现有 Data Proxy | `200`，返回 Data Proxy service/version/vendor 信息 | Pass（Data Proxy baseline） |
| `GET /data-proxy/api/v1/clusters/{clusterID}` | `200`，单个 canonical metadata | `404 page not found` | Fail |
| `GET /data-proxy/api/v1/orgs/{orgID}/clusters?page=1&pageSize=1` | `200`，list envelope 包含样本 cluster | `404 page not found` | Fail |
| `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}` | `200`，org ownership 下的 canonical metadata | `404 page not found` | Fail |

结论：prod key 和真实样本已验证有效，但新实现尚未进入 prod 请求链，当前不满足用户定义的验收条件。implementation PR 与 master image 已完成；下一次验证只能在 prod metadata 只读连接 Secret/config、internal-key-only Gateway contract、prod image bump 和 Data Service 部署完成后执行，重测时还需增加错误 org、错误 cluster、无 key 和普通 key 的 negative cases。

### 13.2 META-01/02/04 direct-DB prod deployment gap（historical, superseded）

以下 DB credential/config 前置只适用于 #1572 direct-DB 镜像。当前 facade 不需要为 Data Proxy 创建或注入 metadata DB 账号；prod 的新前置是同时部署兼容的 Clinic internal server 与 Data Proxy image，并验证既有 `clinic_api_base` Service 链路。

| 项目 | 已确认 | 最小 next action |
| --- | --- | --- |
| Ingress | prod `/data-proxy` Prefix 已覆盖 `/data-proxy/api/v1/...`，无需新增 path | 保持现有 Ingress；部署后用 `/data-proxy/info` 与三条新 path 做同一 key 对照 |
| Data Service image | 最新 app-delivery `origin/main` 的 prod tag 为 `master-85517377706df1802fe90959804caa1ad88b894c`，不含 #1572 metadata Handler/Router；实际新 path 为 `404`。#1572 master image 已构建并在 staging 验证 | 提交/合并 app-delivery prod image bump，目标必须是包含 `f6fc862d...` 或更新代码的已验证 master tag |
| Data Service config | prod `config-dataproxy.yaml` 尚无 `cluster_metadata` section；Deployment 已通过 `envFrom` 挂载 `prod-clinic-data-service` ExternalSecret | app-delivery prod config 增 `required: true` 和 `${CLINIC_METADATA_DB_*}`；无需改变现有 ExternalSecret/Deployment 结构 |
| DB credential | Git 中没有证据证明 `prod-clinic-data-service` 已含 metadata DB credential；Kong 使用的 `root` 配置不可复用 | 优先确认可否复用已批准且最小权限的 Clinic read-only 身份；仅在没有可复用身份时创建独立账号。把 host/user/password/database/TLS 写入既有远端 secret 通道，不在 Git/本文保存值 |
| DB network/TLS/schema | Data Service、Clinic/Kong 位于同一 prod control-plane 环境，具备可达的可能性，但没有 Data Service Pod 到 RDS:3306 的运行时证据 | 上线前从相同 ServiceAccount/namespace 做 DNS、TCP、TLS、`SELECT` 两表字段及 replica lag preflight；失败时不得把 `required` 改回 false 规避 |
| Auth（当前 acceptance） | provided internal Clinic key 已能访问旧 global metadata 与现有 Data Proxy；现有 Gateway 对 internal key 允许未映射 path | 本批先明确为 Investibook/internal-key-only；验证 no-key/普通 key 被拒绝。未来对普通 credential 开放前补 `/data-proxy/api/v1` fail-closed、org RBAC 与 global internal-only policy |

因此，下一次 prod smoke 的前置条件不是再改 Handler 或重新构建实现，而是准备/确认最小权限只读 DB 身份、验证网络/TLS、提交并合并 app-delivery prod config/image PR 后部署。任何一步缺失都不能宣称“用 Clinic key 获取到预期数据”。

### 13.3 META-01/02/04 direct-DB staging 验证记录（historical baseline）

`app-delivery` 中现有 staging 部署骨架已经足够：Argo ApplicationSet `staging-clinic-data-service` 部署到 `clinic-staging` namespace；base Ingress 的 `/data-proxy` Prefix 覆盖三条新路径，overlay 挂载 `clinic-auth`；Deployment 通过 `envFrom` 引入 `clinic-data-service` Secret。因此本批没有修改 GitHub workflow、Ingress、ExternalSecret manifest、Deployment env 或 Gateway 配置。已合并的 app-delivery [#74878](https://github.com/tidbcloud/app-delivery/pull/74878) 和 [#74907](https://github.com/tidbcloud/app-delivery/pull/74907) 另有带 `dbaas-auth` 的 `/api/v1/orgs/{org}/clusters/{cluster}/data-proxy` Prefix；该路径服务 #1571 的 DBAAS query contract，不替代本批 `/data-proxy/api/v1` metadata contract。

#### 配置与 Secret 落点

1. [app-delivery #74935](https://github.com/tidbcloud/app-delivery/pull/74935) 只修改 staging `config-dataproxy.yaml`，增加：

```yaml
cluster_metadata:
  required: true
  host: "mysql-clinic.cdllqpdubujz.us-west-2.rds.amazonaws.com"
  user: "${CLINIC_METADATA_DB_USER}"
  password: "${CLINIC_METADATA_DB_PASSWORD}"
  database: "clinic"
```

`port` 和连接池沿用代码默认值，避免提交非必要调优。该 PR 的全部 CI 已通过，并于 2026-07-20T07:10:42Z 合并为 `3d29948a10aa66fe8ca6ff7629dfa48431abc1fb`；没有提交本地 optional 示例配置。

2. ExternalSecret 已从 AWS account `552185537300`、region `us-west-2` 的 Secrets Manager secret `staging-clinic-data-service` 整包 `extract`，base Deployment 也已 `envFrom` 该 Secret，因此 manifest 无需改动。staging Grafana datasource 已使用 Clinic DB 的 `clinic-readonly`，本轮复用该现有只读身份：在 AWS 内部从既有 secret 复制 password，并在保留所有原 key 的前提下向 `staging-clinic-data-service` 增加：

```text
CLINIC_METADATA_DB_USER
CLINIC_METADATA_DB_PASSWORD
```

Secret 新版本已创建并强制触发 ExternalSecret refresh；状态为 `Ready=True`，Kubernetes Secret 中两个 key 名已确认存在。全过程没有把值输出到命令、Git、PR、issue、聊天或验证日志。由于已有身份已在 staging Clinic DB 上提供只读访问，本轮不需要创建新 DB 账号，也不需要确认额外 owner。

3. 从 `clinic-staging` namespace 的临时只读 Pod 和隔离 Data Service Deployment 完成了 DNS、TCP 3306、认证、`SELECT`、所需列和联合归属查询验证。数据库中存在可供验收的 cloud/DBAAS org 与 cluster；API 成功返回真实记录。没有执行写入、DDL，也没有把真实 tenant 数据写入本文。

#### 镜像与部署顺序

1. Clinic CD [run 29713618565](https://github.com/pingcap-inc/clinic/actions/runs/29713618565) 已从实现分支成功构建 `feat-57b1a48c3054a3e7f8d35f81ca46f1252ec3b938`。
2. CD 触发的 app-delivery bump [run 29713655953](https://github.com/tidbcloud/app-delivery/actions/runs/29713655953) 成功，[#74936](https://github.com/tidbcloud/app-delivery/pull/74936) 已合并。
3. 合并前，Argo 曾把 feature image 部署到共享 `clinic-data-service`，Deployment `1/1 Ready`；当时配置 PR #74935 尚待 review，因此该状态只用于预合并兼容性验证，不作为持续可用环境。
4. 为在合并前验证实现与环境兼容性，先创建无 Ingress 的临时 `clinic-data-service-metadata-smoke` Deployment/ConfigMap，复用相同 ServiceAccount、Secret、feature image 和 #74935 配置；backend smoke 通过后已删除。
5. 公网验收时将同一目标 section 临时应用到共享 hashed ConfigMap 并滚动单 Pod；公网 smoke 全部通过。Argo 随后自动恢复 Git 中尚未合并的配置，复核 `hasClusterMetadata=false`、feature image 不变、Deployment `1/1 Ready`，没有留下手工漂移。
6. Clinic #1572 于 2026-07-20T06:14:46Z 合并为 `f6fc862d708e04172b6ed6ff4be9c223a5e44835`；Clinic CD [run 29721241960](https://github.com/pingcap-inc/clinic/actions/runs/29721241960) 成功构建 `master-f6fc862d...`，自动生成并合并 app-delivery [#74956](https://github.com/tidbcloud/app-delivery/pull/74956)。
7. app-delivery #74935 合并后，Argo 在约 2026-07-20T07:14:39Z 完成持久配置 rollout。Deployment generation/observed=`58/58`、updated/ready/available=`1/1/1`，新 Pod restart=`0`，挂载 `clinic-data-service-config-k7dm8fbb6k`；该 ConfigMap 已确认包含 `required: true` 及 Secret 环境变量引用。

实际状态检查只读取状态和 key 名，不读取 Secret 内容：

```bash
kubectl -n clinic-staging get externalsecret clinic-data-service
kubectl -n clinic-staging get secret clinic-data-service -o json | jq -r '.data | keys[]'
kubectl -n clinic-staging rollout status deployment/clinic-data-service --timeout=5m
kubectl -n clinic-staging get pods -l app=clinic-data-service
```

#### staging Clinic key 验收

验收入口为 `https://staging-clinic.pingcap.com`，必须使用 staging 的 internal Clinic access key；prod key 不能用于 staging，也不能把 key 写进命令历史或本文。现有 staging API Gateway image 已包含 internal key 对 internal user 的未映射 path 放行逻辑，因此本批 internal-key-only smoke 不要求 Gateway 发版；普通 credential 的 URI/RBAC 策略仍属于后续正式开放 gate。

验证顺序：

1. 用 staging key 调旧 `GET /clinic/api/v1/dashboard/clusters?limit=1&page=1&show_deleted=false`，取得一个真实 `orgID`（DBAAS org ID）和 `clusterID` 基线。
2. 同一 key 调 `GET /data-proxy/info`，确认 staging Kong、key、Ingress 和当前 Data Service 请求链可达。
3. 调三条新 API：global detail、org-scoped list、org-scoped detail；要求均为 `200`，list 包含基线 cluster，两个 detail 的 canonical 字段与旧接口一致，`orgId` 等于 DBAAS org ID。
4. 至少验证错误 org + 正确 cluster、未知 cluster 返回 `404`；无 key 返回 `401/403`。若本轮只承诺 internal-key-only，不把普通 credential 的拒绝记成新 API 失败，但必须记录其实际状态。
5. 记录 app-delivery commit、image tag、UTC 时间、HTTP status、request ID、脱敏字段 diff 和失败日志摘要；不记录 key、DB password 或真实租户完整响应。

当前已经完成 backend 与公网 live smoke，实际结果如下（真实 org、cluster、name 和完整 response 均脱敏）：

| 场景 | 实际结果 | 结论 |
| --- | --- | --- |
| `GET /healthz` | `200` | feature image + required DB 初始化成功 |
| global detail | `200`，返回真实 provider/region/status/tier/project 等字段 | Pass |
| org list（`page=1&pageSize=2&showDeleted=false`） | `200`，`total=1` 且包含同一样本 | Pass |
| org-scoped detail | `200`，与 global detail canonical 字段一致 | Pass |
| 错误 org + 正确 cluster | `404 NotFound` | Pass |
| 未知 cluster | `404 NotFound` | Pass |
| 非法 org ID | `400 InvalidArgument` | Pass |
| `page=0` | `400 InvalidArgument` | Pass |
| 公网无 key 请求 `/data-proxy/info` 和 metadata path | `401` | Pass |
| 公网 staging internal key `/data-proxy/info` | `200`，service/version baseline 正确 | Pass |
| 公网 legacy dashboard list | `200`，取得 active cloud/DBAAS 样本 | Pass |
| 公网 global detail / org list / org detail | 全部 `200`；字段 parity、detail equality、list membership/pagination 均为 true | Pass |
| 公网错误 org / 未知 cluster | `404 NotFound` / `404 NotFound` | Pass |
| 公网非法 org / `page=0` | `400 InvalidArgument` / `400 InvalidArgument` | Pass |

#### Post-merge 持久部署 E2E（2026-07-20T07:14:39Z–07:25:46Z）

此前的 feature-image 与临时 ConfigMap smoke 只作为合并 gate。本轮在 Clinic #1572、app-delivery #74956 和 #74935 全部合并、Argo desired state 已同步后重新执行完整公网验收，才作为 staging 迁移完成证据。

| Gate | 脱敏结果 | 结论 |
| --- | --- | --- |
| Deployment provenance | image=`master-f6fc862d...`；ConfigMap=`clinic-data-service-config-k7dm8fbb6k`；generation/observed=`58/58` | Pass |
| Rollout health | updated/ready/available=`1/1/1`、unavailable=`0`、Pod restart=`0`、ExternalSecret=`Ready=True` | Pass |
| Existing path baseline | legacy dashboard list=`200`；`/data-proxy/info=200` 且 `service`/`version`/`supported_vendors` schema 正确 | Pass |
| New metadata success paths | global detail、org list、cluster-ID 定向 org list、org detail 均=`200`，JSON content type 正确 | Pass |
| Canonical schema/parity | required response shape 正确；ID、DBAAS org、name、status、version、provider、region、deployType、deployTypeV2、project、parent、shootName、createdAt、deleted 共 14 项全部一致 | Pass |
| Collection/detail consistency | global detail=org detail；定向 list 命中样本且 item=detail；普通第一页本轮也命中样本 | Pass |
| Ownership/not-found | 正确 cluster + 错误 org=`404 NotFound`；opaque unknown cluster=`404 NotFound` | Pass |
| Parameter validation | 非法 org、`page=0`、`pageSize=201`、`showDeleted=maybe` 均=`400 InvalidArgument` 且错误契约正确 | Pass |
| Authentication | 无 key 的 `/data-proxy/info` 与 metadata path 均=`401` | Pass |
| Server-side placement | 注入伪造 provider/region/project/deployType/cluster headers 后仍=`200` 且 body 与基线相同 | Pass |
| Observability | metadata 日志聚合为 5×`200`、4×预期 `400`、2×预期 `404`，error body 包含 request ID；无 metadata panic/fatal/init/query error | Pass |
| Post-E2E health | 2026-07-20T07:25:46Z 再次检查仍 `1/1 Ready`、restart=`0` | Pass |

System-log 审计按 `clinic-api` skill 使用 `/data-proxy/system-logs/api/v1/...` 和 `Loki-aws-us-west-2` datasource，不使用 business cluster Loki path。窗口内两条非 metadata error 是既有的“未配置 AI client”以及一次错误 datasource 探测被取消；两条 warning 是 Gin debug/trusted-proxies 提示，均不属于本批回归。验收请求最大观察延迟约 1.8 秒；未设置性能 SLO，仅记录为本轮环境基线。

至此第一批 Cluster metadata API 在 staging 的实现、持久配置、Secret、rollout、Gateway、数据 parity、隔离负例和日志均验收通过。后续剩余项是 prod preflight/canary、Investibook caller 切换和最终 Skill 更新；不再把 staging #74935 review 或临时配置视为未完成项。

### 13.4 最新代码与同事改动重叠审计（2026-07-20）

本轮已核对 Clinic 和 app-delivery 远端状态。Clinic [#1572](https://github.com/pingcap-inc/clinic/pull/1572) 已合并为 `f6fc862d708e04172b6ed6ff4be9c223a5e44835`；app-delivery 原本地工作区处于 upstream 已删除的旧功能分支，因此没有切换或覆盖它，而是在 `/private/tmp/app-delivery-staging-metadata` 独立 worktree 的 `codex/staging-data-proxy-metadata` 分支提交最小配置改动并创建 [#74935](https://github.com/tidbcloud/app-delivery/pull/74935)，该 PR 现已合并为 `3d29948a10aa66fe8ca6ff7629dfa48431abc1fb`。

| 来源 | 已包含的相关能力 | 对 #1572 的影响 |
| --- | --- | --- |
| Clinic master #1573–#1576 | O11y/NGM routing 修复 | 没有提交触碰 `clinic_data_service`，无 metadata route/config/repository；已无冲突合并到 #1572 |
| Open Clinic [#1571](https://github.com/pingcap-inc/clinic/pull/1571) | API Gateway `dbaas_auth` 直连 canonical Clinic DB，按 cluster 的 `TenantID` 校验 org；Data Service 增加 `POST /api/v1/orgs/{org}/clusters/{cluster}/data-proxy/query` | 是鉴权和 path 设计的部分重叠，不提供 cluster list/detail payload，也没有 `organizations.dbaas_org_id` 联表、metadata repository 或配置，不能替代 META-01/02/04；合并时需处理同一 Data Service handler/router 附近的潜在冲突 |
| Merged app-delivery [#74878](https://github.com/tidbcloud/app-delivery/pull/74878)、[#74907](https://github.com/tidbcloud/app-delivery/pull/74907) | staging 新增 `dbaas-auth` Ingress Prefix；resource map 当前只登记 query POST | 可供未来普通 TiDB Cloud credential path 对齐评审；当前 internal Clinic key smoke 仍走已有 `/data-proxy` + `clinic-auth`，无需重复新增 Ingress |
| Data Service 既有 MySQL/DAO | 指向 analysis results 库，已有 legacy `clusters` 单表读取和可写的通用 DAO | 可复用现有 MySQL driver/GORM 依赖和连接池经验；不能复用其数据源、扁平 model 或暴露写方法的通用 DAO 作为只读 metadata authority |
| 最新 app-delivery Data Service 配置 | [#74956](https://github.com/tidbcloud/app-delivery/pull/74956) 已把 staging image 更新为 `master-f6fc862d...`；[#74935](https://github.com/tidbcloud/app-delivery/pull/74935) 已合并 metadata config，Argo generation 58 已持续生效；prod 仍未配置/部署本批 | staging merged image、Secret、DB、Gateway 与 post-merge E2E 全部通过；prod gap 不变 |

结论：同事改动覆盖了 DBAAS query 的外部 path 和 Gateway org/cluster 校验的一部分，但没有覆盖本批三条 metadata API。#1572 的独立 metadata repository 虽然读取 canonical Clinic DB、不是对 analysis DB 的包装，但仍把 Clinic 已有 datasource 查询语义复制到了 Data Service，因此在 #1594 中删除；新的 Clinic internal adapter 复用迁移前 datasource，Data Proxy `/data-proxy/api/v1` path/DTO 保持不变。后续正式面向普通 TiDB Cloud credential 开放前，应与 #1571 统一外部 path 和授权模型。

### 13.5 ACT-01 direct-ActivityHub staging E2E（historical baseline）

以下记录证明 #1591 direct 版本的 response/filters/empty-window parity，仅作为 facade 复验基线。`activity_hub` Data Service config、Secret 与 #74974 不是当前架构的发布前置。本节当时要求重新执行的 facade post-deploy E2E 已于 2026-07-21 完成，当前状态以 13 节的 current facade 记录为准。

ACT-01 使用与 metadata 相同的 Data Proxy public namespace，新接口为 `GET /data-proxy/api/v1/orgs/{orgID}/clusters/{clusterID}/activities`。历史 direct 批次在不修改共享 Deployment/Ingress desired state 的前提下完成隔离 E2E，临时资源已全部删除。该结果只曾作为 #1591 的代码合并 gate；当前正式 hostname 的 facade 验收由 #1594 与 app-delivery #75010/#75011 提供，和 #74974 无前置依赖。

| Gate | 脱敏结果 | 结论 |
| --- | --- | --- |
| Code/image provenance | Clinic [#1591](https://github.com/pingcap-inc/clinic/pull/1591) head=`1a1e1937...`；CD [run 29729277123](https://github.com/pingcap-inc/clinic/actions/runs/29729277123) 构建最终 feature image；app-delivery [#74976](https://github.com/tidbcloud/app-delivery/pull/74976) 已合并该 image bump | Pass |
| Deploy config/Secret | [#74974](https://github.com/tidbcloud/app-delivery/pull/74974) 仅为 staging 增加 `activity_hub.required: true`、固定 endpoint/source、Secret key 引用和 10 秒 timeout；所有 CI 通过但仍为 Draft/review required。远端 Secret 已包含目标 key，ExternalSecret=`Ready=True`；没有读取或记录值 | Config Pending |
| Real nonempty parity | 同一个真实 active Starter/AWS cluster、同一时间窗口：legacy=`200`、new=`200`，均为 3 条 activity，完整 JSON body 相等 | Pass |
| Real filter parity | 使用样本中真实 type/name/severity 组合：legacy=`200`、new=`200`，均为 2 条，完整 JSON body 相等。该样本 catalog 为空，因此 catalog 正向 live filter 未覆盖，参数透传由 unit test 覆盖 | Pass with noted sample gap |
| Empty-window semantics | 首轮发现 ActivityHub 对空窗口返回 HTTP `404` + 固定 `Activity not found.` body，新接口当时错误映射为 `502`；#1591 用严格、窄匹配修复后复验 legacy/new 均=`200`，new 固定返回 `activities: []` | Pass after fix |
| Gateway/auth | 隔离 Ingress 挂载 `clinic-auth`：无 key=`401`，staging Clinic key=`200`；临时 smoke hostname 只用于历史 direct 验证。当时尚未完成正式持久链路；当前 facade 已在正式 hostname 通过，不依赖 #74974 | Historical Pre-merge Pass |
| Server-side isolation | 注入 legacy cursor Header、placement/project Headers、Cookie 和攻击者 Authorization 后，HTTP status 与完整 response body 都不变；新接口只消费 query cursor，目标解析仍由 metadata ownership 和服务端配置决定 | Pass |
| Ownership/parameters | 错误 org、未知 cluster=`404`；非法 org、缺少 `begin_ts`、重复 scalar query=`400`；ownership 校验先于 ActivityHub 请求 | Pass |
| Cursor live parity | 使用合法 90 天窗口扫描 staging 68 个 active cluster，全部=`200`，均没有返回 cursor；真实第二页请求没有可用样本，明确记为 Not Run。探索期过宽的 180 天请求因上游窗口限制被排除，不计入失败；最终日志验收前已重启隔离 Pod | Blocked by staging data |
| Clean logs/health | 最小验收矩阵产生 2×`200`、1×预期 `400`、1×预期 `404`；无 panic/fatal/ActivityHub error，无 credential、样本 activity ID 或完整 payload；Pod Running/Ready、restart=`0` | Pass |
| Cleanup/regression | 隔离 Ingress/Service/Deployment/ConfigMap 与临时响应文件全部删除；正式 Deployment 保持 `1/1 Ready`、restart=`0`。正式 image 已是最终 feature tag，`/data-proxy/info` 和 metadata 仍=`200` | Pass |
| Formal persistent state（当时） | 正式 ConfigMap 尚无 direct `activity_hub` section，所以 key 请求 ACT-01=`503`、无 key=`401`；该 direct 架构已被 #1594 facade 取代 | Historical Pending（superseded） |

结论：上述 direct 方案的持久化前置已被 #1594 facade 架构取代，#74974 仅保留为可选清理。2026-07-21 已在 `https://staging-clinic.pingcap.com` 对当前 facade 重跑 nonempty/filter/empty/auth/negative/header-isolation/log/health 矩阵并标记为 Staging Pass；cursor 第二页仍因没有真实样本记为 Not exercised，不能用 unit test 代替 live 结果。

## 14. 风险与缓解

### 14.1 历史 `cluster_metadata` 启动依赖与排障边界（superseded）

本节记录 #1572 direct-DB 版本的历史排障证据。当前 facade 代码已删除 Data Proxy 的 `cluster_metadata` loader/repository，以下启动依赖不再适用于重构后的镜像；保留内容仅用于旧镜像回滚和历史事故对照。

本批对已有 HTTP handler 和业务路由是增量，但 metadata 初始化进入了 Data Service 的进程启动路径；因此不能把“已有 API 逻辑未改”理解成“任何 metadata 故障都绝不会影响已有 API 可用性”。后续排障必须先区分以下状态：

| 配置/依赖状态 | 启动与请求表现 | 对已有 API 的影响 |
| --- | --- | --- |
| `cluster_metadata` section 缺失（默认 `required: false`） | 不连接 metadata DB；三条新 metadata API 返回 `503 Unavailable` | 进程正常启动，已有 API 正常；这是未启用状态，不是旧功能回归 |
| section 已配置、`required: false`，但 DB Ping 失败 | 记录 repository unavailable；新 API 返回 `503` | 进程继续启动，已有 API 正常 |
| `required: true` 且配置、Secret、DNS/TCP、TLS、认证或 DB Ping 任一失败 | Router 初始化 panic，新 Pod 无法 Ready | 整个 Data Service 新 Pod 不提供 `/healthz` 或已有 API，rollout 可能卡住 |
| 共享 `config-dataproxy.yaml` 不可读、YAML 无法解析，或已提供的 metadata 字段非法 | 无法安全判断配置，Router 初始化 panic | 即使本意是 optional，也可能阻止整个进程启动 |

典型日志关键词为 `load cluster metadata config` 和 `initialize required cluster metadata repository`。若 metadata 发布后出现 Pod `CrashLoopBackOff`、rollout 超时或已有 Data Proxy API 同时不可用，按以下顺序排查，不要先假设是旧 handler 回归：

1. 核对实际运行 image、rendered ConfigMap 和 `cluster_metadata.required`，确认 Argo desired state 与 Pod mount 一致。
2. 只检查 ExternalSecret/Secret 的状态和 key 名 `CLINIC_METADATA_DB_USER`、`CLINIC_METADATA_DB_PASSWORD`，不得输出值。
3. 查看新 Pod 启动日志中的上述关键词，再从同 namespace 验证 DNS、TCP 端口、TLS、DB 认证和只读 `SELECT`。
4. 验证 `organizations.dbaas_org_id`、`clusters.org_id` 及 metadata 查询需要的列/schema；账号必须保持只读。
5. 若 `required: true` 的前置条件无法及时恢复，应通过正常 app-delivery 回滚/修复 desired state，不能依赖旧 Pod 长期兜底。

环境基线（2026-07-20）：staging 的 Secret key、网络、DB 权限/schema、required 配置和新旧公网 smoke 已验证；app-delivery #74935 已合并，持久 desired state 与 post-merge E2E 均通过。prod/dev 尚无 `cluster_metadata` section，因此默认 optional，当前尚未建立 prod DB 启动依赖；未来任何环境改为 `required: true` 前必须重复完成该环境 preflight。代码合并、镜像构建、app-delivery bump 与 Argo rollout 是独立状态，排障时必须分别核对。

### 14.2 当前 `clinic_api_base` 依赖与排障边界

重构后 metadata、Activity、components 共用已有 `clinic_api_base`，默认 staging/prod 值为 `http://clinic-internal-api:80`。没有新增 DB/ActivityHub Secret、Ingress、Service、IAM 或只读账号前置。

- 配置文件不可读、YAML 非法或 `clinic_api_base` 不是 absolute HTTP(S) URL 时，Data Proxy 启动失败；这是现有 internal client 的必需服务地址。
- Clinic internal Service DNS/TCP 不可达、internal Pod 未 Ready 或返回 5xx 时，仅这批 facade API 返回稳定 502/503/504；Data Proxy 不 fallback 到 direct DB/ActivityHub。
- 排障顺序：核对 Data Proxy/Clinic internal 两个 image 与 rollout → rendered `clinic_api_base` → Service endpoints/DNS/TCP → Clinic internal route/status/log → 对照 legacy Clinic API 与 public Data Proxy 响应。
- staging 验收必须在正式 hostname 用 Clinic key覆盖三条 metadata、Activity、components、wrong-org/not-found/invalid/no-key/Header-isolation，并确认两个 Deployment Ready/restart 与 system logs；一次完成，不逐接口重复创建隔离环境。

### 14.3 通用风险表

| 风险 | 影响 | 缓解 |
| --- | --- | --- |
| TiDB Cloud org + cluster 的查询/权限边界尚未完成生产验证 | staging DB 联合归属、共享 Gateway internal-key、跨 org 和 no-key 已通过，但 prod parity 尚未完成，仍可能出现环境差异 | staging 证据作为 implementation merge gate；prod 发布前原样执行真实 schema parity、跨 org 和 no-key tests |
| Clinic internal Service/route 不可用或 internal/public 版本不匹配 | metadata、Activity、components 同时 502/503/504，或 DTO/path 不兼容 | 同一 Clinic PR 同时交付 internal owner 与 Data Proxy client；部署时先确认 internal/public image compatibility，再一次性执行 facade E2E；保留旧 public route 作为回滚期基线 |
| Cluster detail 过度复制或过渡字段长期残留 | Data Service 引入 Jira/quota/feature config 强耦合，或 `projectId/parentId` 成为永久 caller routing API | 只实现 Investibook-minimal DTO；过渡字段列删除前置条件和 owner，禁止作为新 Handler 授权输入 |
| O11y 能力覆盖或 Data Service 网络/凭证不足 | 不同 tier 语义不一致，或错误地把 Clinic-owned 逻辑复制到 Data Proxy | 每个 capability 先分类 owner：数据面优先 O11y/provider；Clinic-owned 使用 canonical internal facade；只有无明确 owner 时才评审 Data Proxy 直连，禁止旧 public/NGM fallback |
| 把 legacy `/data-proxy/query` 的 caller-selected routing 复制到新 API | caller 可影响数据源/region/vendor，破坏租户隔离 | 将其视为仅限旧接口的兼容例外；新 resource API 只接受服务端 typed resolver 结果 |
| nested Go module 导致 resolver 重复 | 两套 tier 路由漂移 | interface + golden tests；长期共用时抽 shared module |
| Gateway path 上线但 resource map 缺失 | caller 401/403 或 internal-only 漏放 | route、allow-list、RBAC 与 Handler 同一 release checklist |
| 依赖 caller placement Header | IDOR/错数据源 | 完全由 ClusterContext 派生；伪造 Header negative test |
| 下载跨 cluster 读取 | 高严重度数据泄露 | opaque handle、path binding、prefix canonicalization、审计 |
| 旧 ConProf token 不安全且关闭策略未定 | 公开 key 可持续伪造任意 expiry，敏感 claims 泄露 | Phase 0 阻塞决策：优先 T0 关闭；若暂不能关闭，先补 auth/permission/fixed cutoff 并限定最多一个旧 TTL；新接口使用 opaque handle |
| O11y proxy 泄露入站 Header | 用户凭证到达下游 | upstream Header allow-list |
| metadata target 被利用为 SSRF | 服务访问任意内网/控制面 | HTTPS + host/port/path allow-list；禁止重定向到非 allow-list target |
| Token query 被边缘/APM 记录 | 下载能力泄露 | 全链路 query redaction、短 TTL、caller/purpose binding、no-store/no-referrer |
| Live 数据导致 old/new diff 波动 | 难判断 parity | 固定已结束短窗口；schema/aggregate 容差；必要时录制 fixture |
| OpenAPI 与 runtime 漂移 | consumer 按错误参数实现 | Router inventory check + generated diff CI + live consumer contract |
| Data Service nested tests/contract checks 覆盖不足 | PR 合并后才发现失败或 OpenAPI 漂移 | metadata PR 保持 focused runtime scope 并记录本地命令；Phase 0a 独立增加 nested Go job，并在修复历史 Swagger baseline 后接 OpenAPI semantic check |
| Root `make test` 用 pipeline 掩盖 `go test` 失败 | master/PR check 假绿，stale 或 DB fixture failures 长期不被阻塞 | 独立 baseline PR 为 test recipe 启用 `pipefail` 或保留 `go test` 退出码，并在启用前逐项修复/隔离当前 6 个已知 root failures；不得在 capability PR 中用 rerun 或无关改动掩盖 |
| 只更新一个 Skill 文件或一条 Investibook 链 | 文档看似完成但真实 workspace 仍请求旧 Clinic/NGM path | 10.4 逐项追踪 helper/sub-skill/builtin/三条 caller；以全新 workspace 安装和旧 QPS 为完成证据 |

## 15. 开放问题

用户已经确认的 org ID、范围优先级、O11y-first 原则和 Skill 交付要求已移入决策日志，不再作为开放问题重复询问。

### 15.1 Phase 0 阻塞项

1. 当前阶段是继续由 Kong/API Gateway 完成 authentication + RBAC，还是本次同时把认证/RBAC 移入 Data Service？无论选哪种，公开 Data Proxy Service 都不能被 caller 绕过 Gateway；Clinic canonical internal route 也必须保持集群内边界。
2. 旧 path 的 deprecation window、按 caller/credential 的零 QPS 观察期和 consumer sign-off owner 分别是谁？
3. ConProf 旧免鉴权 download/view 能否 T0 关闭？若不能，谁负责旧 route 的 authentication、cluster permission、fixed cutoff 紧急修复，最晚何时关闭？
4. opaque handle 的最大 TTL、caller/credential binding、是否一次性、并发/Range/replay policy、状态存储和轮换 owner 如何确定？
5. Statements 是否把 `resource_groups[]` 做成跨 deploy 的统一能力；`digest/plans[]` 是否从 runtime 宽松行为收紧为 required？
6. `projectId`、`parentId` 两个 Investibook 过渡字段分别由哪条 caller 依赖，何时以新 CP/files 和服务端 metrics parent routing 替换并删除？
7. 谁担任迁移文档 DRI、各 Phase Owner 和 Data Proxy/Gateway/Security/SRE/consumer Required reviewers？是否接受 2026-07-24 的 Phase 0 决策截止，以及 2026-07-20 的 Critical 0e owner/处置截止？

### 15.2 已记录但不阻塞本轮的候选项

- 跨 org fuzzy search（META-03）、ConProf config（CP-06）、OOM top-memory（FILE-OOM-03）、Top RU config（TOPRU-01）保持 Deferred Candidate。
- Activity detail/groups/file view、`clusters2` 和 Statement config/export/binding/resource-group companion API 不进入当前 Required/OpenAPI/DoD；若后续提升范围，先更新本文、API ID、依赖和验证 gate。

## 16. 决策日志

| 日期 | 决策 | 状态 | 说明 |
| --- | --- | --- | --- |
| 2026-07-17 | 公共服务根前缀沿用 `/data-proxy` | Approved | 与固定 Skill、现有 Router 和 app-delivery Prefix Ingress 一致；既有 query/schema/protocol path 不改 |
| 2026-07-17 | 新 first-party resource API 使用 `/data-proxy/api/v1` | Approved | 新增 versioned namespace；与固定 `/data-proxy` 根、已实现 Router、staging 部署及 caller guide 一致，既有 query/schema/protocol path 不变 |
| 2026-07-17 | 新 path 的 `{orgID}` 使用 TiDB Cloud/DBAAS org ID | Approved | 不向 caller 暴露 Clinic internal organization ID |
| 2026-07-17 | Cluster detail 以 Investibook 诊断所需最小字段为方向 | Approved | 精确 DTO 仍为 Proposed，由三条 caller contract tests 冻结；不复制 Jira/quota/feature enrichment |
| 2026-07-17 | ConProf single-profile view 和 Plan Replayer download 为 Required | Approved | 与各自 token/list 形成可用且可安全切流的完整链路 |
| 2026-07-17 | config、OOM top-memory、跨 org search、Top RU config 暂缓 | Approved | 保持 Deferred Candidate，不进入当前 OpenAPI/DoD |
| 2026-07-17 | O11y capability API 优先；有语义缺口时按业务 owner 选择 Clinic facade 或经评审的 Data Proxy 直连 | Amended 2026-07-20 | 禁止用旧 Clinic public/NGM compatibility API fallback |
| 2026-07-17 | 迁移完成前更新固定 Data Proxy Skill 及 helper/consumer | Approved | 不只改 `SUBSKILL.md`；须验证 Investibook 三条链和新 workspace 实际安装版本 |
| 2026-07-17 | placement 信息由服务端解析 | Approved | 不再把 NGM `x-*` Header 或 caller datasource selector 作为新公开契约 |
| 2026-07-17 | logical cluster 负责授权，parent/O11y ID 只作 effective target | Proposed | 解决 Premium/BYOC 映射 |
| 2026-07-17 | 新请求链不回调旧 Clinic public/NGM compatibility API | Approved | Clinic-owned 能力允许调用专用 canonical internal API；shadow/parity 仅测试使用 |
| 2026-07-17 | Metadata PR 不修改通用 GitHub workflow，也不提交本地 optional DB 示例 | Approved | nested-module CI/OpenAPI baseline 拆为 Phase 0a 独立 PR；真实 `required: true` 配置只进入 app-delivery 环境 overlay，避免把本地样例误当成已完成部署 |
| 2026-07-17 | Plan/OOM download 使用随机 opaque handle + 服务端状态 + prefix 校验 | Proposed | 不保留裸 provider/region/key；object key 不进入 URL/token payload |
| 2026-07-17 | 不把旧 ConProf 固定 JWT key 带入 Data Proxy | Approved | 新 endpoint 使用 opaque handle，新旧 endpoint pair 原子切换 |
| 2026-07-17 | 旧 ConProf 免鉴权 view/download 的关闭方案 | Blocked | 用户暂不确认；Phase 0 在“立即关闭”和“先加固 + fixed cutoff”之间决策 |
| 2026-07-17 | Plan/OOM 4a/4b 只分开发、不分发布 | Proposed | list 的 `downloadUrl` 依赖新 download route |
| 2026-07-17 | ConProf action-token 改为 typed POST | Proposed | 不签 raw caller `q`；必要兼容必须 decode/canonicalize |
| 2026-07-17 | Statements 最后迁移 | Proposed | 多源、聚合、binary plan 语义最复杂 |
| 2026-07-20 | 区分 Data Service analysis DB、Clinic canonical DB 与 Gateway DB 校验 | Approved | Data Service 既有 MySQL/legacy `clusters` 能力不等于 canonical metadata 数据源；#1571 Gateway ownership validation 可协同但不替代 metadata payload API。#1572 direct repository 仅作为历史实现/验证基线，#1594 删除它而不迁移其 SQL |
| 2026-07-20 | Metadata、Activity、Components 改为 Clinic-owned facade | Approved | Clinic internal server 是 facade owner；Metadata internal adapter 复用迁移前 `ListOrganizationAndCluster`、`GetOrganizationByCloudID` + `ListClusters`/`GetCluster`，Data Proxy 只保留 public contract、typed internal client、状态映射与观测。删除 Data Proxy direct metadata DB/ActivityHub 和 component datasource 重实现 |
| 2026-07-20 | facade 复用既有 `clinic-internal-api` Service 与 `clinic_api_base` | Approved | 不新增 Ingress/Service/IAM/Secret/DB account，也没有必需的 app-delivery 改动；[#74974](https://github.com/tidbcloud/app-delivery/pull/74974) 仅为可选的 direct-config 清理，Secret 值可在回滚观察窗后另行清理 |
| 2026-07-20 | Legacy Activity 与 mocked TiCDC compatibility 不随 facade 改变 | Approved | Legacy Activity 保留宽松参数/统一错误契约；mocked TiCDC 只留在旧 components route，canonical internal/Data Proxy 不返回 mock |
| 2026-07-21 | Top RU 采用 Data-plane capability resolver，不复制 NGM/O11y 聚合 | Approved | Clinic internal 只返回 server-resolved target；Data Service 固定调用 O11y summary/user_stats、安全转发并统一鉴权/错误。本批仅 plain logical Premium/Essential Required；Resource Pool、Dedicated、Starter、BYOC 与 config 暂不支持；无新增 app-delivery 配置 |
| 2026-08-10 | Top RU 跟随 O11y 拆分增加按 plan 查询 | Approved | canonical `summary-plan` 固定映射 O11y `summaryplan`；`digest` 不进入普通 summary；调用方只在 drill-down 时查询 plan rows，不在 Data Proxy 合并或扇出请求 |

## 17. 完成标准

本次迁移只有同时满足以下条件才算完成：

- 本文所有 `Required` API 的 Contract 为 `Approved`、Delivery 为 `Implemented`、Verification 为 `Pass`，对应 Owner、PR 和环境证据齐全；Candidate 可保持 Deferred，但不得出现在当前 OpenAPI/Router 或阻塞本轮完成。
- OpenAPI 与实际 Router/contract 一致，并有 CI check。
- Gateway/API key/RBAC/租户隔离 negative tests 通过。
- 支持的 tier/provider matrix 已验证；不支持项有明确、稳定的错误。
- Investibook source preparer、标准 sweep collector、targeted/ad-hoc clinic-cli 三条独立链，以及 Clinic UI 的 Required caller 都已切换；新 path QPS/错误率满足 SLO，仅允许按 10.3 规则批准且未过期的 caller-specific Exempt。
- `nutshell-skills` 的 Data Proxy SUBSKILL、shell/Python helpers、相关 sub-skills/index/changelog，以及 Investibook builtin skill/schema/tests 已更新、发布；全新 workspace 安装 revision 与 PR 一致并通过 tier smoke。
- 既有 `/data-proxy/query`、schema、metrics、serverless-parent metrics、Loki/system logs 回归 smoke 通过，未因新 namespace/middleware 改变既有 Skill 契约。
- 旧 path 在约定观察期内 QPS 为零，或只有明确豁免 caller。
- Data Service 新 Handler 不依赖旧 Clinic/NGM HTTP API；不再信任 caller placement Header。
- 下载/token 安全项通过评审，日志中没有 token、裸 object key、profile bytes 或敏感 SQL。
- Gateway/Ingress/APM query 已脱敏，resolver target allow-list、文件名净化和资源上限测试通过。
- 回滚演练通过；所有非豁免旧路由已用单独 PR 删除并 sign-off，豁免路由具有完整审批、补偿控制和到期/复审证据。

## 18. 代码与 consumer 参考

- 固定 Data Proxy Skill 契约：[nutshell-skills `data-proxy/SUBSKILL.md` @ `443ac493`](https://github.com/tidbcloud/nutshell-skills/blob/443ac49324e52cc60d7f4eb9f9768ef5dbb954d1/skills/platform/clinic-api/data-proxy/SUBSKILL.md)。同目录还需盘点 `scripts/clinic-data-proxy.sh`，并同步上层 `scripts/clinic_api.py`、`scripts/clinic-shell.sh`、相关 sub-skills/index/changelog。
- Data Service Router/middleware：`clinic_data_service/pkg/server/router.go`、`clinic_data_service/pkg/server/middleware.go`。
- 当前 Clinic internal client：`clinic_data_service/pkg/clinic-internal-api/client.go`。
- API Gateway auth/RBAC：`cmd/api_gateway/clinic/main.go`、`pkg/api_gateway/auth/api_key/api_key.go`、`pkg/api_gateway/rbac/permission_checker.go`、`pkg/api_gateway/rbac/resource_uri.go`。
- Cluster metadata/components：`pkg/api/cloud/global_dashboard.go`、`pkg/api/cluster/cluster.go`、`pkg/api/cluster/cluster_nextgen.go`、`pkg/datasource/cluster.go`。
- Activity：`pkg/api/activities/activities.go`、`pkg/activityhub_client/activityhub.go`。
- NGM route：`pkg/ngm/register.go`、`pkg/ngm/middleware.go`。
- Continuous Profiling：`pkg/ngm/conprof/service.go`、`pkg/ngm/conprof/conprof.go`。
- Plan/OOM：`pkg/ngm/file_download/service.go`、`pkg/ngm/file_download/impl.go`。
- Statements：`pkg/ngm/statements/statements.go`、`pkg/ngm/statements/service.go`、`pkg/ngm/statements/proto.go`。
- Top RU：`pkg/ngm/topru/service.go`、`pkg/ngm/topru/proto.go`。
- O11y target resolver：`pkg/o11y/urls/urls.go`。
- app-delivery Ingress：以 `origin/main@9d3aa7cb` 为审计基线；`clinic/clinic-data-service/base/ingress.yaml` 与 `staging/clinic/clinic-data-service/base/ingress.yaml` 都以 `/data-proxy` Prefix 转发，prod/staging overlay 分别挂载 Clinic auth plugin；staging base 另有 DBAAS Data Proxy Prefix。运行时状态仍需部署后动态核对。
- Clinic UI Statements：`../clinic-ui/packages/clinic-web/src/pages/cluster/SQLRepository/utils/context-impl.ts`。
- Clinic UI Top RU：`../clinic-ui/packages/clinic-web/src/pages/cluster/top-ru/_shared/api.ts`。
- Clinic UI Plan/OOM：`../clinic-ui/packages/clinic-web/src/api/ngm.ts`、`../clinic-ui/packages/clinic-web/src/pages/cluster/files.tsx`。
- Investibook source preparer：`../investibook/backend/services/source_preparer.py`、`../investibook/backend/tests/test_source_preparer.py`。
- Investibook standard sweep：`../investibook/agentkit/skills/builtin/tidb-clinic-diagnosis/scripts/collect_evidence.py`。
- Investibook targeted/ad-hoc caller：`../investibook/agentkit/skills/builtin/clinic-cli/scripts/clinic_cli.py`、`../investibook/agentkit/skills/builtin/clinic-cli/tests/test_clinic_cli.py` 及其 sub-skills；vendored `agentkit/skills/nutshell-skills/platform/clinic-api/` 也需同步或通过发布流程刷新。
