# 处方 — 补 `./web-components` 入口的类型声明：**它不是「没人管」，是一条带修法方向的具名豁免；真难点在两个没写进 fixDirection 的坑**

- 日期：2026-08-28 · 入口：`docs/round2-status.md` §30.2 第 3 条 / §30.3「如果只做一件事」
- 目标 sha：**起手 `3d9498ee` → 止 `fbfc3e4f`**（⚠️ 上次交接记的 `07d3498f` 起手已漂 **4** 步；**本 session 执行期间又漂 1 步**。DS 侧执行时按当时 HEAD 重核行号）
- **执行状态**：🟢 **已由 DS 落地** —— 亲验 pin **`7fcca917`**（2026-09-01，`lab:N93`）。🔴 **最硬的一条证据是豁免表空了**：`scripts/audit-exports-types-contract.mjs:60` 现为 `export const EXEMPTIONS = []` ⇒ 本处方针对的 `EXEMPTIONS[0]`（`./web-components`，`since: '2026-08-13'`，带 `fixDirection`）**已被删行**。而该闸的 **S4 是 shrink-only**（`:36-37` 逐字「豁免表里的条目若已经不再需要（该出口现在有 types 了），闸变红并要求删行」）⇒ **删得掉 = 该出口真的有类型了**，⛔ 这不是「把豁免删掉绕过闸」。旁证两条：`src/web-components/web-components.d.ts` 存在；`scripts/export-claude-design-bundle.mjs:284` DS 自陈「npm 包的 `./web-components` 出口**有**随包 `.d.ts`，React + TS 项目里那些标签是有类型的」。~~⚠️ **未复核项**：§2.6 那张「JSX 增广 × `@types/react` 五版本矩阵、必须双写、分界在 18.3」的结论**本轮 ⛔ 未重跑**（要真装 5 个 `@types/react` 版本跑 tsc）⇒ ⛔ 不得据本行断言「双写已正确落地、5 个版本都覆盖到了」~~，本行只断言**豁免已消、类型文件在位**
  - 🔴 **2026-09-14 销账：上面那句「未重跑」已过期 —— §2.6 已被重跑两次，就写在 §2.6 自己正文里。**
    ⚠️ 这是 `lab:N56`（知情面与登记面分叉）**在同一份文件内部**发作：§2.6 正文写着「已验」，
    而本行（文件头执行状态栏）仍写「未重跑」⇒ 正是 `AGENTS §26` 推论一「改分项的人只看那一段」。
    · **`lab:L3`（2026-09-03）** —— 在 DS 真发出的 719 行 d.ts 上重取（被测物 `dist-wc/tvu-web-components.d.ts`
      `sha256 3e4d80cd…34680` @ DS `d1ba0505`）：§2.6 测过的 **12 格逐格相同** ⇒ 「最小复现 ⇒ 真载体」那次外推**成立**；
      并**补上** §2.6 当年没测的 3 个 `G` 格（`17.0.2`/`18.0.0`/`18.2.0` 全绿）。
      报告 [`reports/2026-09-03-l3-jsx-matrix.md`](../reports/2026-09-03-l3-jsx-matrix.md)（10/10 全绿 +
      **4 个实证红格**证这条线能失败 + `MH4` 每格回读载体 sha）。
    · **`lab:L4`（2026-09-03）** —— 把 §2.6 自己登记的那条边界（tsc 版本维度）**关闭**：
      面 = **12 条 tsc 线 × 5 个 `@types/react` = 60 格**，双写正反两向 **60/60 全绿**（48 个红格证能失败）。
      报告 [`reports/2026-09-03-tsc-axis.md`](../reports/2026-09-03-tsc-axis.md)。
    ⇒ 🔴 **因此「先估成本再决定做不做」这个问法不成立** —— ⛔ 不需要装 5 个 `@types/react`，那件事已经做完了。
    ⚠️ **仍未覆盖的边界（⛔ 是边界，不是 TODO，逐字取自 L4）**：tsc 4.x 够不到（`moduleResolution: Bundler` 是 5.0 引入）·
    线间/patch 间不可插值 · `7.1` 及之后未测 · `baseUrl` 在 6.0 弃用/7.0 移除 ⇒ 照抄 §5 那份 tsconfig 跨到 TS 6/7 会红在装置上
- **执行状态重取**：@`d07be8e9`（2026-09-11 §30 全量重取，量具 `metrics/proposal-execution-status-refresh.mjs`）—— 锚 4/4 仍成立（⚠️ 行号已漂） ⇒ **结论不变**。

> ### 🔎 漂移已核（收尾重取，⛔ 非假设）
> `fbfc3e4f` 唯一新增 commit = `fix(gate): 清掉三件卡在「等 owner」的一行修 + 触发器 S 加第三行`，改 9 个文件。
> 与本处方涉及的 8 个文件求交集：**只命中 1 个 —— 正是核心闸 `scripts/audit-exports-types-contract.mjs`**。
> 逐行读 diff：改动是给三条 **fail-closed early-return 补 `exemptCount` 字段**，
> **`EXEMPTIONS[0]` 原样在**（`fbfc3e4f` 下行号 62/63/67）、`isCodeEntry` 与 S2/S3/S4 判据**一行未动**；
> `react-pilot/src/{wrappers,demos}/` 命中 **0**。
> ⇒ 🟢 **本处方全部读数与结论在 `fbfc3e4f` 下仍成立**，§4.1 的 56/4/3 处计数无需重取。
- 执行方：**DS 侧 session 在真源执行。lab 只出处方（§8 只读边界），⛔ 未改 DS 任何字节**（本处方全部实测在 scratchpad 仿包中完成，见 §2.0）
- 关联：`audit-exports-types-contract.mjs` 的 `EXEMPTIONS[0]`（`since: 2026-08-13`）

---

## 0. 一句话

**处方的形态被实测改写了。**

§30.3 把它写成「一个改动，解锁一整类消费方」——**方向对，但它不是一个改动**。实测：

1. 🔴 **这不是一个「没人发现的缺口」** —— DS 早在 **2026-08-13** 就登记了它，写成 `audit-exports-types-contract.mjs` 里的**具名豁免**，还逐字写好了 `fixDirection`。lab 的增量**不是「指出它」**，是下面两条。
2. 🔴 **`fixDirection` 说的「dts 插件」方向不充分** —— React consumer 真正需要的是逐 tag 的 `JSX.IntrinsicElements` 增广，`vite-plugin-dts` 从 `register.ts` 产不出来（§3.1 实测判别）。
3. 🔴 **有两个会当场把 master 弄红的坑，`fixDirection` 里一条都没有**：
   - **56 处 `@ts-expect-error` 会同时失效**（`TS2578`）⇒ 必须**原子提交**，⛔ 不能照搬 DS 自己那条「先删指令再扩范围」的先例（§4.1 说明为什么这次相反）
   - 动 37 个 `.tsx` ⇒ 撞 pre-commit **视觉闸**，需要 owner 说出 `VISUAL_COMMIT_APPROVED`（§4.2）——**这是个非技术前置，08-18 那次就是卡在这里只落了一半**
4. 🔴 **JSX 增广必须【双写】** —— 消费方是**一类会生成 React 代码的 AI 工具**（§1.2），其 `@types/react` 版本不可能统一。实测 5 个版本：单写 `declare module 'react'` 在 **17.x / 18.0–18.2 上静默失效**，单写 `declare global` 在 **19.x 上静默失效**，**只有两段都写才全覆盖**（§2.6）

---

## 1. 🔴 先订正入口里的三个前提（lab 实测）

| §30.3 / 交接 prompt 里的说法 | 实测 | 依据 |
|---|---|---|
| 「唯一缺 types 的代码入口」 | ✅ **成立** | 用闸**自己的** `isCodeEntry` 判据程序化跑 `package.json@3d9498ee`：**15 条 exports · 7 个代码出口 · 缺 types 的 1 个 = `./web-components`**（§2.1） |
| 「React + TypeScript 走官方路径**导入即报错**」 | ⚠️ **一半，措辞要改** | **纯副作用 `import '.../web-components'` 不报错**（实测 exit 0）。报错的是另两种用法：**具名导入 → `TS7016`**、**JSX 里用 `<tvu-button>` → `TS2339`**（§2.2） |
| 「lab 发现的缺口」 | 🔴 **不成立** | 它是 DS **已登记 + 已具名豁免 + 已写修法方向**的已知缺口，`since: '2026-08-13'`（§2.3） |

### 1.2 消费方是**一类 AI 工具**，不是某个 React 项目（owner 2026-08-28 澄清）

owner 逐字：「**Claude.ai/design 是消费 React 的其中一个 AI 工具之一，不是唯一**」。

这条改变了两件事：

| | 影响 |
|---|---|
| **优先级** | ✅ **维持 §30.2 第 3 条的排序**。⚠️ lab 曾据「Claude Design 走的是第三条分发轨道、且实测够不到内网 Gitea registry」推断「React consumer 当前为空」并建议降级 —— **那一步收窄错了**，Claude Design 只是这一类里的一个样本，本地运行的 AI 工具（能访问内网 registry 的那些）不受该限制 |
| **技术口径** | 🔴 **JSX 增广必须跨 `@types/react` 版本成立** —— 一类工具产出的版本不可能统一 ⇒ 直接催生了 §2.6 的五版本矩阵，并把 §7 未验项 2 从「交回 owner」变成「已实测闭合」 |

> ⚠️ **留档，不影响本处方**：`docs/CLAUDE_DESIGN_SETUP.md` 与 `export-claude-design-bundle.mjs:269`
> 逐字声明 Claude Design 那条轨道**有意不提供 `.jsx`/`.d.ts`**，且实测「Claude Design 访问不到内网 Gitea」。
> ⇒ **Claude Design 自己确实不经过本入口**；本处方服务的是这一类里**其余**能走 npm 的工具。
> 要不要让 Claude Design 也用上真实组件，属**分发**问题（内网 registry 够不到）而非类型问题，
> 且与 DS 现行设计意图相反 ⇒ 🔴 **那是另一件事，属 owner 的产品决策**，⛔ 不在本处方内。

> ### 1.1 这条顺带回答了 §30.2 第 4 条记账的那件事
> §30.2 把「那 25 条『有人管』是不是真在管」记了账、当前不做。
> **本处方撞到了其中一条样本：它是真在管的** —— 豁免带 `since` / `reason` / `fixDirection` 三个字段，
> 且 `S4` 是 shrink-only（修完不删豁免行 ⇒ 闸变红）。
> ⛔ **别据此推广到另外 24 条**（n=1，且是自选样本，非随机抽样）。

---

## 2. 实测证据

### 2.0 复现环境（⛔ 未碰 DS）

⛔ 不 symlink、⛔ 不在 DS 上跑 build。做法：scratchpad 里造一个**仿包**，`exports` 形态逐字取自
`package.json@3d9498ee` 的三个入口（`.` / `./chart` 有 types，`./web-components` 无），
产物形态与 DS 同形（`dist/` 有 `.d.ts`、`dist-wc/` 零 `.d.ts`）；
`typescript@5.9.3` / `@types/react@19.2.17` / `react@19.2.7` 只**只读引用** DS 的 `node_modules`。
可重跑脚本见 §6。

### 2.1 分母（用闸自身判据，⛔ 非模式匹配）

```
exports 条目总数      : 15
代码出口（闸口径）    : 7
其中缺 types          : 1 → ./web-components
```

**「其它桶」逐条抽查**（8 条非代码出口，全为字符串形态）：
`./style.css` · `./icons/manifest.json` · `./icons/svg/*` · `./eslint-plugin` · `./eslint-plugin/*` ·
`./tokens` · `./composition` · `./chart/style.css`

> ⚠️ **一处口径差，如实登记、非本处方范围**：`./eslint-plugin` 与 `./eslint-plugin/*` 是**字符串形态**
> ⇒ 按闸的 `isCodeEntry` **不受检**；而实测 `eslint-plugin/` 目录下 **0 个 `.d.ts`**。
> 即：**在消费方口径下它们也是「无类型的 JS 入口」，只是闸看不见。**
> ⛔ 本处方**不主张**给它们补类型（ESLint 配置极少被 TS 消费），只登记这条口径差存在。

### 2.2 三态 + 阴性对照（隔离跑，每态单独确认退出码）

| # | 场景 | `tsc` exit | 报什么 |
|:-:|---|:-:|---|
| 1 | `import '@ux-team/…/web-components'`（纯副作用） | **0** | — |
| 2 | `import { chart } from '…/chart'`（有 types 的对照） | **0** | — |
| 3 | `import { registerTvuElements } from '…/web-components'` | **2** | `TS7016` × 1 |
| 4 | JSX `<tvu-button size="M">` | **2** | `TS2339` × 2（开/闭标签各一） |

**阴性对照独立成立**：只留场景 1+2 ⇒ `exit 0` + **零输出行** ⇒ 环境本身不产假阳。

> 🔴 首轮跑时混进一条 `TS7016 react/jsx-runtime` —— **那是我复现环境的 `typeRoots` 配错**，
> 不是 DS 的缺陷。已修掉后重跑才取上表。⛔ 别把它读进 DS 的账。

### 2.3 根因（两条产线的差异，非配置疏漏）

| | `dist/`（有类型） | `dist-wc/`（零类型） |
|---|---|---|
| 配置 | `vite.config.ts:54` → `dts({ include: ['src'], insertTypesEntry: true })` | `vite.web-components.config.ts` **全文无任何 dts 插件**（22 行，已通读） |
| 实测产物 | `find dist -name '*.d.ts'` ⇒ **193** | `find dist-wc -name '*.d.ts'` ⇒ **0**（find 跑完，exit 0，无输出 = 真零，⛔ 非命令失败） |

闸的豁免行逐字：

```js
{ subpath: './web-components', since: '2026-08-13',
  reason: 'dist-wc/ 构建（vite.web-components.config.ts）不产出任何 .d.ts …',
  fixDirection: '给 web-components 构建接上 d.ts 产出（dts 插件或手写一份 CE 元素名 → 属性的声明），'
              + '产出后删掉本行、给该出口补 types 条件。在此之前 React/CE consumer 无类型（已知缺口）。' }
```

### 2.4 正向验证：**补上之后真能修好**，且不是假绿

给仿包补 `types` 条件 + 一份含 JSX 增广的 `.d.ts` ⇒ 场景 3、4 **全部消失**，`exit 0` 零输出。
**三向判别证明类型真生效**（⛔ 不是逃逸成 `any`）：

| 判别 | 用法 | 实测 | 说明 |
|:-:|---|---|---|
| 1 | `size="XXL"`（union 外） | `TS2322 Type '"XXL"' is not assignable to type 'ButtonSize \| undefined'` | 类型**真在检查** |
| 2 | `size="M"`（合法） | exit 0 零输出 | 不是「什么都报」 |
| 3 | `<tvu-slider />`（未在增广里声明） | `TS2339` | **不是通配索引签名兜底** ⇒ 🔴 **37 个 tag 必须逐个列** |

### 2.5 两条会让处方翻车的边界（都已实测出对照解法）

| 风险 | 条件 | 实测 | 解 |
|---|---|---|:-:|
| 增广伤到 Vue-only consumer | 无 react / 无 `@types/react` / 无 jsx 配置 | ✅ **exit 0 零输出**，且它顺带拿到了 `registerTvuElements` 的类型 | 无害 |
| d.ts 引用 `React.*` | Vue-only + **`skipLibCheck:false`** | 🔴 `TS2503 Cannot find namespace 'React'` | ⛔ 见 §3.3 |

> 🔴 第二条直接否掉了「复用现成产物」的省事做法：
> `react-pilot/src/wrappers/types.ts` 逐字写着 `children?: React.ReactNode`
> ⇒ **⛔ 不能把它原样搬进随包 d.ts**。同条件改用 React-free 类型后 ⇒ exit 0。

### 2.6 🔴 JSX 增广写法 × `@types/react` 版本矩阵 —— **必须双写**

**立项理由**：消费方不是某一个 React 项目，而是**一类会生成 React 代码的 AI 工具**（owner 2026-08-28 逐字：
「Claude.ai/design 是消费 React 的其中一个 AI 工具之一，**不是唯一**」）
⇒ 它们产出的 `@types/react` 版本**不可能统一**，写法必须跨版本成立。

每格判据 = **正反同跑**：合法值 `size="M"` 必须绿 **且** 非法值 `size="XXL"` 必须报 `TS2322`
（⛔ 只看「不报错」会把类型逃逸成 `any` 读成通过）。

| `@types/react` | **M** 只写 `declare module 'react'` | **G** 只写 `declare global` | **B 双写** |
|---|:-:|:-:|:-:|
| 17.0.2 | 🔴 失效 | — | ✅ |
| 18.0.0 | 🔴 失效 | — | ✅ |
| 18.2.0 | 🔴 失效 | — | ✅ |
| 18.3.31 | ✅ | ✅ | ✅ |
| 19.2.17 | ✅ | 🔴 **失效** | ✅ |

⇒ 🔴 **`B` 是唯一在全部 5 个版本上都生效的写法。** 分界点在 `@types/react` **18.3**：
它把 JSX 命名空间搬进了 `react` 模块（为 19 铺路），此前的版本只有全局 JSX；而 19 **移除了**全局 JSX。

> ### 🟢 2026-09-03 后续验证（`lab:L3`）—— 上表已在 **DS 真发出的 d.ts** 上重取
>
> ⚠️ **上表的口径是「最小复现」**（§2.0 标题逐字「复现环境（⛔ 未碰 DS）」）⇒ 它 ⛔ 不是
> 在 DS 真发出的那份 719 行 d.ts 上跑的。「最小片上成立 ⇒ 真载体上也成立」**是一次外推**。
> **L3 把这次外推验了**（报告 [`2026-09-03-l3-jsx-matrix.md`](../reports/2026-09-03-l3-jsx-matrix.md)，
> 被测物 `<DS>/dist-wc/tvu-web-components.d.ts` `sha256 3e4d80cd…34680` @ DS `d1ba0505`）：
>
> - 🟢 **上表测过的 12 格，在真载体上逐格相同** ⇒ 那次外推**成立**。
> - ✅ **补上上表当年没测的 3 个 `G` 格**：`17.0.2` / `18.0.0` / `18.2.0` 上单写 `declare global`
>   **全绿** ⇒ 完整承重图 = `global` 在 ≤18.2 唯一承重、`module` 在 19 唯一承重，
>   `18.3.31` 是唯一两块都行的版本 ⇒ 🔴 **双写 ⛔ 不是冗余，去掉任一块都会炸掉至少 1 个已测版本。**
> - 🔴 **⛔ 但这 ⛔ 不等于「以后换载体不用再验」** —— 证的是这一次外推没被推翻。
>
> ⚠️ **本次仍未覆盖的维度**：TypeScript 版本（L3 只测了 `5.9.3` 一个）。**登记为边界，⛔ 不是 TODO。**

> ### 🟢 2026-09-03 第二次后续验证（`lab:L4`）—— **上一条边界（tsc 版本维度）已关闭**
>
> owner 拍板立项后重取，报告 [`2026-09-03-tsc-axis.md`](../reports/2026-09-03-tsc-axis.md)，
> 同一份载体（`sha256 3e4d80cd…34680`，逐字未变）。
> **面 = 12 条 tsc 线 × `@types/react` 5 版本 = 60 格**，口径名 **`真载体口径 · tsc 轴 · 无 baseUrl`**。
>
> - 🟢 **`5.0.4` … `7.0.2` 全部 12 条线上，双写正反两向 60/60 全绿**（48 个红格证这条线能失败）。
> - 🔴 **上表那张承重图在 3 个 major 上逐格不变**（C3 = 一维，55 格比对、0 差异）
>   ⇒ 上表「分界在 18.3、必须双写」这个结论 **⛔ 不随 tsc 版本改变**，
>   且「⛔ Both blocks are required」的覆盖面**从 1 条 tsc 线扩到 12 条**。
> - 🔴 **`npm view typescript dist-tags` 实读 `latest = 7.0.2`** —— TypeScript 6/7 均已 GA
>   ⇒ 上表脚注里那个 `typescript@5.9.3` **⛔ 已不是 consumer 默认拿到的版本**。
>
> ⚠️ **仍未覆盖 / 新登记的边界（⛔ 都不是 TODO）**：
> ① **tsc 4.x 够不到** —— 口径里 `moduleResolution: Bundler` 是 5.0 引入的；
> ② **线间/patch 间 ⛔ 不可插值**（`6.0.0`–`6.0.2`、`7.0.0`–`7.0.1`、5.x 中间 patch 一个都没测）；
> ③ **`7.1` 及之后一格未测**（当前只有 `-dev` 预发布）；
> ④ 🔴 **`baseUrl` 在 `6.0` 弃用（`TS5101`）、`7.0` 移除（`TS5102`）** ——
>    §5 那份复现脚本的 tsconfig 用了 `baseUrl`，**照抄它跨到 TS 6/7 会整片红在装置上、⛔ 不是被测对象的问题**。

> ⚠️ **一处被实测推翻的说法，留痕备戒**：本处方早期版本（及 lab 在对话中给 owner 的第一次答复）写过
> 「React 18 的 JSX 命名空间在全局 ⇒ 18 用 `declare global`、19 用 `declare module`，两种写法不通用」。
> **那是凭记忆写的、已被上表推翻**：18.3.31 下 `M` 写法**照样生效**，真正的分界不是大版本 18/19 而是 **18.3**。

**双写对非 React consumer 无害**（三格对照，全部 `exit 0` 零输出）：

| 格 | 条件 | 结果 |
|:-:|---|:-:|
| 基线 | 纯 Vue TSX，不 import DS 包 | exit 0 |
| 对照 M | import DS，d.ts 只有 `declare module 'react'` | exit 0 |
| 实验 B | import DS，**双写（含 `declare global`）** | exit 0 |

> 🔴 **这一格差点被误判成「双写污染 Vue」**：首轮跑出 `TS2339 Property 'div' does not exist on
> type 'JSX.IntrinsicElements'`，看着就像 `declare global` 把 Vue 的 JSX 冲掉了。
> 实际是**我的 tsconfig `paths` 没映射 `vue/jsx-runtime`**（同一行还印着 `TS2875 … none could be found`）。
> 补上映射后三格全绿。
> ⚠️ **本 session 在这个坑上栽了两次**（前一次是 `react/jsx-runtime`，见 §2.2 注）——
> **`paths` 只映射包名、漏映射子路径，会让 JSX 相关判据整片假阳。** §6 的脚本已内置这两条映射。

---

## 3. 处方（本方推进决策，⛔ 不列选项）

### 3.1 走 **generator 路线** —— ⛔ 不用 dts 插件、⛔ 不手写

`fixDirection` 给的两个方向**都不采纳**，理由是实测的：

| 方向 | ⛔ 为什么不 |
|---|---|
| **dts 插件** | 它从 `register.ts` 能产出的是 `defineCustomElement()` 的返回类型（Vue 的 `VueElementConstructor`）。而 React consumer 报的是 `TS2339 … JSX.IntrinsicElements`（§2.2 场景 4），**JSX 增广不在任何 dts 插件的产出面内**；且 §2.4 判别 3 证明增广必须**逐 tag 精确列出**，无法由类型推导代劳 |
| **手写** | 会引入**第二份真源**。`register.ts` 头一行逐字：`AUTO-GENERATED by scripts/generate-react-bindings.mjs — do not edit by hand. Source: src/web-components/components.config.ts` ⇒ 手写的 d.ts 与 config 之间**没有任何机制保持同步**，正是本仓反复付代价的「SoT 副本」形态 |

✅ **采纳**：让**已有的** generator 多产一份产物。依据 —— 生成 d.ts 所需的信息，真源里**已经全有了**：

- `components.config.ts` 的 `ComponentConfig` 已带 `tag`（CE 标签名）/ `name` / `PropConfig.tsType` /
  `VModelConfig.valueType` / `EventConfig.handlerType`
- 渲染器 `scripts/lib/react-binding-templates.mjs` 的 `renderTypesFile()` **已经在产出**完整的
  `ButtonProps` + 字面量联合类型（实测产物 `react-pilot/src/wrappers/types.ts` 已核）

⇒ **零新真源、零新信息，只是同一个 generator 多写一个文件 + 多一段 JSX 增广。**

### 3.2 落位：⛔ 别直接产到 `dist-wc/`

`dist-wc` 是 `vite build` 的 `outDir`，`emptyOutDir` 会清空它。**直接产进去会被静默清掉。**

✅ 采纳：**generator 产到源码目录**（进 git、可 review、被根 `tsconfig` 的 typecheck 覆盖 ⇒ 这份 d.ts 自己也有闸守），
再由 `vite.web-components.config.ts` 的一个 inline plugin 在 `closeBundle`（清空之后）`copyFileSync` 到 `dist-wc/`。

> ⚠️ **强度声明**：「`emptyOutDir` 会清空」这条**未在 DS 上实测**（⛔ 不在 DS 跑 build，§8）。
> 支持证据是弱的：`dist-wc/` 现有 30 个文件 **mtime 完全一致**（08-28 12:17），无更早残留。
> **DS 侧执行时请先验这一条**：`touch dist-wc/_probe.txt && pnpm build:wc && ls dist-wc/_probe.txt`
> —— 文件没了 ⇒ 确实清空，按本节做；还在 ⇒ 可直接产到 `dist-wc/`，省掉 copy 步骤。

### 3.3 d.ts 的形态约束（每条都由 §2 实测支撑）

| # | 约束 | 依据 |
|:-:|---|---|
| 1 | **⛔ 不许出现 `React.*` 任何类型**（`children` 用 `unknown` 或自带最小定义） | §2.5 实测 `TS2503` |
| 2 | 🔴 JSX 增广**必须双写**：`declare module 'react'` **与** `declare global` **两段都要**（§2.6 五版本矩阵） | 单写任一段都会在某些 `@types/react` 版本上静默失效 |
| 3 | **37 个 tag 逐个列全** | §2.4 判别 3：无通配兜底 |
| 4 | 同时导出 37 个 `export declare const Tvu*` + `registerTvuElements()` | 修 §2.2 场景 3 的 `TS7016` |

**已实测通过的骨架**（节选，DS 侧由 generator 按 37 个 config 展开）：

```ts
export type ButtonSize = 'XS' | 'S' | 'M' | 'L'
export interface ButtonProps { size?: ButtonSize; children?: unknown }   // ⛔ 不用 React.ReactNode

export declare const TvuButton: CustomElementConstructor
export declare function registerTvuElements(): void

// 37 个 tag 的映射只写一份，两段增广都 extends 它（⛔ 别复制两份，会漂）
interface TvuIntrinsicElements {
  'tvu-button': ButtonProps & { ref?: unknown }
}

// ① 新版 @types/react（18.3.x / 19.x）只认这段
declare module 'react' {
  namespace JSX { interface IntrinsicElements extends TvuIntrinsicElements {} }
}
// ② 旧版 @types/react（17.x / 18.0–18.2）只认这段 —— ⛔ 两段缺一不可，见 §2.6
declare global {
  namespace JSX { interface IntrinsicElements extends TvuIntrinsicElements {} }
}
```

### 3.4 改动清单（4 个文件，**必须同一个 commit**，理由见 §4.1）

| # | 文件 | 改什么 |
|:-:|---|---|
| 1 | `scripts/lib/react-binding-templates.mjs` | 新增一个渲染函数产出随包 d.ts（复用 `renderTypesFile` 的类型渲染逻辑，但按 §3.3 剥掉 `React.*`）；**同时**把 `renderWrapper` 里那两行 `@ts-expect-error` 删掉 |
| 2 | `scripts/generate-react-bindings.mjs` | 多写一个产物（第 208 行 `writeFile(typesPath, …)` 旁边加一行）。⚠️ **同名文件也被另一份处方要改**，见下方顺序约束 |
| 3 | `vite.web-components.config.ts` | 加 inline plugin，`closeBundle` 时 copy d.ts 到 `dist-wc/`（依 §3.2 的先验结果决定要不要） |
| 4 | `package.json` | ① `exports["./web-components"]` 补 `types` 条件（**放在 `import`/`require` 之前**，条件顺序敏感）② ⛔ **别忘**：删掉 `audit-exports-types-contract.mjs` 的 `EXEMPTIONS[0]` 整条 |

> 🔴 第 4 项那个 ② 是**硬的**：`S4` 是 shrink-only —— 补了 `types` 却留着豁免行，闸**当场变红**
> 并逐字要求「请删掉这一行，别留着吸收下一次回归」。

> ### 🔴 3.5 本处方要**排在另一份之前**，⛔ 别并行
>
> 改动文件与 [`2026-08-28-claude-design-real-components.md`](./2026-08-28-claude-design-real-components.md)
> 求交集，命中 **`scripts/generate-react-bindings.mjs`**（两边都要让它多写一个产物）。
> ⇒ **先做本处方**：本处方会把「从 `components.config.ts` 渲染 CE 标签 + attribute 映射」写进
> `react-binding-templates.mjs`（JSX 增广正需要它），另一份**直接复用那段**换个输出格式即可。
> 反过来做会写两遍；并行做必冲突。
>
> ⚠️ DS 仓同期有**落地线 / 并行线**两条工作线在跑 ⇒ **执行 session 起手就开 worktree**。

---

## 4. 🔴 两个没写进 `fixDirection` 的坑

### 4.1 56 处 `@ts-expect-error` 会同时失效 ⇒ **必须原子提交**

程序化统计（`@3d9498ee`，⛔ 跑完不截断，分桶后已抽查「其它」桶）：

| 桶 | 文件数 | 处数 | 性质 |
|---|:-:|:-:|---|
| A `react-pilot/src/wrappers/` | **37** | **56** | **generator 产物** ⇒ 改模板即可 |
| B `react-pilot/src/demos/` | 2 | 4 | **手写** ⇒ 要人工改 |
| C 模板自身 `react-binding-templates.mjs` | 1 | 3 | 生成 A 桶那些指令的地方 |
| 其它（tests / docs / 归档 plan） | 29 | 46 | ✅ **抽查逐条确认与本议题无关**（多为 eslint-plugin 测试），⛔ 不计入影响面 |

**B 桶 4 处已逐条定位，全部同病因**（`@3d9498ee`）：

| 文件:行 | 逐字 |
|---|---|
| `react-pilot/src/demos/FormItem.tsx:440` | `{/* @ts-expect-error tvu-icon is not in JSX.IntrinsicElements */}` |
| `react-pilot/src/demos/Tooltip.tsx:167` | `{/* @ts-expect-error tvu-tooltip is not in JSX.IntrinsicElements */}` |
| `react-pilot/src/demos/Tooltip.tsx:176` | `{/* @ts-expect-error tvu-icon is not in JSX.IntrinsicElements */}` |
| `react-pilot/src/demos/Tooltip.tsx:182` | `{/* @ts-expect-error close tag */}` |

> ⚠️ 一处**处数 ≠ 出现次数**的实例，留痕备戒：按 `IntrinsicElements` 出现次数数，`Tooltip.tsx` 是 **2**；
> 按 `@ts-expect-error` **处数**数是 **3**（多出的是 `close tag` 那处，它不含 `IntrinsicElements` 字样）。
> **本表以处数为准。**

A 桶逐字长这样（`Button.tsx`）：

```tsx
// @ts-expect-error tvu-button is not in JSX.IntrinsicElements yet
<tvu-button ref={ref}>
```

**四态实测/推演**：

| 动作 | 结果 |
|---|:-:|
| 现状（有指令、无增广） | 🟢 绿 |
| **只加增广** | 🔴 红 — 指令失效，实测 `TS2578: Unused '@ts-expect-error' directive.` |
| **只删指令** | 🔴 红 — `TS2339` 暴露（§2.2 场景 4） |
| **两者同时** | 🟢 绿（§2.4 正向验证） |

> 🔴 **⛔ 别照搬 DS 自己那条先例 —— 这次顺序是相反的。**
> `audit-typecheck-scope.mjs` 头注释逐字记着：
> 「**两步的顺序是硬的：先删掉 harness 里那 5 处已失效的 `@ts-expect-error`，再扩根 include。**
> 反过来会让那条无条件 typecheck 当场变红 = 把红潜伏到 master（第九轮刚为此付过 6 天代价）。」
>
> **那次能「先删」，是因为那 5 处指令【已经失效】（错误本来就不存在了）。**
> **这次的 56 处指令【仍然有效】（`TS2339` 真实存在）** ⇒ 拆成两步，**无论哪一步在前都会红一次**。
> ⇒ 唯一安全解 = **§3.4 那 4 个文件 + 重跑 generator，进同一个 commit**。
> ✅ 好消息：A 桶 56 处全是 generator 产物，改模板 + 重跑天然就是一次原子改动；**要人工碰的只有 B 桶 4 处**。

### 4.2 🔴 非技术前置：需要 owner 的 `VISUAL_COMMIT_APPROVED`

改动会重写 37 个 `.tsx`。`audit-typecheck-scope.mjs` 头注释逐字记录了 08-18 那次**就是卡在这里**：

> 「08-18 只落一半的原因不是技术：删注释要动 `.tsx` ⇒ 撞 pre-commit 视觉闸
> （**判据是扩展名不是内容**），而 `VISUAL_COMMIT_APPROVED` 只能由 owner 说出口。owner 08-20 签核后两步一起完成。」

⇒ **DS 侧起手第一件事就是找 owner 要这个批准**，⛔ 别等改完了才发现提交不上去。
⚠️ 判据是**扩展名**，所以哪怕改动只是删注释行也照样撞闸。

### 4.3 ✅ 已确认**不受影响**的两项

| 项 | 结论 | 依据 |
|---|---|---|
| `check-dist-wc-freshness.mjs` | 不受影响 | 全文已读：`process.exit(0) // 永不阻塞 dev`，纯提示。改 `vite.web-components.config.ts` 只会触发一次 stale 提示 |
| `exports["./web-components"].require` | 本来就没问题 | 实测 `dist-wc/tvu-web-components.umd.cjs` **存在**（3133692 B） |

---

## 5. 验收判据（⛔ 文字总结不算数，要这三样的实际输出）

| # | 命令 | 通过判据 |
|:-:|---|---|
| 1 | `pnpm audit:exports-types-contract` | 自印行从 `7 个代码出口受检 · 1 条具名豁免` 变成 **`· 0 条具名豁免`**，且 `✓ … OK`、**exit 0**。⚠️ 注意它的 `S3` 在 `dist-wc/` 未 build 时会 **SKIP 并大声说明**——⛔ **SKIP 不是通过**，必须在 build 之后跑 |
| 2 | `pnpm vue-tsc --noEmit` + `node scripts/audit-typecheck-scope.mjs` | **exit 0**，且 ⛔ 零 `TS2578`（证明 56 处指令已同批清干净） |
| 3 | **consumer 侧实测**（§6 脚本，把仿包换成 `npm pack` 出来的真 tarball） | 场景 3、4 从红转绿；**且三向判别仍成立** —— ⛔ 判别 1（`size="XXL"` 必须报 `TS2322`）不过 = 类型逃逸成 `any` 的假绿 |

| 4 | **跨版本回归**：判据 3 在 `@types/react` **17.0.2 / 18.0.0 / 18.2.0 / 18.3.31 / 19.2.17** 各跑一遍 | 五格**全绿**。⛔ 少跑任一格都可能放过「某个版本上静默失效」——单写写法正是这样漏的（§2.6） |

> 🔴 **判据 3 的判别 1 是防假绿的关键**：只看「不报错了」会把「类型全变 `any`」读成成功。
> 🔴 **判据 4 是防「只在我这台机器上生效」的关键**：消费方版本不统一是本处方的立项前提之一（§1.2）。

---

## 6. 可重跑的复现脚本（DS 侧验收用）

```bash
# ⛔ 不碰 DS：造仿包 + 只读引用 DS 的 tsc/@types
DS=~/Documents/AICoding/VS_Code/tvu-design-system
SB=$(mktemp -d)/repro; mkdir -p "$SB/src" "$SB/node_modules/@ux-team/tvu-design-system/dist-wc"
# 验收时把上面这行换成：cd "$SB" && npm i "$(cd $DS && npm pack --silent)"
cat > "$SB/src/named.ts"  <<'EOF'
import { registerTvuElements } from '@ux-team/tvu-design-system/web-components'
registerTvuElements()
EOF
cat > "$SB/src/jsx.tsx"    <<'EOF'
export function App() { return <tvu-button size="M">Hi</tvu-button> }
EOF
cat > "$SB/src/negative.tsx" <<'EOF'
export function Bad() { return <tvu-button size="XXL">x</tvu-button> }   // 必须报 TS2322
EOF
# 🔴 tsconfig 的 paths 必须【连子路径一起映射】，否则整片假阳（本 session 栽过两次，§2.6 注）
cat > "$SB/tsconfig.json" <<JSON
{"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"bundler",
"jsx":"react-jsx","strict":true,"noEmit":true,"skipLibCheck":true,"types":[],"baseUrl":".",
"paths":{"react":["<@types/react 目录>"],
         "react/jsx-runtime":["<@types/react 目录>/jsx-runtime"]}},"include":["src"]}
JSON
# ⛔ 漏掉第二条 → 报 TS2875 + 连锁 TS2339，看着像「增广没生效 / 污染了别人」，其实是环境错
"$DS/node_modules/.bin/tsc" -p "$SB/tsconfig.json" --pretty false; echo "exit=$?"
# 判据：ok 那两个文件零错 且 negative.tsx 报 TS2322。⛔ 只看 exit 0 会把类型逃逸读成通过。
# 跨版本回归：把 paths 换成 @types/react@17.0.2 / 18.0.0 / 18.2.0 / 18.3.31 / 19.2.17 各跑一遍（§2.6）
```

---

## 7. 覆盖面如实声明（⛔ 别据此宣称「类型契约已守住」）

**已实测**：exports 分母（闸自身判据）· 三态 + 阴性对照 · 三向判别 · 正向修复 · Vue-only 无害性 · `React.*` 越界 · `TS2578` 错误码 · 56/4/3 处的分桶与抽查 · 两个相邻闸的影响面。

**⛔ 未实测，DS 侧执行时须先验**：

| # | 未验项 | 为什么没验 |
|:-:|---|---|
| 1 | `emptyOutDir` 是否真会清掉直接产进 `dist-wc/` 的 d.ts | ⛔ 不在 DS 跑 build（§8）。验法见 §3.2 |
| 2 | ~~**React 18** 下增广是否生效~~ | ✅ **已实测闭合，⛔ 不再需要 owner 裁定** —— 五版本矩阵见 §2.6，答案 = **双写**。⚠️ 未覆盖 `@types/react` 16.x 及 20+（前者已 EOL，后者未发布） |
| 3 | 真 tarball 的解析行为 | 用的是仿包。`files` 字段已含 `dist-wc` ⇒ 预期一致，但仍以 §5 判据 3 为准 |
| 4 | 37 个组件里，`tsType` 是否都能干净渲染成 React-free 类型 | 只核了 `Button` / `Input` 两个 config 的产物形态 |
| 5 | B 桶 4 处手写 demos 改完的连带影响 | **位置已全部定位**（见 §4.1 表下方），未展开的是改完的连带渲染影响 |

**本处方不触碰**：`./eslint-plugin` 的类型缺口（§2.1 口径差）· 那 8 条闸结构性看不见的规则（§29.2）· 14 件已归档 owner 待拍。

---

## 8. ⇒ 登记回 `round2-status.md` §30.4 下方（⛔ 不开新格）

第二轮已于 §30.4 冻结。本处方是 **§30.2 第 3 条推荐的落地产物**，⛔ 不是新的一格评审。
新发现按冻结纪律**只登记不展开**：

- **N67**：`./web-components` 缺 types 是**已具名豁免**项（`since 2026-08-13`），非新发现 ⇒ §30.3 措辞「lab 发现」应订正
- **N68**：「导入即报错」需改为「**具名导入 `TS7016` / JSX `TS2339`**；纯副作用 import 不报」
- **N69**：修它的真实代价 = **56 处 `@ts-expect-error` 原子清理 + owner 的 `VISUAL_COMMIT_APPROVED`**，⛔ 不是「一个改动」
- **N70**（口径差，记账不做）：`./eslint-plugin` 系两条为字符串形态 ⇒ 闸结构上看不见，而它们同样无类型
