diff --git a/docs/guide/SDD.md b/docs/guide/SDD.md
new file mode 100644
index 000000000..bb2822e7c
--- /dev/null
+++ b/docs/guide/SDD.md
@@ -0,0 +1,477 @@
+---
+title: SDD 规格驱动开发
+group: 研发
+order: 6
+---
+
+# SDD + Harness 实践指南(dt-react-component)
+
+**一句话:** 人维护 `*.brief.md` → AI 生成 `*.spec.yaml` → 派生**测试、API、实现(④)、Demo(②′ 人工触发)、Harness(②″ 可选)**。
+
+**试点组件:** [Copy](../../src/copy/Copy.brief.md)
+
+## 流程总览
+
+### 架构:谁维护什么
+
+```mermaid
+flowchart TB
+ subgraph Human["人维护"]
+ B["*.brief.md
行为契约 · Props 语义"]
+ DEMO_REF["index.md 示例引用
title / description"]
+ end
+
+ subgraph AI["AI 生成 · 人审"]
+ S["*.spec.yaml
机器 SSOT"]
+ end
+
+ subgraph Derived["从 spec 派生"]
+ IMPL["index.tsx
Prompt ④"]
+ TEST["__tests__/*.test.tsx
Prompt ②"]
+ API["index.md API 表
脚本自动"]
+ DEMOGEN["demos/*.tsx
Prompt ②′ 人工触发"]
+ HAR["demos/harness.tsx
Prompt ②″ 可选"]
+ end
+
+ B -->|"Prompt ①"| S
+ S --> IMPL
+ S --> TEST
+ S --> API
+ S -.->|"人工命令 AI"| DEMOGEN
+ S -.-> HAR
+ DEMOGEN --> DEMO_REF
+```
+
+### 首次建链(新组件 / 试点接入)
+
+```mermaid
+flowchart TD
+ Start([开始]) --> S1["写 Component.brief.md"]
+ S1 --> S2["Prompt ① → spec.yaml"]
+ S2 --> R1{"审 spec diff
审查三问 #1"}
+ R1 -->|不通过| S1
+ R1 -->|通过| S3["index.md API 区加 @generated 标记
(示例区不动)"]
+ S3 --> S4["Prompt ④ → index.tsx
新组件写初版 / 存量最小 diff"]
+ S4 --> R2{"人审实现 diff
审查第四问"}
+ R2 -->|不通过| S4
+ R2 -->|通过| S5["Prompt ② → 单元测试"]
+ S5 --> S6["pnpm test"]
+ S6 --> R3{"测试全绿?"}
+ R3 -->|否| S4
+ R3 -->|是| S7["pnpm spec:generate --artifact api"]
+ S7 --> S8["npm run dev 验收"]
+ S8 --> OPTD{"需要新 demo?"}
+ OPTD -->|是| S10["Prompt ②′ 生成 demo
人审 · 手贴 index.md 引用"]
+ OPTD -->|否| OPT{"需要 Harness?"}
+ S10 --> OPT
+ OPT -->|是| S9["Prompt ②″ → harness.tsx"]
+ OPT -->|否| Done([试点建链完成])
+ S9 --> Done
+```
+
+### 改需求级联(日常维护)
+
+```mermaid
+flowchart TD
+ Start([需求变更]) --> S1["改 brief.md"]
+ S1 --> S2["Prompt ③ 或 ①
regenerate spec.yaml"]
+ S2 --> R1{"审 spec diff"}
+ R1 -->|不通过| S1
+ R1 -->|通过| S3["Prompt ④
index.tsx 最小 diff"]
+ S3 --> R2{"人审实现"}
+ R2 -->|不通过| S3
+ R2 -->|通过| S4["Prompt ②
regenerate 测试"]
+ S4 --> S5["pnpm test"]
+ S5 --> R3{"全绿?"}
+ R3 -->|否| S3
+ R3 -->|是| S6["pnpm spec:generate --artifact api"]
+ S6 --> OPT1{"regen harness?"}
+ OPT1 -->|是| S7["Prompt ②″"]
+ OPT1 -->|否| OPT2{"demo 要更新?"}
+ S7 --> OPT2
+ OPT2 -->|是| S8["Prompt ②′ 生成 demo
人审 · 手贴 index.md 引用"]
+ OPT2 -->|否| Review
+ S8 --> Review["审查三问 + 第四问 → PR"]
+ Review --> Done([完成])
+```
+
+### 产物更新方式一览
+
+```mermaid
+flowchart LR
+ SPEC["spec.yaml"]
+
+ SPEC -->|"Prompt ④ · 人审"| IMPL["index.tsx"]
+ SPEC -->|"Prompt ② · regenerate"| TEST["测试"]
+ SPEC -->|"脚本自动"| API["index.md API"]
+ SPEC -->|"Prompt ②′ · 人工触发"| DEMO["demos/*.tsx"]
+ SPEC -.->|"Prompt ②″ · 可选"| HAR["harness"]
+ DEMO -->|"人贴引用"| IDX["index.md 示例区"]
+
+ style API fill:#e6f7ff
+ style DEMO fill:#fff7e6
+ style IMPL fill:#f6ffed
+ style IDX fill:#fff7e6
+```
+
+### 全库推广(opt-in)
+
+```mermaid
+flowchart TD
+ Start([团队决策]) --> Q1{"组件类型?"}
+ Q1 -->|"有行为分支 · 常改"| P0["P0:Copy / StatusTag / FilterRules"]
+ Q1 -->|"antd 二次封装"| P1["P1:Drawer / PopConfirm / Catalogue"]
+ Q1 -->|"纯展示 · 少分支"| Skip["暂缓 SDD"]
+ Q1 -->|"新建组件"| P2["P2:直接走首次建链"]
+
+ P0 --> Onboard["执行「首次建链」流程"]
+ P1 --> Onboard
+ P2 --> Onboard
+ Onboard --> Done["该组件目录出现
brief + spec + @generated API"]
+ Done --> Loop["改需求走「级联」流程"]
+```
+
+### 开发者入口:我该走哪条路?
+
+```mermaid
+flowchart TD
+ Start([开发者接到任务]) --> Q1{"改动类型?"}
+
+ Q1 -->|"首次为组件接入 SDD
(如 Drawer)"| A["场景 A · 首次建链"]
+ Q1 -->|"已有 brief,要改行为"| B["场景 B · 改需求级联"]
+ Q1 -->|"新组件从零开发"| C["场景 C · 新组件"]
+ Q1 -->|"只改样式 / 文案"| D["不走 SDD
直接改代码 + 现有测试"]
+ Q1 -->|"只补 demo 展示"| E["Prompt ②′ demo
brief 可不动"]
+
+ A --> Branch["git checkout -b feat/sdd-{component}"]
+ C --> Branch
+ B --> BriefEdit["改 Component.brief.md"]
+ BriefEdit --> Cascade
+ Branch --> Cascade["见下方详细流程"]
+```
+
+### 场景 A / C:首次建链(Drawer、新组件)
+
+```mermaid
+flowchart TD
+ Start([开始]) --> S1["1. 建分支 feat/sdd-{component}"]
+ S1 --> S2["2. 写 src/{c}/{Component}.brief.md
含 Props · 场景 · Antd继承 · outOfScope"]
+ S2 --> S3["3. Prompt ① → {Component}.spec.yaml"]
+ S3 --> R1{"4. 审 spec
props 仅封装层?
extendsAntd 正确?"}
+ R1 -->|否| S2
+ R1 -->|是| S4["5. index.md API 加 @generated 标记
示例区不动"]
+ S4 --> S5["6. pnpm spec:generate -- --artifact impl
Prompt ④ → 人审 index.tsx"]
+ S5 --> S6["7. pnpm spec:generate -- --artifact test
Prompt ② → pnpm test"]
+ S6 --> R2{"测试全绿?"}
+ R2 -->|否| S5
+ R2 -->|是| S7["8. pnpm spec:generate -- --artifact api
(可先 --dry-run)"]
+ S7 --> S8["9. npm run dev 验收"]
+ S8 --> OPT1{"需要新 demo?"}
+ OPT1 -->|是| S9["10. pnpm spec:generate -- --artifact demo
②′ → 人审 → 手贴 index.md"]
+ OPT1 -->|否| OPT2{"需要 Harness?"}
+ S9 --> OPT2
+ OPT2 -->|是| S10["pnpm spec:generate -- --artifact harness"]
+ OPT2 -->|否| PR
+ S10 --> PR["11. PR:brief + spec + 测试 + API 区块
+ index.tsx(如有)"]
+ PR --> Done([完成])
+```
+
+### 场景 B:已有 brief · 组件行为变更
+
+```mermaid
+flowchart TD
+ Start([行为变更]) --> S1["1. 改 Component.brief.md"]
+ S1 --> S2["2. Prompt ① 或 ③
regenerate spec.yaml"]
+ S2 --> R1{"3. 审 spec diff"}
+ R1 -->|否| S1
+ R1 -->|是| S3["4. pnpm spec:generate -- --artifact impl
Prompt ④ · 人审 index.tsx"]
+ S3 --> S4["5. pnpm spec:generate -- --artifact test
Prompt ②"]
+ S4 --> S5["6. pnpm test"]
+ S5 --> R2{"全绿?"}
+ R2 -->|否| S3
+ R2 -->|是| S6["7. pnpm spec:generate -- --artifact api"]
+ S6 --> OPT1{"新行为要 demo 展示?"}
+ OPT1 -->|是| S7["8. pnpm spec:generate -- --artifact demo
人审 · 手贴 index.md"]
+ OPT1 -->|否| OPT2{"regen harness?"}
+ S7 --> OPT2
+ OPT2 -->|是| S8["Prompt ②″"]
+ OPT2 -->|否| Review
+ S8 --> Review["9. 审查清单 → PR"]
+ Review --> Done([完成])
+```
+
+### spec:generate 与各 Prompt 对应关系
+
+```mermaid
+flowchart LR
+ subgraph Manual["SDD.md 手动复制"]
+ P1["Prompt ①
brief → spec"]
+ P3["Prompt ③
级联统筹"]
+ end
+
+ subgraph CLI["pnpm spec:generate -- --component X"]
+ IMPL["--artifact impl
Prompt ④"]
+ TEST["--artifact test
Prompt ②"]
+ DEMO["--artifact demo
Prompt ②′"]
+ API["--artifact api
脚本自动写 API"]
+ HAR["--artifact harness
Prompt ②″"]
+ end
+
+ P1 --> SPEC["*.spec.yaml"]
+ SPEC --> IMPL
+ SPEC --> TEST
+ SPEC --> DEMO
+ SPEC --> API
+ SPEC --> HAR
+ P3 -.->|"编排顺序"| IMPL
+```
+
+## 设计原则(低变更)
+
+| 区块 | 谁维护 | SDD 介入方式 |
+| ------------------------------------------------------------- | ------------------------- | ---------------------------------------------- |
+| `index.md` **示例引用**(title / description / ``) | 人贴入 | **②′ 输出建议行**,不自动改 index.md |
+| `demos/*.tsx` | AI 生成 + **人审** | **Prompt ②′**(人工按需触发,非级联必跑) |
+| `index.md` **API 表** | spec 派生 | **自动** — `pnpm spec:generate --artifact api` |
+| `index.tsx` **实现** | spec 驱动 + **人审 diff** | **Prompt ④** |
+| `__tests__/*.test.tsx` | spec 派生 | **Prompt ②** |
+| `demos/harness.tsx` | spec 派生(可选) | **Prompt ②″** |
+
+### 为什么 `index.tsx` 不用一键 regenerate?
+
+| 产物 | 与 spec 关系 | 更新方式 |
+| ----------- | ------------------------------------- | ----------------------------- |
+| 测试 / API | spec 的**直接翻译** | 可整段 regenerate |
+| `index.tsx` | spec 的**一种实现**(多种写法都合法) | **Prompt ④ 最小 diff + 人审** |
+
+spec 约束「该怎么表现」;实现里的 hooks 拆分、性能、样式细节仍由人把关,避免覆盖已有工程决策。
+
+SDD 为 **opt-in**:仅带 `*.brief.md` 的组件走此流程,其它组件零影响。
+
+## 目录约定
+
+```plain
+src/{component}/
+├── {Component}.brief.md # 人维护 · 行为 + Props 语义
+├── {Component}.spec.yaml # AI 生成 · 人审
+├── index.tsx # Prompt ④ 按 spec 最小更新 · 人审
+├── index.md # 示例手维护;API 表为 @generated 区块
+├── __tests__/{component}.test.tsx
+└── demos/
+ ├── basic.tsx … # Prompt ②′ 生成 · 人审
+ └── harness.tsx # 可选
+```
+
+## 工作流
+
+### 首次建链
+
+```plain
+① 写 {Component}.brief.md
+② Prompt ① → spec.yaml → 审 spec diff
+③ Prompt ④ → index.tsx 最小 diff(新组件则实现初始版本)→ 人审
+④ Prompt ② → 测试 → pnpm test
+⑤ pnpm spec:generate -- --component {Name} --artifact api
+⑥ (按需)Prompt ②′ → demos/*.tsx → 人审 → 手贴 index.md 引用
+⑦ (可选)Prompt ②″ → demos/harness.tsx
+```
+
+### 改需求(级联 · Prompt ③)
+
+```plain
+改 brief
+ → Prompt ③ / ① regenerate spec.yaml → 审 spec
+ → Prompt ④ 更新 index.tsx(最小 diff)→ 人审
+ → Prompt ② regenerate 测试 → pnpm test
+ → pnpm spec:generate -- --artifact api
+ → (按需)Prompt ②′ demo → 人审 → 手贴 index.md
+ → (可选)regen harness
+```
+
+**推荐顺序:** brief → spec → **实现(④)** → 测试(②)→ API(脚本)。先改实现,测试用于验证。
+
+## Prompt 模板
+
+将 `Copy` / `copy` 替换为目标组件名。
+
+### Prompt ① brief + 源码 → spec
+
+```text
+阅读 src/copy/Copy.brief.md 与 src/copy/index.tsx,
+生成 src/copy/Copy.spec.yaml。
+
+含:props(**仅封装层**)、extendsAntd(若继承 antd/rc)、
+scenarios、outOfScope、testFixtures、harnessMeta、linkedDemos。
+
+antd 继承参考 src/drawer/index.md;brief 未写清处标 aiSupplement: true。
+@generated-from: Copy.brief.md
+```
+
+### Prompt ② spec → 测试
+
+```text
+根据 src/copy/Copy.spec.yaml 生成 src/copy/__tests__/copy.test.tsx。
+Jest + Testing Library;每个 scenarios.id 一个 it(...);不测 outOfScope。
+@generated-from: Copy.spec.yaml
+```
+
+### Prompt ②′ spec + 设计意图 → Demo(人工触发 · 人审)
+
+**不在自动级联内**——当你需要新增/改版 demo 时,先填「设计意图」,再在 Cursor 发送:
+
+```text
+为 Copy 组件生成 dumi demo。
+
+【设计意图 — 发送前填写】
+- demo 文件:demos/tooltip-variants.tsx(新建或覆盖)
+- 展示目的:对比 tooltip 字符串 / 对象 / 函数三种写法
+- 重点 props:text, tooltip
+- 对应 scenarioId(可选):tooltip-string, tooltip-object
+- index.md 卡片 title:Tooltip 多种写法
+- index.md 卡片 description:字符串、对象、函数形式配置 tooltip
+
+【上下文 — AI 必读】
+- src/copy/Copy.brief.md
+- src/copy/Copy.spec.yaml
+- src/copy/index.tsx
+- 风格参考:src/copy/demos/basic.tsx
+
+【生成要求】
+1. 写出 demos/tooltip-variants.tsx,import { Copy } from 'dt-react-component'
+2. 按「重点 props」组合展示,取值符合 spec.props 语义;不测 outOfScope
+3. 遵循 RC demo 风格(BlockHeader / Space / 示例长文本等),单文件聚焦一个主题
+4. 每个子示例用小标题区分,便于 dumi 卡片内阅读
+5. **不要**直接修改 index.md;在回复末尾输出:
+ - 建议粘贴的 `` 一行
+ - 若需更新 spec,输出 linkedDemos 片段供人粘贴
+6. 生成后自查:npm run dev 下该 demo 可独立运行
+```
+
+获取 Prompt 骨架:
+
+```bash
+pnpm spec:generate -- --component Copy --artifact demo
+```
+
+**与 harness 的区别:** ②′ 面向**文档读者**(好看、有叙事);②″ 面向**维护者验 spec**(控件 + 核对清单)。
+
+### Prompt ②″ spec → Harness(可选)
+
+```text
+根据 src/copy/Copy.spec.yaml 生成 src/copy/demos/harness.tsx。
+不要修改 index.md 的「示例」区块。
+```
+
+### Prompt ④ spec → 实现(最小 diff · 人审)
+
+```text
+阅读 src/copy/Copy.spec.yaml 与 src/copy/index.tsx。
+
+1. 逐条对比 spec.scenarios 与当前实现,列出「已满足 / 缺失 / 行为不一致」
+2. 对 index.tsx 做**最小 diff**以满足 spec,要求:
+ - 只改与 scenarios / props 语义相关的代码
+ - 不碰 outOfScope
+ - 不做无关重构(不重排 import、不改命名风格、不拆 hooks 除非 spec 要求)
+ - 保持 ICopyProps 与 spec.props 一致
+3. 若 spec 与 brief 冲突,在回复中指出,**不要擅自改 spec**
+4. 改完后简述:每个 scenario.id 对应改了哪段逻辑
+```
+
+**新组件(无 index.tsx 或空壳):** 同上 Prompt,第 2 步改为「按 spec 实现 index.tsx 初版」,仍遵循 outOfScope 与项目风格。
+
+### Prompt ③ 组件变更 → 级联
+
+```text
+见 git diff(brief / index.tsx / 测试):
+
+1. 建议 Copy.brief.md 还需改哪几段
+2. regenerate Copy.spec.yaml 要点
+3. 按顺序列出需执行的 Prompt:
+ - ④ index.tsx(最小 diff)
+ - ② 测试
+ - pnpm spec:generate -- --component Copy --artifact api
+ - (按需)②′ demo
+ - (可选)②″ harness
+4. demo 是否需 Prompt ②′(对照 linkedDemos / 新 props 展示)
+5. 审查三问 + 实现审查(见下)检查要点
+```
+
+## 实现审查(Prompt ④ 之后 · 第四问)
+
+在「审查三问」基础上,改 `index.tsx` 后追加:
+
+4. **实现 diff 是否仅满足 spec,无 scope 外改动?** 每个变更能否对应到 scenario.id 或 props 语义?
+
+## index.md API 标记
+
+首次接入时在 `### API` 下增加标记(**示例区块不动**):
+
+```markdown
+### API
+
+
+
+| 参数 | 说明 | 类型 | 默认值 |
+| ... |
+
+
+```
+
+```bash
+pnpm spec:generate -- --component Copy --artifact api
+pnpm spec:generate -- --component Copy --artifact api --dry-run
+```
+
+### 继承 antd props(参考 Drawer)
+
+API 表只列封装层 props;`extendsAntd` 自动生成 `:::info` 块:
+
+```yaml
+extendsAntd:
+ component: Drawer
+ docUrl: https://4x.ant.design/components/drawer-cn/#API
+ version: '4.x'
+ omit: "Omit" # 可选
+```
+
+## CLI
+
+```bash
+pnpm spec:generate -- --component Copy # 全部 artifact + Prompt
+pnpm spec:generate -- --component Copy --artifact impl # Prompt ④
+pnpm spec:generate -- --component Copy --artifact test # Prompt ②
+pnpm spec:generate -- --component Copy --artifact demo # Prompt ②′
+pnpm spec:generate -- --component Copy --artifact api # 自动写 API
+pnpm spec:generate -- --component Copy --artifact harness
+```
+
+| artifact | 行为 |
+| --------- | ----------------------------------------------------------- |
+| `api` | **自动**写 `index.md` API 区块 |
+| `impl` | 打印 **Prompt ④**(最小 diff 改 `index.tsx`,人审) |
+| `test` | 打印 Prompt ② |
+| `demo` | 打印 **Prompt ②′**(生成 demos/\*.tsx,**人工触发**,人审) |
+| `harness` | 打印 Prompt ②″(可选) |
+
+## 审查清单
+
+1. spec 是否忠实反映 brief?
+2. 测试 / API 是否覆盖 spec.props 与 scenarios,且未碰 outOfScope?
+3. `pnpm test` 是否绿?API 表与 `index.tsx` interface 是否一致?
+4. **(改 demo 时)** props 展示与 spec 一致?单 demo 聚焦一主题?index.md 引用已手贴?
+5. **(改实现时)** index.tsx diff 是否最小、且每条变更可对应 scenario?
+
+## 验证清单(Copy 试点)
+
+- [ ] `Copy.brief.md` 入库
+- [ ] `Copy.spec.yaml` 审阅通过
+- [ ] Prompt ④ 对照 spec 审过 `index.tsx`
+- [ ] `copy.test.tsx` 从 spec 派生,`pnpm test` 绿
+- [ ] `pnpm spec:generate -- --component Copy --artifact api`
+- [ ] (按需)Prompt ②′ 生成 demo,`npm run dev` 目视验收
+- [ ] 完成一次 brief 变更 → ④ + ② + api 级联
+
+## 延伸阅读
+
+- 方法论:`AI Coding 2.0:SDD + Harness 实践指南`
+- batch Phase 1:`ResGroupSelector.brief.md` / `ResGroupSelector.spec.yaml`
diff --git a/package.json b/package.json
index e46dbf8f2..51ddc3887 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
"lint:es": "eslint \"src/**/*.{js,jsx,ts,tsx}\" \".dumi/**/*.{js,jsx,ts,tsx}\"",
"prepublishOnly": "father doctor && npm run build",
"deploy": "npm run docs:build && gh-pages -d docs-dist",
- "release": "./scripts/release.sh"
+ "release": "./scripts/release.sh",
+ "spec:generate": "node scripts/generate-from-spec.js"
},
"authors": [
"dtinsight UED"
diff --git a/scripts/generate-from-spec.js b/scripts/generate-from-spec.js
new file mode 100644
index 000000000..4ae479d2e
--- /dev/null
+++ b/scripts/generate-from-spec.js
@@ -0,0 +1,424 @@
+#!/usr/bin/env node
+/**
+ * SDD spec → artifact generator
+ *
+ * - api: deterministic sync of index.md API table from spec.props
+ * - test / harness: print Cursor prompts (codegen TBD)
+ *
+ * Usage:
+ * pnpm spec:generate -- --component Copy
+ * pnpm spec:generate -- --component Copy --artifact api
+ * pnpm spec:generate -- --component Copy --artifact test|impl|demo|api|harness|all
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const GENERATED_START = (component) => ``;
+const GENERATED_END = '';
+
+const ARTIFACTS = {
+ test: {
+ label: '单元测试',
+ output: (dir, _name, kebab) => path.join(dir, '__tests__', `${kebab}.test.tsx`),
+ prompt: (name) =>
+ `根据 src/${toKebab(name)}/${name}.spec.yaml 生成 src/${toKebab(
+ name
+ )}/__tests__/${toKebab(name)}.test.tsx。\n` +
+ `Jest + Testing Library;每个 scenarios.id 一个 it(...);不测 outOfScope。\n` +
+ `@generated-from: ${name}.spec.yaml`,
+ write: false,
+ },
+ api: {
+ label: 'index.md API 表',
+ output: (dir) => path.join(dir, 'index.md'),
+ prompt: (name) =>
+ `(已由脚本自动生成)若需人工修复:根据 src/${toKebab(
+ name
+ )}/${name}.spec.yaml 的 props 段,` +
+ `更新 index.md 中 ${GENERATED_START(name)} 与 ${GENERATED_END} 之间的 API 表格。`,
+ write: true,
+ },
+ impl: {
+ label: '组件实现 index.tsx(Prompt ④ · 最小 diff · 人审)',
+ output: (dir) => path.join(dir, 'index.tsx'),
+ prompt: (name) =>
+ `阅读 src/${toKebab(name)}/${name}.spec.yaml 与 src/${toKebab(name)}/index.tsx。\n` +
+ `1. 列出 scenarios 与当前实现的差异\n` +
+ `2. 对 index.tsx 做**最小 diff**以满足 spec,不碰 outOfScope\n` +
+ `3. 不做无关重构;保持现有代码风格\n` +
+ `4. 若 spec 与 brief 冲突,指出而非擅自改 spec`,
+ write: false,
+ },
+ demo: {
+ label: 'dumi Demo(Prompt ②′ · 人工触发 · 人审)',
+ output: (dir, _name, kebab) => path.join(dir, 'demos', `${kebab}-demo.tsx`),
+ prompt: (name) => buildDemoPrompt(name),
+ write: false,
+ },
+ harness: {
+ label: 'Harness 交互验证(可选)',
+ output: (dir) => path.join(dir, 'demos', 'harness.tsx'),
+ prompt: (name) =>
+ `根据 src/${toKebab(name)}/${name}.spec.yaml 生成 demos/harness.tsx。\n` +
+ `含 props 控件、预设场景按钮、预览区、规格核对清单。\n` +
+ `不要修改 index.md 的「示例」区块;可选在文档底部增加维护者入口。`,
+ write: false,
+ },
+};
+
+function toKebab(name) {
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
+}
+
+/**
+ * Prompt ②′ — demo 由人按需触发,填完「设计意图」段落后交给 AI。
+ * @see docs/guide/SDD.md
+ */
+function buildDemoPrompt(name) {
+ const kebab = toKebab(name);
+ return (
+ `为 src/${kebab}/ 生成 dumi demo。先阅读:\n` +
+ `- ${name}.brief.md、${name}.spec.yaml、index.tsx\n` +
+ `- 现有 demo 风格参考:demos/basic.tsx(或其它已有 demo)\n\n` +
+ `【使用前请填写以下设计意图,再发送本 Prompt】\n` +
+ `- demo 文件:demos/__________.tsx(新建或覆盖)\n` +
+ `- 展示目的:__________(如「对比 tooltip 三种写法」)\n` +
+ `- 重点 props:__________(如 tooltip, button, disabled)\n` +
+ `- 对应 scenarioId(可选):__________\n` +
+ `- index.md 卡片 title:__________\n` +
+ `- index.md 卡片 description:__________\n\n` +
+ `生成要求:\n` +
+ `1. 按上述 props 组合写可运行 demo,import { ${name} } from 'dt-react-component'\n` +
+ `2. 遵循 RC 现有 demo 风格(BlockHeader / Space / 示例文案等),单 demo 聚焦一个主题\n` +
+ `3. props 取值与 spec.props 语义一致,不展示 outOfScope 行为\n` +
+ `4. 输出末尾附:建议写入 index.md 的 一行(**不要**自动改 index.md)\n` +
+ `5. 若为新场景,建议在 spec linkedDemos 追加映射(输出 yaml 片段供人粘贴)`
+ );
+}
+
+function parseArgs(argv) {
+ const result = { component: null, artifact: 'all', dryRun: false };
+ for (let i = 0; i < argv.length; i++) {
+ if (argv[i] === '--component' && argv[i + 1]) {
+ result.component = argv[++i];
+ } else if (argv[i] === '--artifact' && argv[i + 1]) {
+ result.artifact = argv[++i];
+ } else if (argv[i] === '--dry-run') {
+ result.dryRun = true;
+ } else if (argv[i] === '--help' || argv[i] === '-h') {
+ result.help = true;
+ }
+ }
+ return result;
+}
+
+function printHelp() {
+ console.log(`
+SDD spec → artifact generator
+
+Usage:
+ pnpm spec:generate -- --component [--artifact test|impl|demo|api|harness|all] [--dry-run]
+
+Examples:
+ pnpm spec:generate -- --component Copy
+ pnpm spec:generate -- --component Copy --artifact api
+ pnpm spec:generate -- --component Copy --artifact demo
+
+Artifacts:
+ api sync index.md API table (automatic)
+ impl Prompt ④ — index.tsx minimal diff
+ test Prompt ② — unit tests
+ demo Prompt ②′ — dumi demo (manual trigger, human review)
+ harness Prompt ②″ — harness panel (optional)
+
+Docs: docs/guide/SDD.md
+`);
+}
+
+function loadSpec(specPath) {
+ if (!fs.existsSync(specPath)) {
+ return null;
+ }
+ const raw = fs.readFileSync(specPath, 'utf8');
+ const idMatches = raw.match(/^\s*-\s*id:/gm);
+ return {
+ raw,
+ path: specPath,
+ scenarioCount: idMatches ? idMatches.length : 0,
+ props: parsePropsFromSpec(raw),
+ extendsAntd: parseExtendsAntdFromSpec(raw),
+ };
+}
+
+function unquote(value) {
+ return value.trim().replace(/^['"]|['"]$/g, '');
+}
+
+/**
+ * Parse a flat YAML mapping block (structured subset, no external deps).
+ */
+function parseFlatYamlBlock(raw, blockName) {
+ const lines = raw.split('\n');
+ const result = {};
+ let inBlock = false;
+
+ for (const line of lines) {
+ if (new RegExp(`^${blockName}:\\s*$`).test(line)) {
+ inBlock = true;
+ continue;
+ }
+ if (!inBlock) {
+ continue;
+ }
+ if (line.trim() && !line.startsWith(' ') && !line.startsWith('\t')) {
+ break;
+ }
+ const fieldMatch = line.match(/^\s{2}([a-zA-Z_][\w]*):\s*(.*)$/);
+ if (fieldMatch) {
+ const [, key, value = ''] = fieldMatch;
+ result[key] = unquote(value);
+ }
+ }
+
+ return Object.keys(result).length ? result : null;
+}
+
+/**
+ * Parse spec.props block — nested prop definitions.
+ */
+function parsePropsFromSpec(raw) {
+ const lines = raw.split('\n');
+ const props = {};
+ let inProps = false;
+ let currentProp = null;
+ const baseIndent = 2;
+
+ for (const line of lines) {
+ if (/^props:\s*$/.test(line)) {
+ inProps = true;
+ continue;
+ }
+ if (!inProps) {
+ continue;
+ }
+ if (line.trim() && !line.startsWith(' ') && !line.startsWith('\t')) {
+ break;
+ }
+ const propMatch = line.match(/^(\s*)([a-zA-Z_][\w]*):\s*$/);
+ if (propMatch && propMatch[1].length === baseIndent) {
+ currentProp = propMatch[2];
+ props[currentProp] = {};
+ continue;
+ }
+ if (currentProp) {
+ const fieldMatch = line.match(/^\s+(type|description|default|required):\s*(.+)?$/);
+ if (fieldMatch) {
+ const [, key, value = ''] = fieldMatch;
+ props[currentProp][key] = unquote(value);
+ }
+ }
+ }
+
+ return props;
+}
+
+function parseExtendsAntdFromSpec(raw) {
+ return parseFlatYamlBlock(raw, 'extendsAntd');
+}
+
+function formatDefault(value) {
+ if (value === undefined || value === null || value === '' || value === 'null') {
+ return '--';
+ }
+ return value;
+}
+
+function escapeTableCell(value) {
+ return String(value).replace(/\|/g, '\\|');
+}
+
+function buildApiTable(props) {
+ const header = ['| 参数 | 说明 | 类型 | 默认值 |', '| --- | --- | --- | --- |'];
+ const rows = Object.entries(props).map(([name, meta]) => {
+ const desc = escapeTableCell(meta.description || '--');
+ const type = meta.type ? `\`${escapeTableCell(meta.type)}\`` : '--';
+ const def = escapeTableCell(formatDefault(meta.default));
+ return `| ${name} | ${desc} | ${type} | ${def} |`;
+ });
+ return [...header, ...rows].join('\n');
+}
+
+/**
+ * Antd inheritance notice — follows Drawer index.md (:::info block).
+ * @see src/drawer/index.md
+ */
+function buildExtendsAntdNotice(extendsAntd) {
+ if (!extendsAntd || !extendsAntd.component) {
+ return '';
+ }
+
+ const version = extendsAntd.version || '4.x';
+ const { component, docUrl, omit, notice } = extendsAntd;
+
+ let body = notice;
+ if (!body) {
+ if (omit) {
+ body = `其余参数继承 antd${version} 的 \`${omit}\``;
+ if (docUrl) {
+ body += `
详见 [antd${version} 的 ${component}](${docUrl})`;
+ }
+ } else if (docUrl) {
+ body = `其余属性继承 [antd${version} 的 ${component}](${docUrl})`;
+ } else {
+ body = `其余属性继承 antd${version} 的 ${component}`;
+ }
+ }
+
+ return `\n\n:::info\n${body}\n:::`;
+}
+
+function buildApiBlock(props, extendsAntd) {
+ return buildApiTable(props) + buildExtendsAntdNotice(extendsAntd);
+}
+
+function syncIndexMdApi(indexPath, component, apiTable, dryRun) {
+ if (!fs.existsSync(indexPath)) {
+ throw new Error(`index.md not found: ${indexPath}`);
+ }
+
+ const content = fs.readFileSync(indexPath, 'utf8');
+ const start = GENERATED_START(component);
+ const startIdx = content.indexOf(start);
+ const endIdx = content.indexOf(GENERATED_END);
+
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
+ throw new Error(
+ `index.md missing API markers. Add:\n\n${start}\n${apiTable}\n${GENERATED_END}\n\nSee docs/guide/SDD.md`
+ );
+ }
+
+ const before = content.slice(0, startIdx + start.length);
+ const after = content.slice(endIdx);
+ const next = `${before}\n\n${apiTable}\n\n${after}`;
+
+ if (dryRun) {
+ console.log('\n--- API block (dry-run) ---\n');
+ console.log(apiTable);
+ return { updated: false, dryRun: true };
+ }
+
+ if (next === content) {
+ return { updated: false, unchanged: true };
+ }
+
+ fs.writeFileSync(indexPath, next, 'utf8');
+ return { updated: true };
+}
+
+function runApiArtifact(component, componentDir, spec, dryRun) {
+ const props = spec.props;
+ const propNames = Object.keys(props);
+
+ if (!propNames.length) {
+ throw new Error('spec.yaml has no parseable props — check props: block format');
+ }
+
+ const apiBlock = buildApiBlock(props, spec.extendsAntd);
+ const indexPath = path.join(componentDir, 'index.md');
+ const result = syncIndexMdApi(indexPath, component, apiBlock, dryRun);
+ const relIndex = path.relative(process.cwd(), indexPath);
+ const extendsLabel = spec.extendsAntd?.component
+ ? `, extends antd ${spec.extendsAntd.component}`
+ : '';
+
+ if (result.dryRun) {
+ console.log(`[api] Would update ${relIndex} (${propNames.length} props${extendsLabel})`);
+ return;
+ }
+ if (result.unchanged) {
+ console.log(
+ `[api] ${relIndex} API block already up to date (${propNames.length} props${extendsLabel})`
+ );
+ return;
+ }
+ console.log(`[api] Updated ${relIndex} API block (${propNames.length} props${extendsLabel})`);
+}
+
+function main() {
+ const args = parseArgs(process.argv.slice(2));
+
+ if (args.help || !args.component) {
+ printHelp();
+ process.exit(args.help ? 0 : 1);
+ }
+
+ const component = args.component;
+ const kebab = toKebab(component);
+ const componentDir = path.join(process.cwd(), 'src', kebab);
+ const briefPath = path.join(componentDir, `${component}.brief.md`);
+ const specPath = path.join(componentDir, `${component}.spec.yaml`);
+
+ console.log(`\nSDD generate — ${component}\n`);
+
+ if (!fs.existsSync(briefPath)) {
+ console.warn(`⚠ Missing brief: src/${kebab}/${component}.brief.md`);
+ } else {
+ console.log(`✓ Brief: src/${kebab}/${component}.brief.md`);
+ }
+
+ const spec = loadSpec(specPath);
+ if (!spec) {
+ console.error(`\n✗ Spec not found: src/${kebab}/${component}.spec.yaml`);
+ console.error(' Run Prompt ① in Cursor first (see docs/guide/SDD.md).\n');
+ process.exit(1);
+ }
+
+ console.log(
+ `✓ Spec: src/${kebab}/${component}.spec.yaml (${spec.scenarioCount} scenario(s), ${
+ Object.keys(spec.props).length
+ } prop(s))`
+ );
+
+ const artifactKeys = args.artifact === 'all' ? Object.keys(ARTIFACTS) : [args.artifact];
+ const unknown = artifactKeys.filter((k) => !ARTIFACTS[k]);
+ if (unknown.length) {
+ console.error(`\n✗ Unknown artifact: ${unknown.join(', ')}`);
+ console.error(' Valid: test, impl, demo, api, harness, all\n');
+ process.exit(1);
+ }
+
+ console.log('');
+
+ artifactKeys.forEach((key) => {
+ const art = ARTIFACTS[key];
+ if (art.write) {
+ try {
+ runApiArtifact(component, componentDir, spec, args.dryRun);
+ } catch (err) {
+ console.error(`[api] ✗ ${err.message}\n`);
+ process.exit(1);
+ }
+ return;
+ }
+
+ const out = art.output(componentDir, component, kebab);
+ const relOut = path.relative(process.cwd(), out);
+ const exists = fs.existsSync(out);
+
+ console.log(`[${key}] ${art.label}`);
+ console.log(` Output: ${relOut}${exists ? ' (exists)' : ''}`);
+ console.log(` Prompt:\n ${art.prompt(component).replace(/\n/g, '\n ')}\n`);
+ });
+
+ console.log('--- Notes ---');
+ console.log('• demo:Prompt ②′ 人工按需触发,人审 demo 后手贴 index.md 引用');
+ console.log(
+ '• 改 props 后:brief → regen spec → pnpm spec:generate -- --component ' +
+ component +
+ ' --artifact api'
+ );
+ console.log('');
+}
+
+main();
diff --git a/src/copy/Copy.brief.md b/src/copy/Copy.brief.md
new file mode 100644
index 000000000..8ccb0ad4d
--- /dev/null
+++ b/src/copy/Copy.brief.md
@@ -0,0 +1,94 @@
+# Copy 组件行为说明
+
+> **维护入口**:改需求时先改本文件,再 regenerate `Copy.spec.yaml` 及派生产物。
+> **实现代码**:`index.tsx` 通过 **Prompt ④** 按 spec 做最小 diff 更新,**人审 diff** 后合入;不整文件 regenerate。
+
+## 干什么
+
+点击复制区域,将 `text` 写入剪贴板;默认带 Tooltip 提示,复制成功后触发回调或弹出成功提示。
+
+## Props 语义
+
+| Prop | 说明 |
+| --------------------- | -------------------------------------------------------------------------------------- |
+| `text` | 要复制的字符串(必填) |
+| `disabled` | `true` 时点击不触发复制、不调用 `onCopy`,并加上禁用样式 |
+| `button` | 自定义触发节点,默认 `CopyOutlined` 图标 |
+| `tooltip` | Tooltip 配置;支持字符串、`TooltipProps` 对象;`false` / 其它假值时不展示 Tooltip 内容 |
+| `onCopy` | 复制成功回调,参数为 `text`;默认 `message.success(locale.copied)` |
+| `className` / `style` | 透传到复制区域 `.dtc-copy` |
+
+## Antd 继承
+
+**无。** Copy 不对外继承 antd 组件 props(内部使用 Tooltip,不在 API 表列出 antd Tooltip props)。spec 中**不设** `extendsAntd`。
+
+## 场景(应有行为)
+
+### 1. default-render
+
+- 渲染 `.dtc-copy` 可点击区域
+- 默认使用 `CopyOutlined` 图标作为 button
+
+### 2. click-copy
+
+- `disabled=false` 时点击 → `CopyUtils.copy(text, callback)` 被调用
+- 传入 `onCopy` 时,复制成功后调用 `onCopy(text)`
+- 剪贴板内容为 `text`(在测试环境可通过 mock 验证回调参数)
+
+### 3. disabled
+
+- `disabled=true` 时点击不调用 `CopyUtils.copy`、不调用 `onCopy`
+- 复制区域带 `dtc-copy--disabled` class
+
+### 4. custom-button
+
+- 传入 `button` 时渲染自定义节点(如文本按钮)
+- 点击自定义节点同样触发复制逻辑
+
+### 5. tooltip-string
+
+- `tooltip="复制文本"` 等字符串时,Tooltip 展示对应 title
+
+### 6. tooltip-object
+
+- `tooltip={{ title: '复制文本' }}` 时,Tooltip 按对象配置展示
+
+### 7. tooltip-falsy
+
+- `tooltip={false}` 等假值时,不展示 Tooltip 浮层(与 `demos/custom.tsx` 一致)
+
+## 不测什么(outOfScope)
+
+- `CopyUtils` 内部实现、clipboard 权限失败、浏览器兼容性
+- Tooltip 动画时序、`message` 样式与精确 locale 文案
+- `ConfigProvider` / 多语言切换的集成行为
+- E2E 真实剪贴板权限(单测用 mock / `userEvent` 即可)
+
+## 文档与派生产物
+
+| 产物 | 维护方式 |
+| ---------------------------- | ------------------------------------------------------------------------------------- |
+| `index.tsx` | **Prompt ④**:spec 驱动最小 diff,`pnpm spec:generate -- --artifact impl` 打印 Prompt |
+| `demos/*.tsx` | **Prompt ②′** AI 生成 + 人审(按需触发) |
+| `index.md` 示例 `` | **人贴**(②′ 输出建议行) |
+| `index.md` API 表 | **自动**:`pnpm spec:generate -- --artifact api` |
+| `__tests__/copy.test.tsx` | **Prompt ②** |
+| `demos/harness.tsx` | 可选,**Prompt ②″** |
+
+改需求顺序:**brief → spec → ④ 实现 → ② 测试 → api 脚本 →(按需)②′ demo**。
+
+`linkedDemos`(写入 spec):`click-copy` → `demos/basic.tsx`,`custom-button` → `demos/custom.tsx`,`disabled` → `demos/disabled.tsx`
+
+## 给 AI 的项目提示
+
+- **组件路径**:`src/copy/index.tsx`
+- **实现更新**:Prompt ④ 或 `pnpm spec:generate -- --component Copy --artifact impl`
+- **Demo 生成**:Prompt ②′ 或 `pnpm spec:generate -- --component Copy --artifact demo`
+- **测试栈**:Jest + `@testing-library/react` + `@testing-library/user-event` + `jest-dom`
+- **测试目录**:`src/copy/__tests__/copy.test.tsx`(spec 派生时可覆盖或对齐现有用例)
+- **Mock 建议**:
+ - `@dtinsight/dt-utils` → `CopyUtils.prototype.copy = jest.fn((text, cb) => cb?.())`
+ - `antd` → `message.success = jest.fn()`(若测默认 onCopy)
+- **运行**:`pnpm exec jest copy.test.tsx --no-coverage`
+- **API 同步**:`pnpm spec:generate -- --component Copy --artifact api`
+- **样式前缀**:`dtc-copy`
diff --git a/src/copy/Copy.spec.yaml b/src/copy/Copy.spec.yaml
new file mode 100644
index 000000000..8cccd278d
--- /dev/null
+++ b/src/copy/Copy.spec.yaml
@@ -0,0 +1,125 @@
+# @generated-from: Copy.brief.md
+component: Copy
+
+# 无 extendsAntd — Copy 不继承 antd 组件 props
+
+props:
+ text:
+ type: string
+ required: true
+ description: 需要复制的文本
+ button:
+ type: React.ReactNode
+ description: 自定义按钮
+ default: '``'
+ className:
+ type: string
+ description: 样式 class
+ disabled:
+ type: boolean
+ description: 是否禁用
+ default: false
+ style:
+ type: React.CSSProperties
+ description: 样式
+ tooltip:
+ type: "TooltipProps['title'] | TooltipProps"
+ description: 配置提示信息
+ default: '`复制`'
+ onCopy:
+ type: '(text: string) => void'
+ description: 复制后的回调函数
+ default: "`() => message.success('复制成功')`"
+
+scenarios:
+ - id: default-render
+ description: 默认渲染可点击区域
+ given:
+ text: hello
+ expect:
+ - renders .dtc-copy
+
+ - id: click-copy
+ description: 点击触发复制
+ given:
+ text: hello
+ disabled: false
+ expect:
+ - CopyUtils.copy called with hello
+ - onCopy invoked with hello
+
+ - id: disabled
+ description: 禁用时不触发复制
+ given:
+ text: hello
+ disabled: true
+ expect:
+ - CopyUtils.copy not called
+ - element has dtc-copy--disabled
+
+ - id: custom-button
+ description: 自定义 button 节点
+ given:
+ text: hello
+ button: 复制文本
+ expect:
+ - renders custom button
+ - click triggers copy
+
+ - id: tooltip-string
+ description: tooltip 字符串
+ given:
+ text: hello
+ tooltip: 复制文本
+ expect:
+ - Tooltip title is 复制文本
+
+ - id: tooltip-object
+ description: tooltip 对象
+ given:
+ text: hello
+ tooltip:
+ title: 复制文本
+ expect:
+ - Tooltip configured with title
+
+ - id: tooltip-falsy
+ description: tooltip 假值时不展示浮层
+ given:
+ text: hello
+ tooltip: false
+ expect:
+ - no visible Tooltip overlay
+
+linkedDemos:
+ - scenarioId: click-copy
+ demo: demos/basic.tsx
+ - scenarioId: custom-button
+ demo: demos/custom.tsx
+ - scenarioId: disabled
+ demo: demos/disabled.tsx
+
+outOfScope:
+ - CopyUtils implementation
+ - clipboard permission errors
+ - Tooltip animation timing
+ - exact locale strings
+
+testFixtures:
+ mocks:
+ CopyUtils: "jest.fn((text, cb) => cb?.())"
+ message.success: jest.fn()
+
+harnessMeta:
+ controls:
+ - prop: text
+ type: string
+ - prop: disabled
+ type: boolean
+ - prop: tooltip
+ type: string
+ presets:
+ - default-render
+ - click-copy
+ - disabled
+ - tooltip-falsy
diff --git a/src/copy/index.md b/src/copy/index.md
index 7d60e6f6a..8df7f91f6 100644
--- a/src/copy/index.md
+++ b/src/copy/index.md
@@ -20,12 +20,16 @@ demo:
### API
+
+
| 参数 | 说明 | 类型 | 默认值 |
| --------- | ---------------- | --------------------------------------- | ----------------------------------- |
+| text | 需要复制的文本 | `string` | -- |
| button | 自定义按钮 | `React.ReactNode` | `` |
-| className | 样式 | `string` | -- |
+| className | 样式 class | `string` | -- |
| disabled | 是否禁用 | `boolean` | false |
| style | 样式 | `React.CSSProperties` | -- |
-| text | 需要复制的文本 | `string` | -- |
| tooltip | 配置提示信息 | `TooltipProps['title'] \| TooltipProps` | `复制` |
| onCopy | 复制后的回调函数 | `(text: string) => void` | `() => message.success('复制成功')` |
+
+