# 提案 —— 把 `src/icons/generated/X.ts` 拆成「元数据模块 + 载荷模块」（`lab:U3` 候选）

🔴 **本文件是方案，⛔ 不是执行记录** —— 拆模块本身尚未获准，本轮只交付带推荐值的方案供 owner 拍板。
承接：[`reports/2026-09-04-ds-bundle-u2.md`](../reports/2026-09-04-ds-bundle-u2.md) §7、
[`docs/2026-09-02-open-items.md`](2026-09-02-open-items.md) §20.8③。

**被测树**：`~/.ai-ds-lab/wt/u2-bundle`（隔离 worktree，本轮只读探勘，未改一个字节）·
DS 起手 `ed25b3ec`，⚠️ 主工作树 `origin/master` 现取已到 `40017acf`（并行 session 仍在改
`src/icons/*`，见下方风险①）。

---

## 1. 根因复述（已证，非推理，见报告 §7.2）

`src/icons/generated/X.ts`（18 个类目文件）每个都在**同一个模块**里同时导出：
- `xIcons`：SVG 载荷（逐条 `import` 自 `raw.ts`，全仓 1.1 MB）
- `xIconManifest`：纯元数据（`name` / `category` / `exportName` / `aliases` / `tags` / `figmaNodeId`，
  **不含 SVG**）

⇒ 任何只想读元数据的代码（`manifest.ts`、以及本轮新写的 `lazy.ts`）import 该模块时，
**载荷跟着进来** —— 换桩实验实测 `one.js`：`1,305,040 B` / 643 SVG → **`12,310 B`** / **0 SVG**（−99.1%）。

## 2. 🔴 推荐方案（一个值，非菜单）

**把每个 `generated/X.ts` 的 `xIconManifest` 数组字面量搬进新文件 `generated/manifest/X.ts`，
原文件只保留 `xIcons` 载荷 + 对 `raw.ts` 的 import + 现有具名 re-export（`IconAdd` 等）。**

```
src/icons/generated/
  action.ts          ← 保留：xIcons、对 raw.ts 的 import、具名 re-export（不变）
                        删除：xIconManifest 导出（搬走）
  manifest/
    action.ts         ← 新增：只导出 xIconManifest，只 import type { IconDefinition }
                        零 import 自 raw.ts、零 SVG 字符串
  ...（18 类同形态）
```

**只改一个消费点**：`src/icons/manifest.ts` 的 18 行 import 改指
`./generated/manifest/X`（而不是 `./generated/X`）。

**⛔ 不改的**（公共 API 零改动，逐条核对）：
- `registry.ts`（18 行 import `xIcons`）—— 它本来就只要载荷，继续指向原文件，**一字不动**。
- `index.ts`（barrel re-export 具名图标组件）—— 同上，**一字不动**。
- `lazy.ts`（本轮 U2 已写好）—— 它 import `iconManifest` 来自 `./manifest`，
  `manifest.ts` 改了以后它**自动受益**，`lazy.ts` 自己**零改动**。
- `resolveIcon` / `iconRegistry` 具名导出 —— 语义、名字集合逐字不变。

⇒ **改动面 = 18 个新文件（纯数据搬迁）+ 1 处 import 路径改（`manifest.ts`）**，
⛔ 不碰 `registry.ts` / `index.ts` / `raw.ts` / 任何 `.vue` 文件。

### 2.1 为什么推荐这个而不是别的形态

| 候选 | 为什么不选 |
|---|---|
| 保留原文件位置，只是把 `xIconManifest` 挪到文件**末尾**并标注 `/* @__PURE__ */` | U2 已实测：给 `registry.ts` 顶层对象加 `@__PURE__` 无效（报告 §3.3 尝试 b）—— **同模块**这件事本身就是病灶，位置/标注治不了 |
| 反过来把 `xIcons` 挪出去、`xIconManifest` 留在原文件名下 | 会让 `registry.ts` / `index.ts`（18+18 处 import）全部要求改路径 —— 改动面从「1 处」变成「36+ 处」，且这两个文件本来就该继续拿载荷，语义上不该改名 |
| 一次性把 catalog 侧 28 个文件也拆（见 §4） | catalog 侧目前**没有**任何调用方走「只读元数据」这条路径（`catalog/manifest.ts` 唯一消费方是谁尚未查证，见 §4），本轮证据只覆盖 eager 侧 18 个 —— 扩大范围要额外举证，属于换载体外推 |

## 3. 🔴 与「切换到懒加载」解耦 —— 分两个决策，不要捆绑

`§20.5.3` 记的代价**仍在**：把生产路径从 `registry.ts`（同步）换成 `lazy.ts`（异步）会让
10 个测试红（挂载后立刻断言 svg 存在那一类）。

**本方案只做「模块拆分」，不做「切换消费路径」**：

1. **本提案范围** = 只拆 `generated/X.ts`，`manifest.ts` 改 import。
   `Icon.vue` / `Logo.vue` **继续走 `registry.ts`**（同步、不变）—— 此时 `lazy.ts` 存在但**尚无生产调用方**，
   只有 `manifest.ts` 变瘦。**C2（全量 vitest 绿）预期不受影响**，因为生产运行时路径零改动。
2. **后续独立决策** = 是否、何时把 `Icon.vue` 从 `registry.ts` 切到 `lazy.ts`。
   这一步才会触发异步渲染代价，需要先解决"挂载后立刻断言 svg 存在"这类测试，
   **⛔ 不在本提案范围**，交 owner 单独排期。

⇒ 拆完模块本身即可验证「懒加载路径的元数据面已经变轻」，但**生产行为不变、测试不变**——
把两件事分开做，风险面更小、可独立回滚。

## 4. ⚠️ 附带发现：catalog 侧（28 个文件）有同源缺陷，⛔ 本提案不处理

`src/icons/catalog/generated/*.ts`（28 个类目）同样在**同一模块**里既导出 `xIcons`
（SVG 字面量直接内联，比 eager 侧更重）又导出 `xIconManifest`，
而 `catalog/manifest.ts` 的 18 行 import 全部指向这些同模块文件 —— **同一种耦合**。
⚠️ **本提案 ⛔ 不处理这 28 个文件**：本轮证据（换桩实验）只在 eager 侧做过，
catalog 侧是否同样能拆、拆完能省多少，**未实测**，属于后续独立候选。

## 5. 🔴 开工前置（owner 需先确认，⛔ 本轮未查证）

**这 18 个 `generated/X.ts` 文件的生成方式不明确**：
本轮 grep 全仓 `figma-sync/` 与 `scripts/` 未找到任何脚本以 `src/icons/generated/` 为写入目标
（`figma-sync/icon-artifacts.mjs` 写的是 `catalog/generated/`，是另一套）。
`git log --follow` 显示这些文件由手工提交的 feature commit（如
`feat(icons): affordance/synonym SoT layer`）写入，**未见头注释标注"自动生成，勿手改"**。

⇒ **两种可能，需 owner / DS 侧确认**：
1. 确有生成器，只是不在本仓 / 未在这几个脚本里 —— 若是，需先找到它并同步改造（生成两份文件而非一份），
   否则下次重跑生成器会把拆分打回原样。
2. 这些文件事实上是手工维护的（尽管取名 `generated/`）—— 若是，本提案是一次性机械搬迁
   （数组字面量原样剪切到新文件，**零语义改动**，可写一个一次性脚本辅助搬迁并核对搬迁前后
   `xIconManifest` 逐条 deep-equal），之后没有"重新生成会覆盖"的风险。

**⛔ 本提案在这一点确认前不建议开工** —— 顺序反了会导致要么改错目标、要么改完被生成器覆盖。

## 6. 验证计划（复用已有量具，⛔ 不新造）

拆完后按 U2 同一套判据复核，**判据延续 U2 预注册（`docs/2026-09-04-ds-bundle-preregistration.md`）双向写死的口径**：

| 判据 | 工具 | 期望 |
|---|---|---|
| 元数据面真的变轻了 | `probes/ds-bundle/measure-import-cost.mjs`（用 `lazy.ts` 路径当 `ONE`，⛔ 不切换 `Icon.vue`）| `one.js` 逼近换桩实验读数量级（`12,310 B` 附近），⛔ 不承诺逐位相同（真实 manifest 比换桩存根大） |
| 1290 个名字一个不丢 | `probes/ds-bundle/icon-name-coverage.mjs` | 拆分前后 eager 侧可解析名字数**逐位相同** |
| 公共 API 零改动 | 程序化取主入口具名导出集合（C1 同法）| 51 → 51 |
| 全量测试绿 | `pnpm vitest run` | **本提案范围内应为全绿**（因为生产路径未切换）—— 若红，说明拆分本身有回归，非 §3 那 10 个已知红 |

## 7. ⇒ 请 owner 拍的一件事

**是否同意：① 先确认 §5 那个生成器开工前置、② 按 §2 推荐方案执行「只拆模块，不切消费路径」、
③ 切换 `Icon.vue`/`Logo.vue` 到 `lazy.ts` 作为独立后续决策不在本次范围内。**
