# flow-renderer：reject edge "走全图顶部" 假设在大图上崩溃

**日期**：2026-05-17
**场景**：用 `docs/templates/flow-renderer.mjs` 渲染机器人管理 25 节点 / 28 行流程图，发现 G5 驳回（row 15 finance → row 13 sales）的红色虚线一路爬到 SVG 最顶部（y=78），横跨 2000+ px 才到目标节点，目测明显是 bug。
**影响范围**：所有用 flow-renderer 渲染的"长流程图"（row 数 ≥ 5 且包含 reject edge）。小图（business-trip 9 节点 / row 0-5）感觉不到。

## 直接原因

`renderEdge()` 里对 reject style 写死了"走 `REJECT_CORRIDOR_Y` 顶部廊道"：

```js
// 原代码（v1.0）
if (isReject) {
  pathD = `M ${fromTop.x} ${fromTop.y} L ${fromTop.x} ${REJECT_CORRIDOR_Y}
           L ${toTop.x} ${REJECT_CORRIDOR_Y} L ${toTop.x} ${toTop.y}`;
}
```

`REJECT_CORRIDOR_Y = HEADER_BOTTOM_Y + CORRIDOR_TO_HEADER = 40 + 38 = 78`，即 SVG 整图最顶部下方 38 px。

不管 from 在 row 0 还是 row 15，reject 都要先垂直爬到 y=78 再横移再下来。

## 元根因（**值得记的部分**）

**设计假设的"规模盲区"**——v1.0 是为 business-trip 那种 9 节点小图设计的：
- row 0-5，全图高度 < 900 px
- reject 都是"驳回回起点"（row N → row 0 附近），走顶廊道**就是就近廊道**
- 9 节点小图测出来视觉良好 → 通过

但**没有覆盖**"row 大、reject 是局部反向（差 1-3 row）"的情形：
- 28 行图，reject 是 row 15 → row 13，理论上应该走 row 12.5 通道
- 全图顶部 y=78 距离 row 13 节点（y=1930）≈ 1850 px，路径几乎跨全图
- 视觉上像"误连"，业务方一眼看不懂

**这类 bug 的特征**：单个测试用例通过 + 假设隐含在数字常量里 + 用例规模放大后才暴露。

## 修复（应用层）

reject 走"两节点中较上方者再上半行"的就近廊道，顶部用原 `REJECT_CORRIDOR_Y` 兜底（避免 row 0 附近的 reject 出 SVG 边界）：

```js
if (isReject) {
  const minRow = Math.min(from.row, to.row);
  const channelY = Math.max(
    REJECT_CORRIDOR_Y,
    TOP_OFFSET + (minRow - 0.5) * ROW_GAP
  );
  pathD = `M ${fromTop.x} ${fromTop.y} L ${fromTop.x} ${channelY}
           L ${toTop.x} ${channelY} L ${toTop.x} ${toTop.y}`;
}
```

效果：
- robot-lifecycle G5 驳回（row 15→13）：channelY = 1882（在 row 13 节点上方 48 px）
- robot-lifecycle 交付审批驳回（row 17→16，同 sales lane）：channelY = 2302（同 column 内 U-turn，路径短）
- business-trip 两条 reject（都终点 row 0）：channelY clamp 到 78，**行为跟修复前完全一致**
- 验证：headless chrome 渲染对比 business-trip 修复前后视觉无差异

## 工程化保险（避免同类问题重复出现）

**模板/渲染器的视觉常量**（CORRIDOR / OFFSET / GAP 这些）容易让单一规模"测试通过"骗过 review。建议给 `docs/templates/flow-renderer.mjs` 加：

1. **多规模回归 fixture**：除了 business-trip（9 节点），加一份 ≥ 20 行的 fixture（robot-lifecycle 已经是了），CI 跑两个都生成 SVG，diff against snapshot。
2. **特征用例覆盖**：每种 edge style（normal / approve / reject / async）至少覆盖"短距离同 row"+"短距离跨 row"+"长距离跨多 row"三个用例。
3. 模板/渲染器代码 review 时**显式追问**："这个布局常量在 row=0 / row=5 / row=20 时分别是什么效果？"——常量驱动的视觉逻辑必须按规模采样验证。

## 类似的 "假设规模"盲区

- 字号常量：小图 FS=13 OK，大图缩到 50% 后字变 6.5 px 看不清
- ROW_GAP 固定：row 数太多时 SVG 高度超 5000 px 触发浏览器渲染分页
- viewBox crop 模式（如 `--split-header` body 段）在 row=0 时是否退化也要测

## 跟同期 learnings 的关系

- `2026-05-17-swimlane-tooling-selection-traps.md`（PR #412 配套）— 选型层面的踩坑
- `2026-05-17-esm-node-path-createrequire-trap.md`（PR #412 配套）— renderer 依赖解析
- 本 learning — renderer 视觉常量的"规模假设盲区"

三个一起构成 flow-renderer v1.x 时代的完整经验：选对工具 → 装得起来 → 单测通过不代表多规模通过。
