# Codex Prompt: tsconfig include 扩 playground — Round 2 (修 21 type 错误 + 永久扩 include)

> **目标**：永久把 `tsconfig.json` `include` 扩到 `["src/**/*", "playground/**/*"]` + 修完 dry-run 暴露的全部 21 个 type 错误，让 vue-tsc 0 错误通过。
>
> **估时 1.5-2.5h。不要 commit。**

---

## 背景

Round 1 dry-run（[报告](../_spikes/tsconfig-include-extension-dryrun-report.md)）扩 include 暴露 21 个 type 错误，分 4 类（模块解析 / Vue 推断 / 组件 prop 不一致 / readonly array）。Round 1 已还原 tsconfig，本轮 Round 2 一次性永久扩 + 修完。

---

## 必读前置

1. **`AGENTS.md`** — 含硬规则 + 工作流
2. **`docs/meta-rules.md`** — 反模式清单 + 触发器
3. **`docs/internal/_spikes/tsconfig-include-extension-dryrun-report.md`** — Round 1 dry-run 报告（含所有错误清单 + 修法建议）
4. **`tsconfig.json`** — 主 tsconfig
5. 修复目标的 8 个文件（见任务清单）

---

## 任务（9 步，按顺序）

### 任务 1：永久扩 tsconfig include

```diff
- "include": ["src/**/*"]
+ "include": ["src/**/*", "playground/**/*"]
```

只改这一行，其它 compilerOptions 保持。

### 任务 2：加 `*?raw` ambient module 声明

新增文件 `playground/types/raw.d.ts`：

```ts
declare module '*?raw' {
  const content: string
  export default content
}
```

修复 `designAssetData.ts` line 1-4 的 4 个 `Cannot find module '...?raw'` 错误。

**验证**：扩 include 后 tsconfig 自动包含 `playground/types/raw.d.ts`（因为 `playground/**/*` glob 匹配）。

### 任务 3：修 `CheckboxPage.vue` Status 收窄问题（5 错误）

错误位置：line 156, 164, 172, 206, 217

**根因**：[`src/canonical/CheckBox.vue`](../../src/canonical/CheckBox.vue) 的 `Status` union 是 `'off' | 'on' | 'some'`，但 page demo 把 handler 类型写成 `(value: 'on' | 'off') => void`——比 canonical 窄。canonical 的 emit 可能传 `'some'`，page handler 接不住。

**修法**：把 page demo handler 类型 widen 到 `Status`（导入 canonical 的 Status 类型，或用 `'on' | 'off' | 'some'` 字面量）。

```diff
- (status: 'on' | 'off') => ...
+ (status: 'on' | 'off' | 'some') => ...   // or import { Status } from canonical
```

**注意**：handler 内部 logic 可能假设 only on/off——如有，先收窄 input：`if (status === 'some') return; ...`，再当 on/off 处理。或保持 logic 但改 type，不改逻辑。

### 任务 4：修 `RadioPage.vue` helper 签名（7 错误）

错误位置：line 144, 152, 160, 200, 230, 238, 246

**根因**：helper 函数签名要求 `Ref<"producer">` 等，但 call site 传字面量 `'producer'`——签名跟用法对不上。

**修法**（Codex 推荐）：改 helper 签名为 `(currentRef, optionValue, event)`——拆分 ref 跟字面值。

具体：

1. 看 RadioPage.vue 顶部 helper 定义（搜 `function` / `const` 找到 helper）
2. 把签名从 `(ref: Ref<...>, event)` 改为 `(currentRef: Ref<string>, optionValue: string, event: Event)`
3. 7 个 call site 加上 optionValue 参数

**判断题**：如果 helper 内部本来就只用 ref 没用 literal，那只是签名错；改完 logic 不变。如果 helper 内部需要 literal，可能 logic 也错——但这种情况 dry-run 没暴露，先按签名错处理。

### 任务 5：修 `BadgePage.vue` v-for color 类型（1 错误）

错误位置：line 187

**根因**：模板 `v-for="color in colorList"` 时 colorList 推断为 `string[]`，传给 `<Badge :color="color">` 但 Badge 的 color prop 是 `Color` union。

**修法**：把 colorList 显式类型化为 `Color[]`（导入 canonical Badge 的 Color 类型）：

```ts
import type { Color } from '@/src/canonical/Badge.vue'   // 或 wherever the type lives
const colorList: Color[] = ['Black', 'Blue', 'Green', 'Orange', 'Red']
```

或用 `as const`：

```ts
const colorList = ['Black', 'Blue', 'Green', 'Orange', 'Red'] as const
```

`as const` 让数组推断为 readonly literal tuple，`Badge.color` 接受 `'Black' | 'Blue' | ...`，自动通过。

### 任务 6：修 `DocsShell.vue` PageLoader 类型（1 错误）

错误位置：line 67

**根因**：`PageLoader = () => Promise<unknown>`——太宽，不符合 `defineAsyncComponent` 期望的 `AsyncComponentLoader<Component>`。

**修法**：

```ts
import type { Component, AsyncComponentLoader } from 'vue'

type PageLoader = AsyncComponentLoader<Component>
```

或显式：

```ts
type PageLoader = () => Promise<{ default: Component }>
```

任选。

### 任务 7：修 `CanonicalShowcase.vue` variant.feature 收窄（1 错误）

错误位置：line 146

**根因**：`variant.feature` 类型是 `string`，传给 `getOptionsForFeature` 期望 `'time' | 'date' | 'default'`。

**修法**：

- 看 variant 来源类型，改 declaration 让 feature 是 `'time' | 'date' | 'default'`（如有 SelectVariant interface）
- 或在 call site 显式 narrow：`getOptionsForFeature(variant.feature as 'time' | 'date' | 'default')`

优先**修源头 type definition**，不优先 cast。

### 任务 8：修 readonly array 问题（2 错误）

错误位置：
- `SelectPage.vue` line 429: readonly tuple → `Item[]`
- `TablePage.vue` line 148: readonly tuple → `TableColumn[]`

**根因**：page 用 `as const` 写选项 / 列定义（产生 readonly tuple），传给组件 prop（要求 mutable array）。

**修法**（Codex 推荐 + plan owner 同意）：**改组件 prop type 接 readonly**——因为组件不该 mutate 这些 input。

具体：

1. 找 `DropDownListSelect.vue`（在 `src/canonical/` 或 `src/components/`），把 `items: Item[]` 改成 `items: readonly Item[]`
2. 找 `Table.vue`（同上），把 `columns: TableColumn[]` 改成 `columns: readonly TableColumn[]`
3. 验证组件内部**没** push/splice 这些 array（如有 → STOP 报告，让 plan owner 决定怎么办）

**严禁**用 cast / 数组复制绕过——这是 type safety 真问题，不是 cosmetic。

### 任务 9：验证

```bash
# typecheck 应 0 错误
pnpm exec vue-tsc --noEmit
echo "EXIT: $?"   # 必须 0

# 看错误数从 21 → 0
pnpm exec vue-tsc --noEmit 2>&1 | grep -E '^.*error TS[0-9]+:' | wc -l   # 必须 0

# dev smoke 抽样验证（curl 3 个修过的 page）
pnpm dev &
DEV_PID=$!
sleep 8
curl -s "http://127.0.0.1:5173/docs/pages/CheckboxPage.vue" | grep -oE '/@fs/[^"]*src/canonical/' | head -1
curl -s "http://127.0.0.1:5173/docs/pages/RadioPage.vue" | grep -oE '/@fs/[^"]*src/canonical/' | head -1
curl -s "http://127.0.0.1:5173/docs/pages/BadgePage.vue" | grep -oE '/@fs/[^"]*src/canonical/' | head -1
kill $DEV_PID 2>/dev/null
```

3 个 page 都应输出 `/@fs/.../src/canonical/`——证明 alias + import 正常。

如果 typecheck **仍有错误** → STOP 报告，列出残留错误清单，**不** force-fix。

---

## 不要做的事

- ❌ **不 commit / 不 push**
- ❌ 不修跟 21 个错误无关的代码（即使发现别的小问题也不动）
- ❌ 不用 `@ts-expect-error` / `@ts-ignore` 绕过——本次错误都该真修（per Round 1 报告）
- ❌ 不改 `tsconfig.node.json`
- ❌ 不安装新依赖
- ❌ 不主动跑 `pnpm sync:figma-library`
- ❌ 任务 8 不用 cast / 数组复制绕过 readonly 问题——必须改组件 prop type

允许的：

- ✅ 改 `tsconfig.json`（任务 1）
- ✅ 新增 `playground/types/raw.d.ts`（任务 2）
- ✅ 改 8 个 .vue / .ts 文件修错误（任务 3-7）
- ✅ 改 `src/canonical/DropDownListSelect.vue` + `src/canonical/Table.vue`（任务 8）—— **plan owner 已授权**
- ✅ 跑 `pnpm exec vue-tsc --noEmit` + curl smoke 验证

---

## 自验

```bash
# typecheck 0 错误
pnpm exec vue-tsc --noEmit; echo "EXIT: $?"   # 必须 0

# tsconfig include 永久扩
grep '"include"' tsconfig.json   # 应含 playground/**/*

# raw.d.ts 已加
ls -la playground/types/raw.d.ts

# 工作区改动数预期
git status --short | grep '^??' | wc -l   # 应 = 1（仅 raw.d.ts）+ 此前 untracked
git status --short | grep -E '^.M' | wc -l  # 应 ~10（tsconfig + 8 修复文件 + 2 component prop type）

# HEAD 没动
git log --oneline -1   # 应仍是 aaa537f
```

---

## 完成后 STOP

报告格式给主 session：

```
=== 任务 1 tsconfig include 扩 ===
done

=== 任务 2 raw.d.ts ===
created playground/types/raw.d.ts
fixed 4 module resolution errors

=== 任务 3 CheckboxPage Status widen ===
fixed 5 errors at lines 156/164/172/206/217
逻辑改动: yes/no（如有，描述）

=== 任务 4 RadioPage helper 签名 ===
helper signature changed: (oldSig) → (newSig)
fixed 7 errors at lines 144/152/160/200/230/238/246

=== 任务 5 BadgePage colorList 类型 ===
fixed 1 error at line 187
approach: as const / explicit Color[]

=== 任务 6 DocsShell PageLoader ===
fixed 1 error at line 67
approach: AsyncComponentLoader / inline

=== 任务 7 CanonicalShowcase feature ===
fixed 1 error at line 146
approach: source type fix / call site narrow

=== 任务 8 readonly array prop types ===
DropDownListSelect.items: Item[] → readonly Item[]: yes
Table.columns: TableColumn[] → readonly TableColumn[]: yes
组件内部 mutation 检查: 无 push/splice / 有（描述）

=== 任务 9 验证 ===
vue-tsc: 0 errors / N errors（如非 0 列错误）
dev smoke 3 page curl: PASS / FAIL

=== 阻碍 / 困惑 ===
（如有）

=== 工作区状态 ===
N modified, M untracked, 0 commits, HEAD = aaa537f
```

主 session 复审：
1. typecheck 真 0 错误（不是漏检）
2. 任务 8 组件 prop 改动是否影响其它 consumer
3. 任务 3 的 logic 改动（如有）是否合理

复审通过 → plan owner commit 这次 milestone（含 Round 1 prompt + 报告 + Round 2 prompt + 修复 + 待办段更新）。

**STOP。不要进 T2 样板。**
