# Part B-17 · 全链路重写方法论（CTO 张进）

> 由 SKILL.md V1.4.0 拆分而出（V1.1 引入，V1.2 增 ExecPlan 子章节）。SKILL.md 主文档只在 Part 选择表里指出"SmartETL 全链路改写走 B-17"；具体方法论看本文件。
>
> **核心区别**：B-17 跟 B-2 治理扫描互补——B-2 解决"有哪些 ETL 该治理"，B-17 解决"具体重写一条链路时怎么做才不留尾巴"。强调**全链路追到原始源**，不接受只重写最终 ADS。
>
> 内容来源：观远 CTO 张进的 SmartETL 完整改写经验。如果用户说"把这条链路重新做一遍" / "替换数据源" / "做副本页验收"，必走 B-17。
>
> 未删减原文版另见 [etl-rewrite-original.md](etl-rewrite-original.md)。

## 目录
1. [何时用 B-17](#b-171-何时用-b-17)
2. [4 件交付（缺一不可）](#b-172-4-件交付缺一不可)
3. [8 条硬规则](#b-173-8-条硬规则)
4. [标准工作流（5 步）](#b-174-标准工作流5-步)
5. [三层验收方法](#b-175-三层验收方法)
6. [差异追踪 5 步法](#b-176-差异追踪-5-步法)
7. [空快照处理（不能写"已完成"）](#b-177-空快照处理不能写已完成)
8. [标准交付物清单](#b-178-标准交付物清单)
9. [B-17 特有的常见坑](#b-179-b-17-特有的常见坑)
10. [完成标准](#b-1710-完成标准同时满足才能说替换成功)
11. [用 ExecPlan 管理重写工程](#b-1711-用-execplan-管理重写工程v12-新增)

---

## B-17.1 何时用 B-17

- 用户明确要把已有 SmartETL **完整**改写成 SQL 版 SmartETL
- 目标不仅是重建数据集，还包括**页面副本替换**和**卡片级验收**
- 需要把旧 ETL 当作只读参考，重新梳理页面、数据集、ETL DAG、源数据
- 需要判断差异到底来自页面配置、执行快照时点、还是某一层数据链
- 需要处理"上游空快照，但链路定义仍要重写完"的场景

如果用户只是新建一个 SQL 节点数据集，不涉及旧链迁移、页面替换和链路审计 → 走 B-3 ~ B-9，不用 B-17。

## B-17.2 4 件交付（缺一不可）

交付不是"SQL 能跑"这么简单，必须同时满足：

1. 旧链所有参与本次范围的 ETL 加工都被新 SQL 链替代
2. 新链只依赖原始表/表单/文件/系统内置表或本轮新 SQL 中间层，**不再依赖旧 ETL 数据集**
3. 新数据集能挂到页面副本上，且**卡片结果可与原页比对**
4. 对无法验收的部分，必须把阻塞点上推到真正的空源或权限/平台问题，**不能含糊写成"已完成"**

## B-17.3 8 条硬规则

- ✅ **全链路重写**：不能只改最终 ADS，必须继续上追，直到原始源
- ✅ **旧资产只读**：旧文档、旧 ETL、旧数据集只作为参考，不作为本轮交付对象
- ✅ **新结果放新目录**：用子目录隔离新旧对象，正式对象名保持业务原名
- ✅ **新 SmartETL 只允许 SQL 节点**：不要混回可视化清洗节点（FILTER_ROWS、JOIN_DATA、CALCULATOR 等）
- ✅ **SQL 输入只引用 input1/input2**：不要赌平台按对象名解析。引用顺序对应 `sources[]` 数组顺序
- ✅ **双层验收**：结构验收（字段、行列规模、依赖关系、是否 FINISHED）+ 数值验收（与原数据集或页面卡片对齐）
- ✅ **空快照不是"完成"**：原始源为空时只能写"新链已重写完成，但数值验收硬阻塞"
- ✅ **页面验收只看副本页**：不直接改原页

## B-17.4 标准工作流（5 步）

### Step 1. 锁定范围和命名

明确：
- 正式页面、正式数据集、排除范围
- 本轮新 ETL 目录和新数据集目录
- 建 ExecPlan，写清：正式范围 / 只读历史 / 命名规则 / 暂停条件 / 验收标准

### Step 2. 从页面往下重新拉血缘

**不要从 ETL 往上推**。从页面卡片清单重新盘点：
- 卡片绑定的数据集
- selector / 参数卡
- 页面默认筛选器
- 页面运行时 payload

对每个正式数据集，拉数据集详情和 ETL DAG。标注每个输入是：
- 原始源
- 旧 ETL 数据集
- 系统表
- 当前页面不在范围内的旁路输入

### Step 3. 严格判断"是否已经到原始源"

判断口径很严格：

- ✅ 表单、Excel、ADLS、数据库物理表、系统内置时间表 → **可以视为源**
- ❌ 输入是 `DATAFLOW` 且背后还有 ETL → **没到底，必须继续追**
- ❌ 不要因为名字叫 `清洗_*`、`ods_*`、`report_*` 就误判成源

### Step 4. 重建顺序：上游先，下游后

按"最上游老 ETL 先重写，再往下游收口"推进：

```text
1. 先重写被多个下游复用的清洗层 / 中间层
2. 再重写共享中间 ADS
3. 最后重跑最终消费 ADS
4. 最后再做页面副本切换
```

避免反复改下游输入。

### Step 5. 每个对象的重建模板

每个待重建对象固定记录：

- 原 ETL 定义文件
- 目标输出 selector
- 目标输出名称
- 结构对齐参考的 schema 数据集
- 需要替换成"本轮新数据集"的输入 override

**自动从 ETL JSON 编译 SQL 时的特殊坑**：
- `split(col, ']')[1]` 这类**数组下标不能误识别成字段引用**
- 分组维度重名时要去重
- 空公式列表不能生成非法 `select`
- 列名里如果带**换行或特殊字符**，最终 SQL 拼接不能破坏原标识符

## B-17.5 三层验收方法

### 数据集验收

每新建一层至少检查：
- 新 ETL 是否 `FINISHED`
- 新数据集是否**真实物化**（不是只保存了草稿 ETL）
- 行数、列数是否与旧对象一致
- 关键字段名是否一致

**结构不一致先修这一层，不要急着看页面。**

结构一致但页面不一致，优先检查：
- 上下游执行时点
- 页面筛选是否真正作用到卡片
- selector 是否还绑定旧 ds

### 副本页验收

**只换副本页**，原页保持不动。副本页先替换图表卡，再检查 selector 卡。

⚠️ **不要默认相信页面首屏渲染**——副本页常出现两类假差异：
- selector 还挂旧数据集
- 页面默认时间筛选器没真正传到卡片

### 卡片级验收法（推荐）

```text
1. 先抓原页运行时 payload
2. 把副本卡切到新数据集
3. 用原页真实 payload 去重放副本卡片
4. 这样可以把"页面联动问题"和"数据链问题"拆开
```

## B-17.6 差异追踪 5 步法

当页面只剩 1~2 张卡不一致时，**不要盲改 SQL**，按下面顺序压缩范围：

```text
1. 先对比新旧最终数据集的键集合
2. 再对比有差异的字段（不要一次看全表）
3. 把差异收缩到少量记录
4. 沿链路往上追，看差异最早出现在哪一层
5. 对该层再看：
   - SQL 定义是否不同
   - 执行时间是否落后于上游
```

**关键经验**：很多"SQL 错了"的假象，实际是因为新链吃到了**更早的快照**。如果定义相同但时点落后，先补跑最近一层上游，再补跑下游。

## B-17.7 空快照处理（不能写"已完成"）

如果上游是空的：

```text
1. 继续把链路定义完整重写到源头
2. 如平台允许，物化出新的空数据集
3. 在文档里明确写：
   - 根阻塞源是谁
   - 当前行列规模是多少
   - 哪些下游因此为空
4. 结论只能写成：
   "全链路 SQL 重写完成"
   "数值验收硬阻塞"

❌ 不能写成"已完成对齐"
```

## B-17.8 标准交付物清单

```text
output/<restart_tag>/
  ExecPlan.md              ← 范围、命名、暂停条件、验收标准
  modeling.md              ← 每个对象的建模决策、依赖关系、字段对齐
  evidence.md              ← 验收证据：行列数、键集合差异、卡片对比
  sql/<object_name>/       ← 每个对象的完整 SQL（可独立复跑）
  raw/                     ← ETL 保存前定义、执行结果、页面运行时 payload
```

## B-17.9 B-17 特有的常见坑

| 坑 | 表现 | 修复 |
|---|---|---|
| 把旧 `DATAFLOW` 当源头 | 只重写了半条链 | 严格按 B-17.3 判断"是否到原始源" |
| 只保存 ETL 没执行 | 误以为已经有新数据集 | 检查数据集是否物化、状态是否 FINISHED |
| 卡换了新 ds，selector 还在用旧 ds | 副本页假差异 | 卡片 + selector **都要换** |
| 时间筛选器没真正生效 | 直接拿页面数字做结论 | 抓原页 runtime payload 重放 |
| 看到少量差异就修 SQL | 没先检查执行时点 | 走 B-17.6 五步压缩 |
| 上游空快照时为了"有东西看"接受降级结果 | 文档写成"已完成对齐" | 必须按 B-17.7 写"硬阻塞" |

## B-17.10 完成标准（同时满足才能说"替换成功"）

- [x] 新链已追到原始源，不再依赖旧 ETL 数据集
- [x] 新 SmartETL 全部是 SQL 节点
- [x] 目标数据集已**真实物化**并运行成功
- [x] 页面副本已切到新数据集
- [x] 非阻塞范围内，**卡片级**结果与原页一致
- [x] 阻塞范围内，根因已被定位到真实空源或平台限制，**并明确留证**

---

## B-17.11 用 ExecPlan 管理重写工程（V1.2 新增）

> 借自 OpenAI Codex 的 ExecPlan 规范（[execplan-spec.md](execplan-spec.md)）。这套方法论的精髓：**让一个完全没上下文的新人，仅凭 ExecPlan 文档本身就能端到端继续这项重写工作。** SmartETL 全链路重写动辄跨多日、跨几十张表、涉及循环依赖拆解和副本页验收，正是 ExecPlan 的最佳适用场景。

**何时启用**：当本次重写工作满足以下任一条件，就开 ExecPlan：

- 涉及 5 张以上 ETL 重建
- 跨工作日（不能一次性收口）
- 包含循环依赖拆解（一动牵动多张表）
- 需要副本页 + 卡片级验收
- 上游存在空快照需要写明硬阻塞

**核心约束**（来自 ExecPlan 规范，照抄）：

- **自包含**：ExecPlan 不依赖任何外部上下文。读者只有当前工作树和这份文档。
- **活文档**：每个停顿点都要更新 Progress / Surprises / Decision Log，**不是事后补**。
- **可观察结果锚定**：验收标准写"卡片 X 在副本页用原 payload 重放，目标卡片 2 月销售额与原页一致到分"，不写"代码层面满足某个定义"。这条跟 B-17.7"空快照不能写已完成对齐"是同一个原则的两种表达。

**SmartETL 改写专用 ExecPlan 骨架**（拿去直接填空，不用从通用模板自己映射）：

```text
# SmartETL 全链路重写 · <项目代号>

本 ExecPlan 按 references/execplan-spec.md 维护，必须保持 Progress /
Surprises & Discoveries / Decision Log / Outcomes & Retrospective 始终为最新。

## Purpose / Big Picture

这次重写完成后，<业务方> 在 <副本页 URL> 上看到的 <卡片清单> 全部由
新 SQL 链 v2 数据集驱动，不再依赖任何旧 ETL；<指定卡片> 与原页数值
一致，差异 < 1%；<空快照阻塞表> 已明确根因留证。

## 范围 / 命名 / 验收（B-17.1 锁定项）

- 正式范围：<页面 ID> · 数据集 N 个 · ETL N 个
- 旧资产只读：<旧目录路径>，绝不修改
- v2 落地：ETL 目录 <NEW_ETL_DIR_ID> · 数据集目录 <NEW_DS_DIR_ID>
- 命名：dwd_xxx_v2 / dws_xxx_v2 / dim_xxx_v2，业务原名挂在 description
- 暂停条件：上游空快照 / 平台限制 / 副本页卡片 > 3 张数值偏差
- 验收：B-17.10 六项必须全勾

## Progress

- [ ] (待时间戳) 治理扫描完成（依赖图 / 循环组 / 复杂度 → analysis.json）
- [ ] (待时间戳) 第一批 5 张 ETL 写入 + 节点预览通过
- [ ] (待时间戳) 第一批 execute 落表 + ds preview 验数
- [ ] (待时间戳) 副本页卡片切换 + payload 重放对账
- [ ] ...（每张表、每次预览、每次 execute、每张卡片对账都拆条目）

## Surprises & Discoveries

- Observation: <字段名带隐藏换行 / <> NULL 把行过滤光 / SQL 字段名是 sql 不是 sqlScript / ...>
  Evidence: <task error 摘录 / preview 0 行截图 / payload 片段>

## Decision Log

- Decision: 把"门店信息_v1"降回纯 DIM，经营状态字段迁到 dws_store_operating_status
  Rationale: 该表处于 5 张表循环依赖中心，不拆循环就一直反向依赖订单明细
  Date/Author: 2026-XX-XX / <你>

## Plan of Work

按 B-17.4 的 5 步推进：锁范围 → 拉血缘 → 判断到原始源 → 重建顺序（上游先）
→ 每对象重建模板。先写最上游清洗层 v2，再共享中间 ADS，再最终消费 ADS，
最后副本页切换。

## Concrete Steps

每张表的具体命令、payload 路径、dataFlowId、taskId、节点 OUTPUT id（带 _out 后缀）
落到这里，方便接手人原样复跑。

## Validation and Acceptance

- 数据集层：guancli ds get <dsId> --brief 看行列数与旧对象差异 < 1%
- 卡片层：抓原页 runtime payload，副本卡切新 ds 重放，<指定卡片> 数值一致
- 空快照层：明确写"全链路 SQL 重写完成，<根阻塞源> 数值验收硬阻塞"

## Outcomes & Retrospective

每完成一个里程碑写一段：完成什么 / 还剩什么 / 经验是什么。最终对账后写
完整复盘：v1→v2 对齐了 N 张，硬阻塞 M 张，下游看板已切流 K 张。
```

**四个活文档章节怎么用**（关键）：

| 章节 | 实战用法 |
|---|---|
| **Progress** | 每张表的 direct-save / preview / execute / 验数四步都拆条目，时间戳记到分钟。失败的也写"已完成：写入；剩余：execute 报权限错"。30+ 张表的 Progress 应该有 100+ 条。 |
| **Surprises & Discoveries** | B-17.6 差异追踪每一步的发现都进这里。字段隐藏换行、`<> NULL`、relativeFieldAlias 错位、上游运行权限不足、UNION 列差——每个都附 task error 原文摘录或 preview 截图作 evidence。 |
| **Decision Log** | "为什么把门店信息降回 DIM"、"为什么放弃 v2 直留改 SQL 重写"、"为什么 dws_finance_order 不拆"。判断比代码更值钱，写清 Rationale 让接手人理解，不用反复决策。 |
| **Outcomes & Retrospective** | 单张表跑通 = 一段小复盘；批次完成 = 中复盘；整套链路对账完 = 总复盘。"v1→v2 对齐了 27 张，硬阻塞 3 张（根因都是 ODS 空快照），下游看板切流 8 张" 这种结论必须落地。 |

**小工程别用 ExecPlan**：单张表新建、单条 SQL 修复、单个报错排查——直接照 B-1~B-9 走，开 ExecPlan 是负担。判断阈值看本节顶部"何时启用"。

**深度参考**：完整 ExecPlan 规范见 [execplan-spec.md](execplan-spec.md)；OpenAI Codex 的 AGENTS.md 极简版调度规则见 [agents-rule.md](agents-rule.md)。
