@qilitt-mickey/vue3-temp-skill 1.1.30 → 1.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,6 +44,27 @@ Node.js >= 22.12.0
44
44
  npx @qilitt-mickey/vue3-temp-skill@latest install all
45
45
  ```
46
46
 
47
+ ### 设计落地管控(在业务项目根目录)
48
+
49
+ 业务项目内写入 `.handoff/handoff.json` 后:
50
+
51
+ ```bash
52
+ # 初始化 session(记录 git baseline)
53
+ npx @qilitt-mickey/vue3-temp-skill handoff init --project ./my-vue-app --file .handoff/handoff.json
54
+
55
+ # 落地完成后机器闸门(步骤 + JSON 规格)
56
+ npx @qilitt-mickey/vue3-temp-skill handoff check --project ./my-vue-app --require-session
57
+ ```
58
+
59
+ 源码开发时可直接:
60
+
61
+ ```bash
62
+ node scripts/handoff-session-init.mjs --project <业务项目> --file .handoff/handoff.json
63
+ node scripts/handoff-governance.mjs --project <业务项目> --require-session
64
+ ```
65
+
66
+ 详见 `references/handoff-governance.md`。
67
+
47
68
  | 目标 | 说明 |
48
69
  |------|------|
49
70
  | 个人 Skill 目录 | 安装器写入当前 AI 环境支持的 Skill 目录 |
package/SKILL.md CHANGED
@@ -17,7 +17,10 @@ tags: [vue3, typescript, element-plus, vant, pinia, vite, unocss, crud, componen
17
17
  |------|------|
18
18
  | `python scripts/match.py "<需求>"` | **运行时匹配器**(AI 调用):输出命中模块、同批加载、闸门项、文件清单 |
19
19
  | `python scripts/match.py --batch` | 批量测试 15 个典型场景 |
20
- | `npm run check` | **发布前全量检查**(5 项任一失败即中断) |
20
+ | `npm run check` | **发布前全量检查**(skill 包结构,5 项) |
21
+ | `node scripts/handoff-governance.mjs --project <业务项目>` | **设计落地管控闸门**(步骤 + JSON 规格) |
22
+ | `vue3-temp-skill handoff init --project <业务项目> --file .handoff/handoff.json` | 初始化 `.handoff/session.json`(npm/npx 入口) |
23
+ | `vue3-temp-skill handoff check --project <业务项目> --require-session` | 设计落地完成前必跑 |
21
24
  | `npm run validate` | 结构校验:frontmatter、路由表一致性、孤立模块、交叉引用 |
22
25
  | `npm run audit` | 命中精度审计:关键词重叠、盲区、模块评分、边界冲突 |
23
26
  | `npm run content` | 内容可用性审计:样板完整性、Must-do 清单 |
@@ -77,7 +80,7 @@ tags: [vue3, typescript, element-plus, vant, pinia, vite, unocss, crud, componen
77
80
 
78
81
  ## 设计落地(handoff)
79
82
 
80
- 设计技能每次产出时**始终自动附加 `design-handoff/v1` JSON**。本技能检测到 JSON 后**强制加载** `references/design-handoff.md`,按 C/T/F/Co/S/P 六步全流程执行(配置→令牌→框架→共享组件→样式→页面),**任一步跳过 = 硬失败**。执行完毕后**强制加载** `references/design-verify.md` 逐项验证还原度(区域级 + 逐属性 CSS 校验)。设计产出优先级高于本技能出厂默认值,页面保留业务逻辑、重写视觉结构。用户无需提及「JSON」或「handoff」——这是两技能间的自动协议。完整规则与逐项清单见 `design-handoff.md`,验证流程见 `design-verify.md`。
83
+ 设计技能每次产出时**始终自动附加 `design-handoff/v1` JSON**(含 **`structureRequirements` + `interactionSpecs`**)。本技能检测到 JSON 后**强制加载** `design-handoff.md` + `handoff-governance.md`,按 C/T/F/Co/S/P 执行。完成后执行 design-verify,并运行 **`vue3-temp-skill handoff check --project <业务项目> --require-session`**(或 `node scripts/handoff-governance.mjs --project <业务项目> --require-session`)。**lint 通过 ≠ 设计落地完成**。
81
84
 
82
85
  ### 项目级模式 vs 单页模式
83
86
 
@@ -123,7 +126,7 @@ tags: [vue3, typescript, element-plus, vant, pinia, vite, unocss, crud, componen
123
126
  | addon / 查 Re* 名 / 装依赖 | `references/project-inventory.md` |
124
127
  | **启用精简模板已删的 addon(装包 + 改配置)** | `references/addon-enable.md`(与功能模块同读) |
125
128
  | 写完代码后(闸门,非功能命中) | `references/code-quality.md` |
126
- | 设计交接块 / **design-handoff/v1 JSON** / 上游令牌产出落地 | `references/design-handoff.md`(逐文件修改清单)+ `design-verify.md`(还原度验证)+ `design-system.md`(项目设计系统说明) |
129
+ | 设计交接块 / **design-handoff/v1 JSON** / 上游令牌产出落地 | `references/design-handoff.md` + `references/handoff-governance.md` + `design-verify.md` + `design-system.md` |
127
130
  | **设计模式**(设计 Skill 产出存在时触发) | `crud-pages.md` 设计模式样板 + `design-handoff.md` 1:1 还原要求(F1-F14 / P1-P7) |
128
131
 
129
132
  ### 标准业务面
@@ -223,8 +226,9 @@ tags: [vue3, typescript, element-plus, vant, pinia, vite, unocss, crud, componen
223
226
  ## 工作流
224
227
 
225
228
  1. **需求分析**:列出功能模块 ID(不含 `code-quality`)
226
- 2. **加载**:`core-kernel` + 命中模块;addon/缺包时加读 `project-inventory` + **`addon-enable`**;**检测 design-handoff/v1 JSON 是否存在** — 若存在,强制加载 `design-handoff`(逐文件修改清单,含 C/T/F/Co/S/P 全流程 + 共享组件映射 + 项目级模式)+ `design-verify`(还原度验证)+ `design-system.md`,并按设计产出优先级执行
227
- 3. **模式判断**:有 handoff JSON 时,读取 `meta.transformScope`:
229
+ 2. **加载**:`core-kernel` + 命中模块;addon/缺包时加读 `project-inventory` + **`addon-enable`**;**检测 design-handoff/v1 JSON** — 若存在,强制加载 `design-handoff` + **`handoff-governance`** + `design-verify` + `design-system.md`
230
+ 3. **handoff 初始化**:有 JSON `vue3-temp-skill handoff init --project <业务项目>` → 输出规范识别回执
231
+ 4. **模式判断**:有 handoff JSON 时,读取 `meta.transformScope`:
228
232
  - `project` → 进入**项目级模式**:先扫描 `src/router/modules/` 构建页面清单,输出「待改造页面清单」供用户确认
229
233
  - `single-page` 或未设置 → 单页模式,仅处理当前页面
230
234
  4. **依赖**:缺包按 inventory 第九节精确版本,确认后安装;**再按 `addon-enable` 对应章节改配置**
@@ -233,10 +237,11 @@ tags: [vue3, typescript, element-plus, vant, pinia, vite, unocss, crud, componen
233
237
  - 单页模式:C/T/F/Co/S + 当前页面 P 步骤
234
238
  - 项目级模式:C/T/F/Co/S(全局一次) + 逐页面执行 P 步骤,标记完成状态
235
239
  - **有 handoff JSON 时,页面结构以设计输出为准,保留业务逻辑**
236
- 7. **设计还原度验证**:有 handoff JSON 时必须执行,加载 `design-verify.md` 按其流程逐区域逐元素对照设计规范验证(10.1-10.7 区域级 + 10.8 逐属性 CSS 校验),输出「设计还原度验证报告」+「逐属性 CSS 校验报告」,未通过项必须修正
237
- 8. **功能完整性核查**:有 handoff JSON 时必须执行,按 `design-handoff.md` 第十一章逐项核查功能是否正常(框架/共享组件/页面),样式改造不得破坏原有功能,发现问题立即修正
238
- 9. **code-quality**:按 `references/code-quality.md` 执行;**有 handoff JSON 时,额外执行 handoff 落地完整性校验**(项目级模式需校验全页面完成度 + 共享组件修改 + 设计还原度 + 功能完整性)
239
- 10. **输出**:变更说明(含已改配置文件列表)+ 设计还原度验证报告 + 逐属性 CSS 校验报告 + 功能完整性核查报告 + 待确认项;**项目级模式未全部完成时,输出进度 + 待改造页面清单 + 「继续」指令提示**
240
+ 7. **设计还原度验证**:有 handoff 时必须执行 design-verify,落盘 `.handoff/verify-report.json`
241
+ 8. **handoff 管控闸门**:`vue3-temp-skill handoff check --project <业务项目> --require-session` **必须 exit 0**
242
+ 9. **功能完整性核查**:有 handoff 时按 design-handoff 第十一章
243
+ 10. **code-quality**:lint + type-check + 硬失败表
244
+ 11. **输出**:变更说明 + verify 报告 + **handoff 管控结果**
240
245
 
241
246
  ### 最终回复结构
242
247
 
@@ -248,8 +253,9 @@ tags: [vue3, typescript, element-plus, vant, pinia, vite, unocss, crud, componen
248
253
  - 闸门:已执行 code-quality(生成后)
249
254
  - 清单:选型 / 类型 / 组件 / API / 安全 / 业务 / 列表→详情(如适用) / 列表布局(如适用) / 风格 / addon启用(如适用) → 通过 | 已修正:…
250
255
  - 硬失败:无 | 已拦截并修正:…
251
- - handoff 落地(如有 JSON):C/T/F/Co/S/P 全流程已执行 → 通过 | 未完成项:…(硬失败)
252
- - 还原度验证(如有 JSON):design-verify.md 区域级 + 逐属性校验已执行 → 通过 | 偏差项:…(已修正 / 待确认)
256
+ - handoff 落地(如有 JSON):C/T/F/Co/S/P 全流程 → 通过 | 未完成项:…
257
+ - handoff 管控:`vue3-temp-skill handoff check` → 通过 | 失败:…
258
+ - 还原度验证(如有 JSON):design-verify + verify-report.json → 通过 | 偏差项:…
253
259
  - 命令:`pnpm lint:fix` → …;`pnpm type-check` → …
254
260
  - 待确认:…
255
261
 
package/bin/cli.js CHANGED
@@ -251,6 +251,11 @@ function installSkills(targetKey, projectDir) {
251
251
  copyDir(refsSrc, path.join(dest, "references"));
252
252
  }
253
253
 
254
+ const scriptsSrc = path.join(SKILL_DIR, "scripts");
255
+ if (fs.existsSync(scriptsSrc)) {
256
+ copyDir(scriptsSrc, path.join(dest, "scripts"));
257
+ }
258
+
254
259
  // Cursor:清理旧版 rules/*.mdc,避免与 skills 重复
255
260
  if (cfg.legacyRulesFile) {
256
261
  const legacy = cfg.legacyRulesFile(projectDir);
@@ -337,6 +342,8 @@ ${bold("命令:")}
337
342
  update [target] 更新已安装的技能(默认更新全部已安装目标)
338
343
  uninstall [target] 卸载技能(默认卸载全部已安装目标)
339
344
  list 查看已安装的目标和版本
345
+ handoff init 初始化业务项目 .handoff/session.json
346
+ handoff check 设计落地管控闸门(步骤 + JSON 规格)
340
347
 
341
348
  ${bold("目标 (target):")}
342
349
  cursor → Cursor ~/.cursor/skills/
@@ -370,8 +377,9 @@ ${bold("示例:")}
370
377
  ${dim("# 查看已安装情况")}
371
378
  npx @qilitt-mickey/vue3-temp-skill list
372
379
 
373
- ${dim("# 卸载")}
374
- npx @qilitt-mickey/vue3-temp-skill uninstall cursor
380
+ ${dim("# 设计落地管控(在业务项目根目录执行)")}
381
+ npx @qilitt-mickey/vue3-temp-skill handoff init --project ./my-app --file .handoff/handoff.json
382
+ npx @qilitt-mickey/vue3-temp-skill handoff check --project ./my-app --require-session
375
383
  `);
376
384
  }
377
385
 
@@ -591,6 +599,45 @@ function cmdList() {
591
599
  }
592
600
  }
593
601
 
602
+ function cmdHandoff(sub, args, projectDir) {
603
+ const proj = projectDir || process.cwd();
604
+ const fileIdx = args.indexOf("--file");
605
+ const handoffFile =
606
+ fileIdx !== -1 ? path.resolve(proj, args[fileIdx + 1]) : path.join(proj, ".handoff", "handoff.json");
607
+ const requireSession = args.includes("--require-session");
608
+ const scriptDir = path.join(SKILL_DIR, "scripts");
609
+
610
+ if (sub === "init") {
611
+ const script = path.join(scriptDir, "handoff-session-init.mjs");
612
+ try {
613
+ execSync(`"${process.execPath}" "${script}" --project "${proj}" --file "${handoffFile}"`, {
614
+ stdio: "inherit",
615
+ });
616
+ } catch (e) {
617
+ process.exit(e.status ?? 1);
618
+ }
619
+ return;
620
+ }
621
+
622
+ if (sub === "check") {
623
+ const script = path.join(scriptDir, "handoff-governance.mjs");
624
+ const extra = requireSession ? " --require-session" : "";
625
+ try {
626
+ execSync(`"${process.execPath}" "${script}" --project "${proj}"${extra}`, {
627
+ stdio: "inherit",
628
+ });
629
+ } catch (e) {
630
+ process.exit(e.status ?? 1);
631
+ }
632
+ return;
633
+ }
634
+
635
+ console.error(red("✗ handoff 子命令: init | check"));
636
+ console.error(dim(" handoff init --project <dir> [--file .handoff/handoff.json]"));
637
+ console.error(dim(" handoff check --project <dir> [--require-session]"));
638
+ process.exit(1);
639
+ }
640
+
594
641
  // ============================================================
595
642
  // 入口
596
643
  // ============================================================
@@ -626,6 +673,9 @@ function main() {
626
673
  case "list":
627
674
  cmdList();
628
675
  break;
676
+ case "handoff":
677
+ cmdHandoff(target, args.slice(2), projectDir);
678
+ break;
629
679
  default:
630
680
  // 兼容旧用法:直接传 target 名当作 install
631
681
  if (TARGETS[command] || command === "all") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qilitt-mickey/vue3-temp-skill",
3
- "version": "1.1.30",
3
+ "version": "1.1.32",
4
4
  "description": "Vue 3 企业级中后台项目开发规范技能包 — core-kernel 架构、按需功能模块",
5
5
  "bin": {
6
6
  "vue3-temp-skill": "./bin/cli.js"
@@ -36,6 +36,8 @@
36
36
  "gen:index": "node scripts/generate-index.mjs",
37
37
  "gen:index:write": "node scripts/generate-index.mjs --write",
38
38
  "scaffold": "node scripts/scaffold-module.mjs",
39
+ "handoff:init": "node scripts/handoff-session-init.mjs",
40
+ "handoff:check": "node scripts/handoff-governance.mjs",
39
41
  "postpublish": "echo 'Published @qilitt-mickey/vue3-temp-skill to npmjs.org'"
40
42
  },
41
43
  "keywords": [
@@ -13,9 +13,10 @@ tags: [code-review, quality, checklist, lint, typescript, vue3, security, perfor
13
13
 
14
14
  1. 重读本文件
15
15
  2. 列出本轮改动路径,对照下方清单;**硬失败表命中则先改代码再继续**
16
- 3. 业务项目根目录必跑:`pnpm lint:fix` `pnpm type-check`(无脚本则 `pnpm lint` / `pnpm exec eslint . --fix`;`pnpm exec vue-tsc --noEmit`)
17
- 4. 失败则修代码并重跑,直至通过或写明阻塞
18
- 5. 最终回复追加:
16
+ 3. **有 handoff session 时**(`.handoff/session.json` 存在):必跑 `vue3-temp-skill handoff check --project <业务项目> --require-session` **exit 0 才能声称设计落地完成**(见 `handoff-governance.md`)
17
+ 4. 业务项目根目录必跑:`pnpm lint:fix` → `pnpm type-check`
18
+ 5. 失败则修代码并重跑,直至通过或写明阻塞
19
+ 6. 最终回复追加:
19
20
 
20
21
  ```markdown
21
22
  ## 代码质量审查
@@ -23,6 +24,7 @@ tags: [code-review, quality, checklist, lint, typescript, vue3, security, perfor
23
24
  - 改动文件:`path/a`、`path/b`、…
24
25
  - 清单:选型 ✅ / 类型 ✅ / 组件 ✅ / API ✅ / 安全 ✅ / 业务 ✅ / 列表→详情 ✅或N/A / 列表布局 ✅或N/A / 风格 ✅
25
26
  - 硬失败:无 | 已拦截并修正:…
27
+ - handoff 管控(如有 session):`vue3-temp-skill handoff check` → 通过 | 失败项:…
26
28
  - 已修正:…(无则写「无」)
27
29
  - 命令:`pnpm lint:fix` → …;`pnpm type-check` → …
28
30
  - 待确认:…
@@ -128,6 +130,17 @@ lint / type-check **过了也不能放过**下列项。发现即改代码,禁
128
130
  - [ ] 无遗漏页面(对照路由清单逐一核对)
129
131
  - [ ] 所有页面的多余元素(「展示列表」「左侧按钮...」等)均已清除
130
132
 
133
+ #### 设计落地管控(有 `.handoff/session.json` 时必须执行)
134
+
135
+ > 详见 `references/handoff-governance.md`。lint/type-check **不能**代替本段。
136
+
137
+ - [ ] 动手前已 `handoff-session-init`,`.handoff/session.json` 含完整 handoff + baseline
138
+ - [ ] handoff JSON 含 `structureRequirements` + `interactionSpecs`
139
+ - [ ] C/T/F/Co/S/P 全流程已执行(`handoff-step-check` 通过)
140
+ - [ ] 代码与当次 JSON 一致(`handoff-spec-check` 通过)
141
+ - [ ] 已落盘 `.handoff/verify-report.json`(`overallPass: true`)
142
+ - [ ] **`vue3-temp-skill handoff check --project <业务项目> --require-session` → exit 0**
143
+
131
144
  #### 设计还原度验证(有 handoff JSON 时必须执行)
132
145
 
133
146
  > 验证依据:设计 skill 的规范文件(design-tokens.md / unified-framework.md / list-page.md / component-spec.md)
@@ -141,8 +154,10 @@ lint / type-check **过了也不能放过**下列项。发现即改代码,禁
141
154
  - [ ] **细节项还原度**:图标大小 16px、分隔线 #F0F0F0、hover 文字加深不变浅、行高正确、字重正确、圆角统一、阴影自然、更多按钮有箭头、操作列有竖分隔线、页签下划线居中、操作列 sticky、选择列 48px、状态标签颜色对应、卡片有阴影,共 15 项已逐项检查
142
155
  - [ ] **令牌溯源检查**:颜色/尺寸/间距均通过 CSS 变量传递,变量值与 design-tokens.md 一致,无裸 hex/px 值
143
156
  - [ ] **元素清理检查**:设置按钮、展示列表标题、示例按钮、操作列多余图标等已全部移除
144
- - [ ] **已输出「设计还原度验证报告」**(格式见 `design-verify.md` 10.7),包含整体还原度、各层通过率、偏差项及修正方案
145
- - [ ] **逐属性 CSS 校验已执行**(`design-verify.md` 10.8):从设计源文件提取 CSS 属性清单,逐条比对项目代码,不一致项已修正并回读校验通过
157
+ - [ ] **已输出「设计还原度验证报告」**(格式见 `design-verify.md` 10.7),包含整体还原度、各层通过率、偏差项及修正方案
158
+ - [ ] **CSS 冲突检测已执行**(`design-verify.md` 10.7.5):选择器重复扫描完成,无意冲突已消除,导入顺序已验证,新增 CSS 文件已注册
159
+ - [ ] **已输出「CSS 冲突检测报告」**(格式见 `design-verify.md` 10.7.5.5),包含扫描范围、重复选择器明细、导入顺序验证结果
160
+ - [ ] **逐属性 CSS 校验已执行**(`design-verify.md` 10.8):从设计源文件提取 CSS 属性清单,逐条比对项目代码,不一致项已修正并回读校验通过
146
161
  - [ ] **已输出「逐属性 CSS 校验报告」**(格式见 `design-verify.md` 10.8.5),包含校验范围、偏差明细、待确认项
147
162
  - [ ] **设计落盘范围**:设计产出 HTML 未写入项目文件,设计规范未另存到非业务目录,所有修改只在 src/ 等业务代码中
148
163
  - [ ] **功能完整性核查**:样式改造后原有功能全部正常(框架层/共享组件层/页面层均已逐项核查,无按钮失效、事件丢失、数据绑定断裂等功能回归问题)
@@ -82,6 +82,8 @@ tags: [handoff, design-tokens, upstream, artifacts, json, modification-checklist
82
82
 
83
83
  ## 规范识别回执(动手前必出)
84
84
 
85
+ > 动手前须 `handoff-session-init` 写入 `.handoff/session.json`(见 `handoff-governance.md`)。
86
+
85
87
  ```markdown
86
88
  ## 规范识别回执
87
89
  - 来源形态:design-handoff/v1 JSON
@@ -90,9 +92,9 @@ tags: [handoff, design-tokens, upstream, artifacts, json, modification-checklist
90
92
  - 几何:header {n} / tabs {n} / margin {n}
91
93
  - 主色阶:完整 10 级
92
94
  - 侧栏:{shell.sidebar}
93
- - 结构要求:{structureRequirements.frameworkShell.header.length}
94
- - 交互要求:{Object.keys(interactionSpecs).length}
95
- - 将改文件:platform-config.json / design-tokens.scss / navbar / sidebar / tabs / app-main / element-plus.scss / dark.scss / 业务页面
95
+ - 顶栏元素(JSON):{structureRequirements.frameworkShell.header 逐项列出}
96
+ - 交互规格(JSON):tabsActive.underline / tableOperate.showIcon / menuActive.fullWidth 等
97
+ - 将改文件:platform-config.json / design-tokens.scss / navbar / sidebar / tabs / ReTableBar / ReTableOperate / element-plus.scss / 业务页面
96
98
  ```
97
99
 
98
100
  ---
@@ -286,16 +288,16 @@ tags: [handoff, design-tokens, upstream, artifacts, json, modification-checklist
286
288
  | **修改内容** | 1. 分页布局:左右两段式(左侧总数 + 每页条数选择;右侧页码 + 跳转)<br>2. 默认每页条数:10<br>3. 可选条数:10/20/50<br>4. 跳转输入框宽度 48px<br>5. 激活页按钮:品牌色背景 + 白色文字 |
287
289
  | **验证** | 分页样式与设计规范一致 |
288
290
 
289
- ### Co.9 侧栏菜单激活态 — 实色底修正
291
+ ### Co.9 侧栏菜单激活态
290
292
 
291
293
  | 项 | 说明 |
292
294
  |----|------|
293
295
  | **文件** | `src/styles/sidebar.scss` |
294
- | **设计规范** | unified-framework.md §2 Sider:激活项为实色品牌底 + 左侧 3px 指示条 + 白色文字 |
295
- | **修改内容** | 1. 激活态背景:`var(--sidebar-menu-active-bg, var(--brand-primary))`(实色,非 8% 透明)<br>2. 左侧指示条:`::before` 伪元素,width 3px,height 100%,背景 `var(--sidebar-active-indicator, var(--brand-primary))`<br>3. 激活态文字颜色:`var(--sidebar-menu-active-text, #fff)`(白色)<br>4. 确保 `--sidebar-menu-active-bg` 令牌值为实色,而非 `color-mix(... 8%, transparent)` |
296
- | **验证** | 侧栏激活菜单项为实色品牌底 + 左侧 3px 指示条 + 白色文字 |
296
+ | **JSON 来源** | `interactionSpecs.menuActive` |
297
+ | **修改内容** | JSON 实现:`fullWidth` / `marginX` / `radius` / `background` / `indicator` / `textColor` |
298
+ | **验证** | sidebar 激活态与 JSON 一致 |
297
299
 
298
- > ⚠️ 注意:当前模板 `sidebar.scss` 中激活态背景是 `color-mix(in srgb, var(--brand-primary) 8%, transparent)`,这与设计规范的「实色底」不符。Co9 步骤必须将其修正为实色。
300
+ > ⚠️ **以当次 JSON 为准**:本规范默认 `fullWidth:false` + `marginX:8` + `radius:6` + `solid-brand` + `left-before`(见 unified-framework §6.13–6.16)。若 JSON 不同,按 JSON 实现,不得硬套「通栏实色底」。
299
301
 
300
302
  ### Co.10 navbar — 折叠按钮垂直居中
301
303
 
@@ -498,49 +500,58 @@ tags: [handoff, design-tokens, upstream, artifacts, json, modification-checklist
498
500
 
499
501
  ## 八、硬失败(任一项命中 = 落地失败,必须修正)
500
502
 
503
+ > **硬失败分两类**(详见 `handoff-governance.md`):
504
+ > - **流程类**:步骤跳过、无 session、无 verify 报告、lint 代替管控 — **与设计无关,永远检查**
505
+ > - **规格类**:代码与**当次** handoff JSON 的 `structureRequirements` / `interactionSpecs` 不一致 — **随设计变,不写死在本表**
506
+
507
+ ### 8.1 流程硬失败(机器可拦)
508
+
509
+ | 类别 | 条件 | 处理 |
510
+ |------|------|------|
511
+ | 管控 | 有 handoff 但未 `handoff-session-init` | 硬失败:先写 `.handoff/session.json` |
512
+ | 管控 | `handoff-step-check` 任一步(C/T/F/Co/S)文件无 diff | 硬失败:补全该步骤,禁止跳过 Co |
513
+ | 管控 | 只改 C/T 未改 Co 共享组件即声称完成 | 硬失败:执行 Co1-Co12 |
514
+ | 管控 | 未执行 `vue3-temp-skill handoff check --require-session` | 硬失败:机器闸门 exit 0 后才能完成 |
515
+ | 管控 | 以 `lint:fix` / `type-check` 通过代替 handoff 管控 | 硬失败:二者是必要非充分条件 |
516
+ | 管控 | 有 handoff 但未落盘 `.handoff/verify-report.json` | 硬失败:design-verify 报告必须落盘 |
517
+ | 项目级 | 项目级模式下未扫描全部路由即开始改造 | 硬失败:先完成 Step 0 页面清单初始化 |
518
+ | 项目级 | 项目级模式下未完成全部页面即声称完成 | 硬失败:必须告知进度和剩余页面 |
519
+ | 设计落地 | 设计 HTML / 规范文档被写入项目非业务目录 | 硬失败:撤销,设计修改只落在 src/ 等业务代码 |
520
+ | 功能回归 | 样式改造后原有功能被破坏 | 硬失败:修复后重新验证 |
521
+ | 逐属性校验 | 10.8 发现不一致且未修复 | 硬失败:零差异后才能声明完成 |
522
+ | CSS 冲突 | 10.7.5 未执行就进 10.8 | 硬失败:先冲突检测 |
523
+
524
+ ### 8.2 规格硬失败(读当次 JSON + spec-check)
525
+
526
+ | 类别 | 条件 | 处理 |
527
+ |------|------|------|
528
+ | JSON | handoff 缺少 `structureRequirements` 或 `interactionSpecs` | 硬失败:要求设计 skill 补全 JSON |
529
+ | 框架-顶栏 | 顶栏元素与 `structureRequirements.frameworkShell.header` 不一致 | 硬失败:按 JSON 逐项对齐 |
530
+ | 框架-顶栏 | `interactionSpecs.languageSwitch.mode=inline-text` 但仍是下拉框 | 硬失败:改内联文字 |
531
+ | 框架-侧栏 | 与 `interactionSpecs.menuActive` 不一致(fullWidth/radius/background) | 硬失败:按 JSON 修正 |
532
+ | 框架-页签 | 与 `interactionSpecs.tabsActive` 不一致(underline/fontWeight 等) | 硬失败:按 JSON 修正 |
533
+ | 共享组件 | 与 `interactionSpecs.tableOperate` 不一致 | 硬失败:按 JSON 修正 ReTableOperate |
534
+ | 共享组件 | 与 `interactionSpecs.tableToolbar` 不一致 | 硬失败:按 JSON 修正 ReTableBar |
535
+ | 页面 | 与 `structureRequirements.moduleComposition` 不一致 | 硬失败:补全区域结构 |
536
+ | 页面 | `structureRequirements.elementHideList` 项仍在代码中 | 硬失败:隐藏/删除 |
537
+ | 页面 | 状态 Tag 与 `interactionSpecs.statusTag` 不一致 | 硬失败:按 JSON 修正 |
538
+ | 页面 | `structureRequirements` 或 `interactionSpecs` 段未实现 | 硬失败:按 F1-F27 / P1-P7 + JSON 补充 |
539
+
540
+ ### 8.3 配置 / 令牌 / 样式硬失败(与 JSON 字段对照)
541
+
501
542
  | 类别 | 条件 | 处理 |
502
543
  |------|------|------|
503
544
  | 配置 | 只改主色/tokens,未改 Theme / Version | 硬失败:补全 C1-C8 |
504
545
  | 配置 | 设计 `sidebar=dark-gradient` 但 `Theme` 仍为 `light` | 硬失败:改为 `default` 并 `Version++` |
505
546
  | 配置 | 未写入 `shell.showModel` | 硬失败:按 JSON 补写 |
506
547
  | 配置 | `primaryScale` 未写入 `--primary-0..9` | 硬失败:补全 10 级色阶 |
507
- | 令牌 | 用 EP 默认错误色 `#F56C6C` 而非令牌引用 | 硬失败:改为 `var(--error-color)` 或对应令牌值 |
548
+ | 令牌 | 用 EP 默认错误色 `#F56C6C` 而非令牌引用 | 硬失败:改为 `var(--error-color)` |
508
549
  | 令牌 | 壳层/弹窗写死 px | 硬失败:改 `var(--…)` |
509
550
  | 令牌 | 改了 margin 后列表仍用 `100vh` 减顶栏 | 硬失败:迁 flex / `vts-page-card` |
510
- | 框架-顶栏 | 顶栏元素与设计不一致(多了或少了) | 硬失败:按 `frameworkShell.header` 逐项对齐 |
511
- | 框架-顶栏 | 语言切换为下拉框(非内联文字) | 硬失败:改为内联文字 |
512
- | 框架-顶栏 | 设置按钮设计无但代码中还在 | 硬失败:删除或 `display:none` |
513
- | 框架-侧栏 | 侧栏底部折叠按钮设计无但还在 | 硬失败:隐藏,折叠按钮移至顶部 |
514
- | 框架-侧栏 | 菜单 hover 出现折叠功能设计无但还在 | 硬失败:禁用该功能 |
515
- | 框架-侧栏 | 菜单激活态无品牌色底+强调条 | 硬失败:按 `interactionSpecs.menuActive` 补充 |
516
- | 框架-页签 | 页签横线不居中 | 硬失败:调整下划线位置,确保与文字等宽居中 |
517
- | 框架-页签 | 首页页签可关闭 | 硬失败:设为不可关闭 |
518
- | 样式 | 展开收起按钮 hover 文字变浅 | 硬失败:hover 文字色加深或变色,**禁止变淡** |
519
- | 样式 | 表格行 hover 无品牌色底 | 硬失败:补充 hover 背景色 |
520
- | 样式 | 卡片/按钮 hover 无浮起效果 | 硬失败:补充 transform + shadow |
521
- | 样式 | 输入框聚焦无光环 | 硬失败:补充 focus 阴影 |
522
- | 页面 | 页面无标题区 / 筛选区无容器感 | 硬失败:按 `moduleComposition` 补充 |
523
- | 页面 | 状态标签样式与设计规范不一致 | 硬失败:按 `component-spec.md` Tag 规范修正 |
524
- | 页面 | 操作列按钮样式混乱(混用 button/文字链接) | 硬失败:统一为文字链接 + "更多"下拉 |
525
- | 页面 | 设计中未体现的元素未隐藏 | 硬失败:按元素自查隐藏清单逐项处理 |
526
- | 页面 | `structureRequirements` 或 `interactionSpecs` 段未实现 | 硬失败:按 F1-F27 / P1-P7 补充 |
527
- | 共享组件 | ReTableBar 工具栏按钮样式与设计规范不一致(刷新/密度/列设置尺寸、间距、分隔线) | 硬失败:按 Co2 修正 |
528
- | 共享组件 | ReTableOperate 操作列按钮有图标(设计规范为纯文字链接) | 硬失败:按 Co5 移除图标,添加竖分隔线 |
529
- | 共享组件 | 「更多」按钮无下拉箭头(设计规范必须有) | 硬失败:按 Co6 添加下拉箭头 |
530
- | 共享组件 | 侧栏菜单激活态为 8% 透明底(设计规范为实色底) | 硬失败:按 Co9 修正为实色底 |
531
- | 共享组件 | 顶栏设置按钮设计无但代码中还在 | 硬失败:按 Co11 删除或 `v-if="false"` |
532
- | 共享组件 | 折叠按钮未垂直居中 | 硬失败:按 Co10 修正 |
533
- | 共享组件 | 模块分割线间距与设计规范不一致 | 硬失败:按 Co12 修正各区域 padding 和分割线位置 |
534
- | 页面清理 | 列表页仍有「展示列表」等多余标题文字 | 硬失败:按 4.4 清单从模板中删除 |
535
- | 页面清理 | 列表页仍有「左侧按钮可以增加多个」等示例按钮 | 硬失败:按 4.4 清单从模板中删除 |
536
- | 项目级 | 项目级模式下未扫描全部路由即开始改造 | 硬失败:先完成 Step 0 页面清单初始化 |
537
- | 项目级 | 项目级模式下未完成全部页面即声称完成 | 硬失败:必须告知进度和剩余页面 |
538
- | 项目级 | 项目级模式下共享组件未按 Co 步骤修改 | 硬失败:按 Co1-Co12 逐一修正 |
539
- | 设计落地 | 设计 HTML / 规范文档被写入项目非业务目录 | 硬失败:撤销,设计修改只落在 src/ 等业务代码 |
540
- | 功能回归 | 样式改造后原有功能被破坏(按钮不能点、分页不工作、搜索失效等) | 硬失败:样式归样式功能归功能,改样式不得破坏原有功能,必须修复后重新验证 |
541
- | 逐属性校验 | 10.8 逐属性 CSS 校验发现设计值与项目实际值不一致且未修复 | 硬失败:逐条修改至零差异,回读校验通过后才能声明完成 |
542
- | 逐属性校验 | 设计源文件回读校验有未通过项(修改后未重新回读验证) | 硬失败:重新修复并回读,直到零差异 |
543
- | 逐属性校验 | 设计变更同步后未执行全量回读校验(只验变更项未验全量) | 硬失败:必须全量回读,防止变更项误改未变项 |
551
+ | 样式 | `interactionSpecs.collapseBtnHover.textFade=true` 违反时 hover 变浅 | 硬失败:hover 加深 |
552
+ | 页面清理 | 列表页仍有「展示列表」等 demo 文案 | 硬失败:按 elementHideList / 4.4 删除 |
553
+
554
+ > ⚠️ **静态默认值冲突**:F/Co 步骤正文若与当次 JSON / 设计 HTML 矛盾,**以 JSON + 设计 HTML 为准**。
544
555
 
545
556
  ---
546
557
 
@@ -787,9 +798,9 @@ tags: [handoff, design-tokens, upstream, artifacts, json, modification-checklist
787
798
  5. **元素自查隐藏**(第四章)
788
799
  6. **样式修改**(S1)
789
800
  7. **页面代码生成**(P1-P7)
790
- 8. **设计还原度验证** — 加载 `design-verify.md`,执行 10.1-10.7 区域级验证 + 10.8 逐属性 CSS 校验 + 10.9 设计变更同步(如有)
801
+ 8. **设计还原度验证** — 加载 `design-verify.md`,执行 10.1-10.7 区域级验证 + 10.7.5 CSS 冲突检测 + 10.8 逐属性 CSS 校验 + 10.9 设计变更同步(如有)
791
802
  9. **功能完整性核查**(第十一章)—— 检查功能坏没坏
792
803
  10. **code-quality 闸门**
793
- 11. **输出报告**(设计还原度验证报告 + 逐属性校验报告 + 功能完整性核查报告)
804
+ 11. **输出报告**(设计还原度验证报告 + CSS 冲突检测报告 + 逐属性校验报告 + 功能完整性核查报告)
794
805
 
795
- > ⚠️ **三条腿走路**:区域级还原度(10.1-10.7)+ 属性级逐条校验(10.8-10.9)+ 功能完整性(第十一章)缺一不可。只改对样式但功能坏了 = 失败;功能正常但样式有偏差 = 也失败;区域级通过但属性级有遗漏 = 同样失败。三者全部通过才算完成。
806
+ > ⚠️ **四条腿走路**:区域级还原度(10.1-10.7)+ CSS 结构级冲突检测(10.7.5)+ 属性级逐条校验(10.8-10.9)+ 功能完整性(第十一章)缺一不可。只改对样式但功能坏了 = 失败;功能正常但样式有偏差 = 也失败;区域级通过但属性级有遗漏 = 同样失败;属性级通过但 CSS 冲突导致生效值不确定 = 同样失败。四者全部通过才算完成。
@@ -24,6 +24,8 @@ tags: [handoff, verify, css-audit, design-sync, hard-fail]
24
24
 
25
25
  10.6-10.7 验证方法 + 区域级验证报告输出
26
26
 
27
+ 10.7.5 CSS 冲突检测(选择器重复扫描 + 导入顺序验证 + 冲突修复)
28
+
27
29
  10.8 逐属性 CSS 校验(从设计源文件提取属性 → 逐条比对项目代码 → 回读校验)
28
30
 
29
31
  10.9 设计变更增量同步(仅设计变更时执行)
@@ -31,6 +33,35 @@ tags: [handoff, verify, css-audit, design-sync, hard-fail]
31
33
 
32
34
  ---
33
35
 
36
+ ## 10.0 JSON 驱动验证(优先于静态条文)
37
+
38
+ > **验证真相源顺序**:当次 handoff JSON(`structureRequirements` + `interactionSpecs`)> 设计 HTML > 设计 skill references > 模板出厂默认值。
39
+
40
+ 1. 读取 `.handoff/session.json` 或会话中的 handoff JSON
41
+ 2. 从 JSON 展开**本次验证清单**(非 skill 静态表):
42
+ - `frameworkShell.header` → 顶栏元素逐项
43
+ - `interactionSpecs.tabsActive.underline` → 决定是否检查下划线
44
+ - `interactionSpecs.tableOperate.showIcon` → 决定是否禁止 icon
45
+ - `interactionSpecs.menuActive.*` → 侧栏激活 inset/通栏/圆角
46
+ 3. 执行 `vue3-temp-skill handoff check --project <业务项目> --require-session`(内含 step-check + spec-check)作为机器预检
47
+ 4. 人工/AI 区域验证(10.2–10.5)对照 JSON 展开项,**禁止**用上一版设计的静态预期
48
+
49
+ ### 10.0.1 verify-report 落盘
50
+
51
+ design-verify 完成后必须写入项目 `.handoff/verify-report.json`:
52
+
53
+ ```json
54
+ {
55
+ "generatedAt": "<ISO>",
56
+ "overallPass": true,
57
+ "specSource": "session.handoff",
58
+ "layers": { "framework": { "pass": 10, "total": 10 } },
59
+ "deviations": []
60
+ }
61
+ ```
62
+
63
+ ---
64
+
34
65
  ## 10.1 验证原则
35
66
 
36
67
  1. **对照设计规范,不凭感觉**:每一项都有对应的设计规范来源(unified-framework.md / list-page.md / component-spec.md / design-tokens.md),禁止「大概差不多」
@@ -47,9 +78,9 @@ tags: [handoff, verify, css-audit, design-sync, hard-fail]
47
78
  | 折叠按钮位置 | unified-framework.md §3 | 检查折叠按钮是否垂直居中,尺寸是否 40×40px | 垂直居中,40×40px |
48
79
  | 侧栏宽度 | unified-framework.md §2 | 检查 `--sidebar-width` / `--sidebar-collapsed-width` | 展开 208px / 折叠 64px |
49
80
  | 侧栏背景色 | unified-framework.md §2 | 检查侧栏背景色令牌值 | 深色 #001529 |
50
- | 侧栏激活态 | unified-framework.md §2 + list-page.md §12 | 检查激活菜单项的背景、文字、指示条 | 实色品牌底 + 白色文字 + 左侧 3px 指示条 |
81
+ | 侧栏激活态 | unified-framework.md §2 + **JSON `interactionSpecs.menuActive`** | JSON fullWidth/radius/background/indicator 检查 |
51
82
  | 页签高度 | unified-framework.md §4 | 检查 `--tabs-height` + tabs 实际高度 | 40px |
52
- | 页签激活态 | unified-framework.md §4 + component-spec.md §5 | line 模式下检查激活页签样式 | 文字品牌色 + font-weight 500 + 底部 2px 强调线,无背景变化 |
83
+ | 页签激活态 | unified-framework.md §4 + **JSON `interactionSpecs.tabsActive`** | JSON:underline=false 则无底线;underline=true 则检查居中 |
53
84
  | 内容区背景色 | unified-framework.md §5 | 检查内容区背景色 | #F5F5F5(灰底) |
54
85
  | 内容区内边距 | unified-framework.md §5 | 检查 `--layout-content-padding` | 24px |
55
86
 
@@ -168,11 +199,110 @@ AI 在执行设计还原度验证时,按以下步骤操作:
168
199
 
169
200
  ---
170
201
 
202
+ ## 10.7.5 CSS 冲突检测(逐属性校验前置闸门 — 硬失败)
203
+
204
+ > 10.8 逐属性校验依赖「项目代码中每个选择器的实际生效值」。如果同一选择器在多个文件重复定义、导入顺序错乱、或特异性冲突,实际生效值不可预测,10.8 比对结果不可靠。
205
+ > **本节必须在 10.8 之前执行,未通过则不允许进入 10.8。**
206
+
207
+ ### 10.7.5.1 选择器重复扫描
208
+
209
+ 扫描项目全部 `src/**/*.scss`、`src/**/*.css`、`src/**/*.vue`(含 `<style>` 块)文件,检测同一选择器在多个文件或同一文件内多次出现的情况。
210
+
211
+ **检测规则**:
212
+
213
+ 1. 提取每个文件中的全部 CSS 选择器(去掉伪类/伪元素后比对基础选择器)
214
+ 2. 按**选择器文本完全匹配**分组
215
+ 3. 跨文件出现 ≥2 次 = 重复选择器
216
+ 4. 同一文件内出现 ≥2 次 = 文件内重复
217
+
218
+ **输出重复清单**:
219
+
220
+ | 选择器 | 出现次数 | 文件列表 | 行号 | 重复类型 |
221
+ |--------|---------|---------|------|---------|
222
+ | .el-menu-item.is-active | 2 | element-plus.scss:573, sidebar.scss:104 | 573, 104 | 跨文件 |
223
+ | .el-table thead th | 2 | element-plus.scss:342, element-plus.scss:566 | 342, 566 | 文件内 |
224
+
225
+ ### 10.7.5.2 重复选择器分类裁定
226
+
227
+ 对每个重复选择器,判定属于以下哪类:
228
+
229
+ | 类型 | 判定条件 | 处理 |
230
+ |------|---------|------|
231
+ | **有意覆盖** | 后导入文件的选择器特异性 ≥ 前文件,且值确实需要覆盖前者 | ✅ 保留,但必须在选择器上方加注释 `// override: 覆盖 {文件名} 的 {原因}` |
232
+ | **无意冲突** | 两个定义的值矛盾,且覆盖关系不确定 | ❌ 硬失败 — 必须合并为一个定义或删除冗余 |
233
+ | **无害重复** | 两个定义的值完全一致 | ⚠️ 警告 — 删除冗余定义,只保留一个 |
234
+
235
+ **裁定方法**:
236
+ 1. 根据 `index.scss` 的 `@use` 导入顺序确定文件优先级(后导入 = 高优先级)
237
+ 2. 对比特异性(ID > class > tag,`!important` 最高)
238
+ 3. 若优先级+特异性可以确定唯一胜出者 → 检查胜出值是否与设计规范一致
239
+ 4. 若无法确定唯一胜出者 → 硬失败
240
+
241
+ ### 10.7.5.3 导入顺序验证
242
+
243
+ 读取项目全局样式入口文件(通常为 `src/styles/index.scss`),验证 `@use` 导入顺序符合分层约定:
244
+
245
+ ```
246
+ 预期顺序(从先到后):
247
+ 1. design-tokens — 令牌定义(最底层)
248
+ 2. theme — 主题变量
249
+ 3. transition — 过渡动画
250
+ 4. button — 按钮装饰类
251
+ 5. element-plus — EP 组件覆盖(通用层)
252
+ 6. sidebar — 侧栏专属覆盖(高优先级)
253
+ 7. dark — 暗色模式覆盖
254
+ 8. vant — Vant 组件覆盖
255
+ ```
256
+
257
+ **验证规则**:
258
+ 1. 实际 `@use` 顺序与预期顺序对比 — 顺序不一致 = 硬失败
259
+ 2. 设计落地执行时新增的 CSS 文件是否已注册到 `index.scss` — 未注册 = 硬失败
260
+ 3. 新增文件应插入到正确层级位置(通用覆盖在 `element-plus` 附近,专属覆盖在 `sidebar` 附近)— 位置错误 = 硬失败
261
+
262
+ ### 10.7.5.4 冲突修复
263
+
264
+ 检测到硬失败项时,按以下优先级修复:
265
+
266
+ 1. **消除冗余**:同一选择器只在唯一文件中定义,删除其他文件中的重复
267
+ 2. **合并定义**:将分散在多文件的同一选择器属性合并到一个文件中
268
+ 3. **调整导入顺序**:若覆盖关系需要保留,确保导入顺序正确且添加 override 注释
269
+ 4. **提升特异性**:若需覆盖 EP 默认样式,使用更高特异性选择器(如 `.sidebar-container .el-menu-item`)而非 `!important`
270
+
271
+ **修复后必须重新执行 10.7.5.1-10.7.5.3 确认零冲突。**
272
+
273
+ ### 10.7.5.5 冲突检测报告(输出格式)
274
+
275
+ ```markdown
276
+ ## CSS 冲突检测报告
277
+
278
+ ### 扫描范围
279
+ - 文件数:{N}
280
+ - 选择器总数:{total}
281
+ - 重复选择器:{duplicates} 个
282
+
283
+ ### 导入顺序验证
284
+ - index.scss @use 顺序:✅ 正确 / ❌ 错误({详情})
285
+ - 新增文件注册:✅ 已注册 / ❌ 未注册({文件名})
286
+
287
+ ### 重复选择器明细
288
+ | 选择器 | 类型 | 文件 | 胜出值 | 与设计一致 | 处理 |
289
+ |--------|------|------|--------|-----------|------|
290
+ | .el-menu-item.is-active | 有意覆盖 | element-plus.scss:573, sidebar.scss:104 | sidebar 值 | ✅ | 保留+加注释 |
291
+ | .el-table thead th | 无害重复 | element-plus.scss:342, :566 | — | — | 删除冗余 |
292
+ | .new-selector | 无意冲突 | a.scss:10, b.scss:20 | 不确定 | ❌ | 硬失败→已合并 |
293
+
294
+ ### 硬失败项
295
+ - {如有,列出并说明已修复情况}
296
+ ```
297
+
298
+ ---
299
+
171
300
  ## 10.8 逐属性 CSS 校验(设计源文件回读 — 硬失败闸门)
172
301
 
173
302
  > 10.2-10.7 是「区域级」验证(检查"顶栏高度对不对""筛选区有没有")。
303
+ > 10.7.5 是「结构级」验证——确保 CSS 无冲突、导入顺序正确、每个选择器有唯一生效值。
174
304
  > 本节是「属性级」验证——从设计 reference 源文件中逐行提取每个 CSS 属性,与项目代码逐条比对。
175
- > **这是消除精度偏差的核心闸门,10.1-10.7 验证通过后必须执行本节,不可跳过。**
305
+ > **这是消除精度偏差的核心闸门,10.1-10.7.5 验证通过后必须执行本节,不可跳过。**
176
306
 
177
307
  ### 10.8.1 动态发现设计源文件
178
308
 
@@ -0,0 +1,74 @@
1
+ ---
2
+ skill: handoff-governance
3
+ description: 设计落地管控 — 流程闸门(步骤不可跳过)+ JSON 规格闸门(当次设计可变)。配合 scripts/handoff-*.mjs 机器执行。
4
+ scope: project
5
+ tags: [handoff, governance, gate, process, spec-check]
6
+ ---
7
+
8
+ # 设计落地管控(handoff-governance)
9
+
10
+ > **根本原则**:project skill 只规定**固定流水线 + 验收方法**;具体「有没有下划线、有没有设置按钮」写在**当次 handoff JSON** 的 `structureRequirements` / `interactionSpecs` 里,**不写死**在 skill 条文中。
11
+
12
+ ## 三层架构
13
+
14
+ | 层 | 职责 | 会不会随设计变 |
15
+ |----|------|--------------|
16
+ | **设计 skill** | HTML + handoff JSON(含 structureRequirements / interactionSpecs) | ✅ 每次不同 |
17
+ | **工程 skill** | C/T/F/Co/S/P 固定流程 + F/P/Co 映射 + design-verify 方法 | ❌ 流程不变 |
18
+ | **机器闸门** | `handoff-step-check` + `handoff-spec-check` + verify-report | ❌ 逻辑不变,读当次 JSON |
19
+
20
+ ## 会话文件(项目内)
21
+
22
+ 设计落地**动手前**,AI 必须执行(在**业务项目根目录**):
23
+
24
+ ```bash
25
+ vue3-temp-skill handoff init --project <业务项目> --file .handoff/handoff.json
26
+ # 或源码开发:
27
+ node <skill>/scripts/handoff-session-init.mjs --project <业务项目> --file .handoff/handoff.json
28
+ ```
29
+
30
+ 完成后:
31
+
32
+ ```bash
33
+ vue3-temp-skill handoff check --project <业务项目> --require-session
34
+ ```
35
+
36
+ 业务项目内产出(`.handoff/` 目录在**业务仓库**,脚本在 **skill 包**):
37
+
38
+ ```
39
+ <业务项目>/.handoff/
40
+ handoff.json # 设计 JSON 副本
41
+ session.json # init 生成
42
+ verify-report.json # verify 完成后落盘
43
+ ```
44
+
45
+ ## 固定流水线
46
+
47
+ ```
48
+ 0. handoff init
49
+ 1. C → T → F → Co → S → P
50
+ 2. design-verify + verify-report.json
51
+ 3. handoff check --require-session
52
+ 4. lint + type-check
53
+ ```
54
+
55
+ ## 命令
56
+
57
+ | 命令 | 用途 |
58
+ |------|------|
59
+ | `vue3-temp-skill handoff init --project <dir> [--file .handoff/handoff.json]` | 初始化 session |
60
+ | `vue3-temp-skill handoff check --project <dir> [--require-session]` | 汇总闸门 |
61
+ | `node scripts/handoff-step-check.mjs --project <dir>` | 仅步骤检查 |
62
+ | `node scripts/handoff-spec-check.mjs --project <dir>` | 仅 JSON 规格检查 |
63
+
64
+ ## verify-report.json 最小形状
65
+
66
+ ```json
67
+ {
68
+ "generatedAt": "2026-08-27T...",
69
+ "overallPass": true,
70
+ "layers": { "framework": { "pass": 10, "total": 10 } },
71
+ "deviations": [],
72
+ "specSource": "session.handoff"
73
+ }
74
+ ```
package/scripts/cli.mjs CHANGED
@@ -71,6 +71,16 @@ const commands = {
71
71
  file: "scaffold-module.mjs",
72
72
  args: "pass",
73
73
  },
74
+ "handoff-init": {
75
+ desc: "设计落地:初始化业务项目 .handoff/session.json",
76
+ file: "handoff-session-init.mjs",
77
+ args: "pass",
78
+ },
79
+ "handoff-check": {
80
+ desc: "设计落地:管控闸门(步骤 + JSON 规格)",
81
+ file: "handoff-governance.mjs",
82
+ args: "pass",
83
+ },
74
84
  };
75
85
 
76
86
  function run(cmd, extraArgs = []) {
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Handoff governance gate — step check + spec check + optional verify report.
4
+ *
5
+ * Usage:
6
+ * node scripts/handoff-governance.mjs --project <path>
7
+ * node scripts/handoff-governance.mjs --project <path> --require-session
8
+ */
9
+
10
+ import { execFileSync } from "child_process";
11
+ import { existsSync } from "fs";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { parseArgs, readSession, REPORT_FILE } from "./handoff-lib.mjs";
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const NODE = process.execPath;
18
+ const args = parseArgs(process.argv);
19
+
20
+ function runScript(name) {
21
+ const extra = args.skipIfNoSession ? [] : ["--require-session"];
22
+ return execFileSync(NODE, [path.join(__dirname, name), "--project", args.project, ...extra], {
23
+ encoding: "utf-8",
24
+ stdio: "pipe",
25
+ });
26
+ }
27
+
28
+ console.log("Handoff 管控闸门\n" + "=".repeat(40));
29
+
30
+ const session = readSession(args.sessionPath);
31
+ if (!session && !args.skipIfNoSession) {
32
+ console.error("✗ 无 handoff session,请先 handoff-session-init");
33
+ process.exit(1);
34
+ }
35
+
36
+ if (!session) {
37
+ console.log("无活跃 handoff session — 跳过管控(非设计落地任务)\n");
38
+ process.exit(0);
39
+ }
40
+
41
+ const failures = [];
42
+
43
+ try {
44
+ console.log(runScript("handoff-step-check.mjs"));
45
+ } catch (e) {
46
+ failures.push("step-check");
47
+ if (e.stdout) console.error(e.stdout);
48
+ if (e.stderr) console.error(e.stderr);
49
+ }
50
+
51
+ try {
52
+ console.log(runScript("handoff-spec-check.mjs"));
53
+ } catch (e) {
54
+ failures.push("spec-check");
55
+ if (e.stdout) console.error(e.stdout);
56
+ if (e.stderr) console.error(e.stderr);
57
+ }
58
+
59
+ const reportPath = path.join(args.project, ".handoff", REPORT_FILE);
60
+ if (!existsSync(reportPath)) {
61
+ console.warn(
62
+ "\n⚠ 警告: 未找到 .handoff/verify-report.json(AI 须按 design-verify.md 输出并落盘)"
63
+ );
64
+ if (args.strict) failures.push("verify-report");
65
+ } else {
66
+ console.log("✓ 已找到 verify-report.json");
67
+ }
68
+
69
+ console.log("\n" + "=".repeat(40));
70
+ if (failures.length > 0) {
71
+ console.error(`✗ Handoff 管控失败: ${failures.join(", ")}`);
72
+ console.error("禁止声称设计落地完成,请补全步骤并重新执行 handoff check\n");
73
+ process.exit(1);
74
+ }
75
+
76
+ console.log("✓ Handoff 管控全部通过\n");
77
+ process.exit(0);
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared utilities for handoff governance scripts.
4
+ */
5
+
6
+ import { execSync } from "child_process";
7
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
8
+ import path from "path";
9
+
10
+ export const SESSION_FILE = "session.json";
11
+ export const REPORT_FILE = "verify-report.json";
12
+
13
+ /** C/T/F/Co/S step → required project-relative paths (glob suffix * = directory) */
14
+ export const STEP_MANIFEST = {
15
+ C: ["public/platform-config.json"],
16
+ T: ["src/styles/design-tokens.scss"],
17
+ F: [
18
+ "src/layout/components/lay-navbar/index.vue",
19
+ "src/layout/components/lay-sidebar/NavVertical.vue",
20
+ "src/layout/components/lay-tag/index.vue",
21
+ "src/styles/sidebar.scss",
22
+ ],
23
+ Co: [
24
+ "src/components/ReTableBar/src/bar.vue",
25
+ "src/components/ReTableOperate/src/index.vue",
26
+ "src/components/RePagination",
27
+ ],
28
+ S: ["src/styles/element-plus.scss"],
29
+ };
30
+
31
+ export function parseArgs(argv) {
32
+ const args = { project: process.cwd(), session: null, strict: false, skipIfNoSession: true };
33
+ for (let i = 2; i < argv.length; i++) {
34
+ const a = argv[i];
35
+ if (a === "--project" && argv[i + 1]) args.project = path.resolve(argv[++i]);
36
+ else if (a === "--session" && argv[i + 1]) args.session = path.resolve(argv[++i]);
37
+ else if (a === "--strict") args.strict = true;
38
+ else if (a === "--require-session") args.skipIfNoSession = false;
39
+ }
40
+ args.sessionPath =
41
+ args.session || path.join(args.project, ".handoff", SESSION_FILE);
42
+ return args;
43
+ }
44
+
45
+ export function readSession(sessionPath) {
46
+ if (!existsSync(sessionPath)) return null;
47
+ try {
48
+ return JSON.parse(readFileSync(sessionPath, "utf-8"));
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ export function readProjectFile(projectRoot, relPath) {
55
+ const full = path.join(projectRoot, relPath);
56
+ if (!existsSync(full)) return null;
57
+ return readFileSync(full, "utf-8");
58
+ }
59
+
60
+ export function fileExists(projectRoot, relPath) {
61
+ return existsSync(path.join(projectRoot, relPath));
62
+ }
63
+
64
+ export function gitBaseline(projectRoot) {
65
+ try {
66
+ return execSync("git rev-parse HEAD", {
67
+ cwd: projectRoot,
68
+ encoding: "utf-8",
69
+ stdio: ["pipe", "pipe", "pipe"],
70
+ }).trim();
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ export function gitChangedFiles(projectRoot, baseline) {
77
+ if (!baseline) return null;
78
+ try {
79
+ const out = execSync(`git diff --name-only ${baseline}`, {
80
+ cwd: projectRoot,
81
+ encoding: "utf-8",
82
+ stdio: ["pipe", "pipe", "pipe"],
83
+ });
84
+ return out
85
+ .split(/\r?\n/)
86
+ .map(s => s.trim().replace(/\\/g, "/"))
87
+ .filter(Boolean);
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ export function expandStepPaths(relPaths) {
94
+ return relPaths.flatMap(p => (p.endsWith("*") ? [p.slice(0, -1)] : [p]));
95
+ }
96
+
97
+ export function pathMatchesChanged(changedSet, relPath) {
98
+ const norm = relPath.replace(/\\/g, "/");
99
+ if (changedSet.has(norm)) return true;
100
+ // directory: any file under prefix
101
+ for (const f of changedSet) {
102
+ if (f.startsWith(norm.endsWith("/") ? norm : `${norm}/`)) return true;
103
+ }
104
+ return false;
105
+ }
106
+
107
+ export function collectFilesUnder(dir) {
108
+ if (!existsSync(dir)) return [];
109
+ const out = [];
110
+ for (const name of readdirSync(dir)) {
111
+ const full = path.join(dir, name);
112
+ if (statSync(full).isDirectory()) out.push(...collectFilesUnder(full));
113
+ else out.push(full);
114
+ }
115
+ return out;
116
+ }
117
+
118
+ export function getHandoff(session) {
119
+ return session?.handoff || session;
120
+ }
121
+
122
+ export function headerElements(handoff) {
123
+ return handoff?.structureRequirements?.frameworkShell?.header ?? [];
124
+ }
125
+
126
+ export function interaction(handoff) {
127
+ return handoff?.interactionSpecs ?? {};
128
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Initialize handoff session — write .handoff/session.json with baseline git commit.
4
+ *
5
+ * Usage:
6
+ * node scripts/handoff-session-init.mjs --project <path> --file handoff.json
7
+ * cat handoff.json | node scripts/handoff-session-init.mjs --project <path>
8
+ */
9
+
10
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
11
+ import path from "path";
12
+ import { gitBaseline, parseArgs, SESSION_FILE } from "./handoff-lib.mjs";
13
+
14
+ const args = parseArgs(process.argv);
15
+ let handoffJson = null;
16
+
17
+ const fileIdx = process.argv.indexOf("--file");
18
+ if (fileIdx !== -1 && process.argv[fileIdx + 1]) {
19
+ handoffJson = JSON.parse(readFileSync(path.resolve(process.argv[fileIdx + 1]), "utf-8"));
20
+ } else if (!process.stdin.isTTY) {
21
+ const chunks = [];
22
+ for await (const chunk of process.stdin) chunks.push(chunk);
23
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
24
+ if (raw) handoffJson = JSON.parse(raw);
25
+ }
26
+
27
+ if (!handoffJson) {
28
+ console.error("用法: handoff-session-init.mjs --project <path> --file <handoff.json>");
29
+ process.exit(1);
30
+ }
31
+
32
+ if (handoffJson.$schema !== "design-handoff/v1") {
33
+ console.error("handoff-session-init: 需要 $schema: design-handoff/v1");
34
+ process.exit(1);
35
+ }
36
+
37
+ if (!handoffJson.structureRequirements || !handoffJson.interactionSpecs) {
38
+ console.error(
39
+ "handoff-session-init: handoff 缺少 structureRequirements 或 interactionSpecs,请使用新版设计 skill 产出"
40
+ );
41
+ process.exit(1);
42
+ }
43
+
44
+ const handoffDir = path.join(args.project, ".handoff");
45
+ mkdirSync(handoffDir, { recursive: true });
46
+
47
+ const baseline = gitBaseline(args.project);
48
+ const session = {
49
+ manifestVersion: 1,
50
+ startedAt: new Date().toISOString(),
51
+ baseline,
52
+ handoff: handoffJson,
53
+ steps: {
54
+ C: { status: "pending" },
55
+ T: { status: "pending" },
56
+ F: { status: "pending" },
57
+ Co: { status: "pending" },
58
+ S: { status: "pending" },
59
+ P: { status: "pending" },
60
+ },
61
+ };
62
+
63
+ const outPath = path.join(handoffDir, SESSION_FILE);
64
+ writeFileSync(outPath, JSON.stringify(session, null, 2), "utf-8");
65
+
66
+ console.log(`✓ 已写入 ${outPath}`);
67
+ console.log(` baseline: ${baseline || "(非 git 仓库)"}`);
68
+ console.log(` theme: ${handoffJson.meta?.themeName || handoffJson.meta?.themeId || "?"}`);
69
+ console.log(` header: ${(handoffJson.structureRequirements?.frameworkShell?.header || []).join(", ")}`);
70
+ console.log(` tabsActive.underline: ${handoffJson.interactionSpecs?.tabsActive?.underline}`);
71
+ console.log(` tableOperate.showIcon: ${handoffJson.interactionSpecs?.tableOperate?.showIcon}`);
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Handoff spec gate — JSON-driven checks (structureRequirements + interactionSpecs).
4
+ *
5
+ * Usage:
6
+ * node scripts/handoff-spec-check.mjs --project <path> [--require-session]
7
+ */
8
+
9
+ import path from "path";
10
+ import {
11
+ fileExists,
12
+ getHandoff,
13
+ headerElements,
14
+ interaction,
15
+ parseArgs,
16
+ readProjectFile,
17
+ readSession,
18
+ } from "./handoff-lib.mjs";
19
+
20
+ const args = parseArgs(process.argv);
21
+ const session = readSession(args.sessionPath);
22
+
23
+ if (!session) {
24
+ if (args.skipIfNoSession) {
25
+ console.log("handoff-spec-check: 无 .handoff/session.json,跳过规格检查");
26
+ process.exit(0);
27
+ }
28
+ console.error("handoff-spec-check: 缺少 .handoff/session.json");
29
+ process.exit(1);
30
+ }
31
+
32
+ const handoff = getHandoff(session);
33
+ if (!handoff?.structureRequirements || !handoff?.interactionSpecs) {
34
+ console.error(
35
+ "handoff-spec-check: handoff JSON 缺少 structureRequirements 或 interactionSpecs(设计 skill 必产出段)"
36
+ );
37
+ process.exit(1);
38
+ }
39
+
40
+ const project = args.project;
41
+ const specs = interaction(handoff);
42
+ const header = headerElements(handoff);
43
+ const failures = [];
44
+ const passes = [];
45
+
46
+ function fail(id, message) {
47
+ failures.push({ id, message });
48
+ }
49
+
50
+ function pass(id, message) {
51
+ passes.push({ id, message });
52
+ }
53
+
54
+ // --- structureRequirements: header elements ---
55
+ const navbarPath = "src/layout/components/lay-navbar/index.vue";
56
+ const navbar = readProjectFile(project, navbarPath);
57
+
58
+ if (!navbar) {
59
+ fail("navbar-exists", `缺少 ${navbarPath}`);
60
+ } else {
61
+ const headerChecks = [
62
+ { id: "settings", patterns: [/lay-setting|LaySetting|set-icon|settings/i], inHeader: header.includes("settings") },
63
+ { id: "breadcrumb", patterns: [/breadcrumb|LayBreadcrumb/i], inHeader: header.includes("breadcrumb") },
64
+ { id: "collapse", patterns: [/collapse|fold|unfold|iconify.*menu/i], inHeader: header.includes("collapse") },
65
+ { id: "langSwitch", patterns: [/ReLanguageSwitch|language-switch|lang-switch/i], inHeader: header.includes("langSwitch") },
66
+ { id: "fullscreen", patterns: [/fullscreen|FullScreen|screenfull/i], inHeader: header.includes("fullscreen") },
67
+ ];
68
+
69
+ for (const { id, patterns, inHeader } of headerChecks) {
70
+ const found = patterns.some(p => p.test(navbar));
71
+ if (inHeader && !found) fail(`header-missing-${id}`, `JSON header 含 "${id}" 但 ${navbarPath} 未找到对应实现`);
72
+ else if (!inHeader && found) fail(`header-forbidden-${id}`, `JSON header 不含 "${id}" 但 ${navbarPath} 仍存在该元素`);
73
+ else pass(`header-${id}`, `顶栏元素 ${id}: 与 JSON 一致`);
74
+ }
75
+ }
76
+
77
+ // --- interactionSpecs: languageSwitch ---
78
+ if (specs.languageSwitch?.mode === "inline-text") {
79
+ const langFile =
80
+ readProjectFile(project, "src/components/ReLanguageSwitch/src/index.vue") ||
81
+ readProjectFile(project, "src/components/ReLanguageSwitch/index.vue");
82
+ if (langFile && /el-dropdown|ElDropdown/.test(langFile) && !/inline|中\s*[||/]\s*EN/i.test(langFile)) {
83
+ fail("lang-inline", "interactionSpecs.languageSwitch.mode=inline-text 但 ReLanguageSwitch 仍为下拉框");
84
+ } else if (langFile) {
85
+ pass("lang-inline", "语言切换为内联文字模式");
86
+ }
87
+ }
88
+
89
+ // --- interactionSpecs: tabsActive.underline ---
90
+ if (specs.tabsActive?.underline === false) {
91
+ const tagVue = readProjectFile(project, "src/layout/components/lay-tag/index.vue") || "";
92
+ const epScss = readProjectFile(project, "src/styles/element-plus.scss") || "";
93
+ const combined = tagVue + epScss;
94
+ if (/schedule-active|active-bar|border-bottom.*primary|underline/i.test(combined) &&
95
+ !/underline:\s*none|display:\s*none.*active-bar|smart.*无下划线/i.test(combined)) {
96
+ // soft check — only fail on obvious underline patterns for smart/line
97
+ if (/\.schedule-active|active-bar|tabs.*border-bottom/i.test(combined)) {
98
+ fail(
99
+ "tabs-no-underline",
100
+ "interactionSpecs.tabsActive.underline=false 但 lay-tag/element-plus 仍含页签激活下划线样式"
101
+ );
102
+ }
103
+ } else {
104
+ pass("tabs-no-underline", "页签激活无下划线(符合 JSON)");
105
+ }
106
+ }
107
+
108
+ // --- interactionSpecs: menuActive ---
109
+ const menu = specs.menuActive;
110
+ if (menu) {
111
+ const sidebar = readProjectFile(project, "src/styles/sidebar.scss") || "";
112
+ if (menu.fullWidth === false) {
113
+ if (/width:\s*100%|margin:\s*0[^;]*;[^}]*active|通栏/i.test(sidebar) &&
114
+ !/margin:\s*0\s+8px|margin.*8px/i.test(sidebar)) {
115
+ fail("menu-not-fullwidth", "interactionSpecs.menuActive.fullWidth=false 但 sidebar 激活态疑似通栏");
116
+ } else {
117
+ pass("menu-not-fullwidth", "侧栏激活非通栏");
118
+ }
119
+ }
120
+ if (menu.radius === 6 && sidebar && !/border-radius:\s*6px|radius:\s*6/i.test(sidebar)) {
121
+ fail("menu-radius", "interactionSpecs.menuActive.radius=6 但 sidebar.scss 未找到 6px 圆角");
122
+ } else if (menu.radius) {
123
+ pass("menu-radius", "侧栏菜单圆角符合 JSON");
124
+ }
125
+ }
126
+
127
+ // --- interactionSpecs: tableOperate ---
128
+ const tableOp = specs.tableOperate;
129
+ if (tableOp?.showIcon === false) {
130
+ const operate =
131
+ readProjectFile(project, "src/components/ReTableOperate/src/index.vue") ||
132
+ readProjectFile(project, "src/components/ReTableOperate/index.vue");
133
+ if (operate && /:icon=|btn\.icon|icon:\s*/i.test(operate)) {
134
+ fail("operate-no-icon", "interactionSpecs.tableOperate.showIcon=false 但 ReTableOperate 仍绑定 icon");
135
+ } else if (operate) {
136
+ pass("operate-no-icon", "操作列无图标");
137
+ } else {
138
+ fail("operate-exists", "缺少 ReTableOperate 组件文件");
139
+ }
140
+ }
141
+
142
+ if (tableOp?.showDivider === true) {
143
+ const operate =
144
+ readProjectFile(project, "src/components/ReTableOperate/src/index.vue") ||
145
+ readProjectFile(project, "src/components/ReTableOperate/index.vue");
146
+ if (operate && !/divider|分隔|vts-table-action-divider|action-divider/i.test(operate)) {
147
+ fail("operate-divider", "interactionSpecs.tableOperate.showDivider=true 但 ReTableOperate 无分隔线");
148
+ } else if (operate) {
149
+ pass("operate-divider", "操作列有分隔线");
150
+ }
151
+ }
152
+
153
+ // --- interactionSpecs: tableToolbar ---
154
+ const toolbar = specs.tableToolbar;
155
+ if (toolbar?.showTextLabel === true) {
156
+ const bar =
157
+ readProjectFile(project, "src/components/ReTableBar/src/bar.vue") ||
158
+ readProjectFile(project, "src/components/ReTableBar/index.vue");
159
+ if (bar) {
160
+ const hasText = /刷新|密度|列设置|columns|density|refresh/i.test(bar);
161
+ if (!hasText) {
162
+ fail("toolbar-text-label", "interactionSpecs.tableToolbar.showTextLabel=true 但 ReTableBar 工具按钮无文字标签");
163
+ } else {
164
+ pass("toolbar-text-label", "工具栏按钮含文字标签");
165
+ }
166
+ }
167
+ }
168
+
169
+ // --- elementHideList ---
170
+ const hideList = handoff.structureRequirements?.elementHideList ?? [];
171
+ if (hideList.includes("navbar-settings") && navbar && /lay-setting|LaySetting|set-icon/i.test(navbar)) {
172
+ fail("hide-settings", "elementHideList 含 navbar-settings 但顶栏仍有设置按钮");
173
+ }
174
+
175
+ // --- Co files must exist ---
176
+ for (const rel of [
177
+ "src/components/ReTableBar/src/bar.vue",
178
+ "src/components/ReTableOperate/src/index.vue",
179
+ ]) {
180
+ if (!fileExists(project, rel)) fail("co-file", `共享组件缺失: ${rel}`);
181
+ }
182
+
183
+ console.log("\n=== Handoff 规格检查(JSON 驱动)===");
184
+ console.log(` 通过: ${passes.length} 失败: ${failures.length}`);
185
+
186
+ if (failures.length > 0) {
187
+ console.error("\n✗ 规格检查失败:");
188
+ for (const f of failures) {
189
+ console.error(` - [${f.id}] ${f.message}`);
190
+ }
191
+ process.exit(1);
192
+ }
193
+
194
+ console.log("\n✓ 规格检查全部通过\n");
195
+ process.exit(0);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Handoff step gate — verifies C/T/F/Co/S files were touched since session baseline.
4
+ *
5
+ * Usage:
6
+ * node scripts/handoff-step-check.mjs --project <path> [--require-session]
7
+ */
8
+
9
+ import path from "path";
10
+ import {
11
+ STEP_MANIFEST,
12
+ expandStepPaths,
13
+ gitBaseline,
14
+ gitChangedFiles,
15
+ parseArgs,
16
+ pathMatchesChanged,
17
+ readSession,
18
+ } from "./handoff-lib.mjs";
19
+
20
+ const args = parseArgs(process.argv);
21
+ const session = readSession(args.sessionPath);
22
+
23
+ if (!session) {
24
+ if (args.skipIfNoSession) {
25
+ console.log("handoff-step-check: 无 .handoff/session.json,跳过步骤检查");
26
+ process.exit(0);
27
+ }
28
+ console.error("handoff-step-check: 缺少 .handoff/session.json,请先执行 handoff-session-init");
29
+ process.exit(1);
30
+ }
31
+
32
+ const baseline = session.baseline || gitBaseline(args.project);
33
+ const changedList = gitChangedFiles(args.project, baseline);
34
+
35
+ if (!changedList) {
36
+ console.error(
37
+ "handoff-step-check: 无法获取 git diff(非 git 仓库或无 baseline)。请先 handoff-session-init 或提交 baseline。"
38
+ );
39
+ process.exit(1);
40
+ }
41
+
42
+ const changedSet = new Set(changedList);
43
+ const failures = [];
44
+ const results = {};
45
+
46
+ for (const [step, files] of Object.entries(STEP_MANIFEST)) {
47
+ const expanded = expandStepPaths(files);
48
+ const missing = [];
49
+ const touched = [];
50
+
51
+ for (const rel of expanded) {
52
+ const hit = pathMatchesChanged(changedSet, rel);
53
+ if (hit) touched.push(rel);
54
+ else missing.push(rel);
55
+ }
56
+
57
+ results[step] = { touched, missing, ok: missing.length === 0 };
58
+ if (missing.length > 0) {
59
+ failures.push({
60
+ step,
61
+ message: `步骤 ${step} 未修改: ${missing.join(", ")}`,
62
+ missing,
63
+ });
64
+ }
65
+ }
66
+
67
+ console.log("\n=== Handoff 步骤检查 ===");
68
+ for (const [step, r] of Object.entries(results)) {
69
+ console.log(` ${step}: ${r.ok ? "✓" : "✗"} (${r.touched.length}/${expandStepPaths(STEP_MANIFEST[step]).length} 文件已变更)`);
70
+ }
71
+
72
+ if (failures.length > 0) {
73
+ console.error("\n✗ 步骤检查失败(流程硬失败 — 可能跳过了 Co/F 等步骤):");
74
+ for (const f of failures) {
75
+ console.error(` - [${f.step}] ${f.message}`);
76
+ }
77
+ process.exit(1);
78
+ }
79
+
80
+ console.log("\n✓ 全部步骤(C/T/F/Co/S)对应文件均已变更\n");
81
+ process.exit(0);