@agile-team/wl-skills-kit 2.11.2 → 2.11.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.11.4] - 2026-06-21
4
+
5
+ ### Added
6
+
7
+ - **R14 类型检查自动接线(pre-push hook)**:`init`/`update` 自动创建 `.husky/pre-push`,推送前跑 `validate --typecheck`(R14 vue-tsc 类型检查)。补齐"pre-commit 跑 R1~R13、pre-push 跑 R14、CI 跑 R1~R14"三层确定性兜底,R14 不再是 opt-in 空规则
8
+ - **validate 端到端集成测试(tests/validate.test.js)**:通过真实 CLI 二进制跑完整 validate 流程,覆盖正则级(agGrid/cid/defineColumns)+ AST 级(R3/R13)+ 合规页 + 豁免配置。补齐 runValidate 主体的零覆盖缺口("自吃狗粮")
9
+ - **convention-audit 豁免项复核**:审计步骤新增"读取 .wl-skills-validate.json 列出所有豁免条目",供人工逐条确认豁免是否仍需保留(豁免审计闭环)
10
+
11
+ ### Changed
12
+
13
+ - **kit 自身代码质量门禁修复**:修复 13 个 eslint error(8 处未用变量 + 4 处嵌套过深 + 1 处死代码),`lint-staged` 匹配范围从无效的 `src/**` 改为 `bin/lib/scripts/**`,`pnpm lint` 纳入 `verify`/`prepublishOnly`。kit 自身代码不再绕开规范
14
+ - **runAstRules 重构降复杂度**:抽出 `checkR8FileSeparation`/`checkR13Complexity` 独立 helper,降低主函数圈复杂度(84→更低),消除 R9 死代码(hasInterfaceTable/hasEntityDef)
15
+
16
+ ### Fixed
17
+
18
+ - **jenkins-pipeline.md 滞后**:3 处 `.github/` → `.wl-skills/`(v2.11.0 目录迁移遗留未同步);Typecheck 独立 stage 合并进 `validate --strict --typecheck`(R14 不再割裂)
19
+ - **R14 配置错误误报**:`vue-tsc` 退出码非 0 但无标准 TS error 行时,区分 tsconfig 配置问题(warn,非类型错误)与未知失败(error),避免误导用户改类型
20
+ - **lint-skills printFixSuggestions 残留**:`.github/standards/` 引用修正为 `.wl-skills/`
21
+
22
+ ## [2.11.3] - 2026-06-21
23
+
24
+ ### Added
25
+
26
+ - **validate 项目级豁免配置(零功能影响)**:业务项目根可放 `.wl-skills-validate.json`,对表单设计器/行内编辑明细表等 BaseTable 受限场景批量豁免 `R3`/`R10`,kit 不主动创建、不存在时行为完全不变。新增 `lib/ast-rules.js` 的 `loadExemptions`,`runAstRules` 内部加载(CLI/MCP 自动一致);与单文件注释豁免(`wl-skills:ignore R3`)互补。配置 schema 详见 `.wl-skills/docs/validate-exempt.md`
27
+ - **standards/12 豁免规则重写**:明确"标准列表强制 BaseTable+AGGrid / BaseTable 可胜任仍优先 BaseTable / BaseTable 受限特殊场景可降级 el-table"三层优先级,两层豁免机制(单文件注释 + 项目级配置),不再一棍子拍死
28
+
29
+ ### Fixed
30
+
31
+ - **README/脚本系统性滞后修正**:v2.11.0 目录从 `.github/` 迁移到 `.wl-skills/` 后,README 大量路径残留未同步;`sync-version.js`/`verify-version.js` 仍硬编码"13 条"(规范已升 14 条),导致 verify-version 的 headers 校验**静默失效**、`npm version` 触发 sync-version 会把"14 条"**回写成 13 条**。全部修正为 14 条 + `.wl-skills/` 路径,校验链路恢复
32
+ - `_compat/headers/{cursor-mdc,trae,kiro}.txt` 源文件注释 `.github/copilot-instructions.md` → `.wl-skills/copilot-instructions-full.md`
33
+
3
34
  ## [2.11.2] - 2026-06-21
4
35
 
5
36
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @agile-team/wl-skills-kit
2
2
 
3
- **AI Skill 模板包 v2.11.2** — 一键将 14 条规范、11 个 AI Skill、17 个 MCP Tool、编辑器 MCP 配置、文档导入 Vue 3 项目。
3
+ **AI Skill 模板包 v2.11.4** — 一键将 14 条规范、11 个 AI Skill、17 个 MCP Tool、编辑器 MCP 配置、文档导入 Vue 3 项目。
4
4
 
5
5
  让 AI 编辑器(Copilot / Cursor / Windsurf / Claude Code / Cline / Kiro / Trae / Qoder / 通用 Agents)**真正理解项目规范**,从原型/详设到完整页面代码全流程自动化。
6
6
 
@@ -25,7 +25,13 @@ pnpm standards:init # 本包维护/业务项目均可
25
25
 
26
26
  ## 版本亮点
27
27
 
28
- **v2.11.1**:精准卡控闭环 —— "约定"接线到确定性执行器,生成即精确。
28
+ **v2.11.3**:确定性闭环再加固 —— 编码最佳实践从"文档约定"接线到执行器,特殊场景豁免可配置。
29
+
30
+ - **新增 R13 圈复杂度执行器(standard 04)**:对每个函数计 McCabe 圈复杂度(与 ESLint `complexity` 一致),`>10` 报 error 阻断;补"降复杂度手法"示例
31
+ - **新增 R14 类型错误零容忍(standard 09 升级为 🔴必遵)**:委托 `vue-tsc/tsc --noEmit` 解析 TS error,`validate --typecheck` / MCP `typecheck:true` 触发,无 checker 优雅降级;ESLint 管风格、R14 管正确性,职责分离
32
+ - **新增 validate 项目级豁免配置**:`.wl-skills-validate.json` 对表单设计器/行内编辑明细表等 BaseTable 受限场景批量豁免 R3/R10,零功能影响(kit 不主动创建);与单文件注释豁免互补
33
+
34
+ **v2.11.1**:精准卡控闭环 —— 把"约定"接线到确定性执行器,生成即精准。
29
35
 
30
36
  - **新增 page-spec 落盘 + spec-align 确定性比对(S1~S5)**:`page-codegen` 生成页面时同步写出 `page-spec.json`(原型约定真值),`validate` 用 AST 解析 `data.ts` 的 `queryDef/columnsDef/toolbarDef` 与之逐项比对——查询字段顺序、表格列顺序、工具栏按钮顺序与颜色、操作列按钮集合、label 文字保真。过去只靠 AI 自觉的 6 条"精准实现"约定,现在变成可阻断的硬卡控
31
37
  - **新增 `wl-skills fix` 确定性机械修复**:对幂等、零语义判断的偏差(BaseTable 补 `render-type`、`::v-deep`→`:deep()`、行尾空白、文件末尾换行)做确定性自动修复,AI 只处理需语义判断的部分;`--dry-run` 预览
@@ -115,7 +121,7 @@ docs/business/0X-xx/{index,requirement,dictionary,field}.md
115
121
  api.md(页面级前后端契约)
116
122
 
117
123
  ▼ [Skill: page-codegen]
118
- data.ts + index.vue + index.scss(13 条 standards 自动满足)
124
+ data.ts + index.vue + index.scss(14 条 standards 自动满足)
119
125
 
120
126
  ▼ [Skill: convention-audit] ← 也可对存量代码单独触发
121
127
  reports/AUDIT_AI_*.md + AUDIT_HUMAN_*.md
@@ -147,9 +153,9 @@ wl-skills-kit/ ← 你正看的这个仓库
147
153
  │ └── wl-skills.js CLI 实现(init / update / clean / check / diff / validate / validate-page / fix / doctor-ui / export / mock-clean)
148
154
 
149
155
  ├── files/ ★★★ 真正会被打包并复制到业务项目的内容 ★★★
150
- │ ├── .github/
151
- │ │ ├── copilot-instructions.mdAI 主入口(编辑这里,不要编辑业务项目里的副本)
152
- │ │ ├── standards/ 13 条规范
156
+ │ ├── .wl-skills/ 统一隔离目录(所有 Skill/规范/指南/报告/模板)
157
+ │ │ ├── copilot-instructions-full.md AI 主入口完整指令(业务项目根另有薄壳)
158
+ │ │ ├── standards/ 14 条规范
153
159
  │ │ ├── skills/ Skill 目录(含 _compat/ 多编辑器适配源)
154
160
  │ │ ├── guides/ 人读指南
155
161
  │ │ └── reports/ 领域基线模板(菜单/字典/权限)
@@ -182,14 +188,14 @@ wl-skills-kit/ ← 你正看的这个仓库
182
188
  ```
183
189
  你的业务项目/
184
190
 
185
- ├── .github/ ← 来自本包 files/.github/
186
- │ ├── copilot-instructions.md Copilot 主入口(精简 ~340 行)
187
- │ ├── standards/ 13 条模块化规范 + index.md 门控
191
+ ├── .wl-skills/ ← 来自本包 files/.wl-skills/(统一隔离)
192
+ │ ├── copilot-instructions-full.md Copilot 完整指令
193
+ │ ├── standards/ 14 条模块化规范 + index.md 门控
188
194
  │ │ ├── 01-toolchain.md
189
195
  │ │ ├── 02-code-structure.md
190
- │ │ ├── ... (共 13 条)
191
- │ │ └── 13-platform-components.md
192
- │ ├── skills/ 10 个启用 Skill(全部激活)
196
+ │ │ ├── ... (共 14 条)
197
+ │ │ └── 14-layout-containers.md
198
+ │ ├── skills/ 11 个启用 Skill(全部激活)
193
199
  │ │ ├── _registry.md ★ 触发词 → SKILL 路径单一数据源
194
200
  │ │ ├── _compat/ 多 AI 编辑器适配(配置 + headers)
195
201
  │ │ ├── core/ 核心通用 Skill
@@ -208,6 +214,7 @@ wl-skills-kit/ ← 你正看的这个仓库
208
214
  │ │ │ └── code-fix/ { SKILL.md } 已启用
209
215
  │ │ └── domain/ 领域专属(按需创建)
210
216
  │ ├── guides/ 人读指南(usage.md / architecture.md)
217
+ │ ├── docs/ 组件 API 文档 + validate 豁免配置说明
211
218
  │ └── reports/ AI 生成报告(追加不覆盖)
212
219
  │ ├── SYS_MENU_INFO.md 线上菜单基线
213
220
  │ ├── SYS_DICT_INFO.md 线上字典基线
@@ -225,12 +232,13 @@ wl-skills-kit/ ← 你正看的这个仓库
225
232
  ├── .trae/rules/conventions.md Trae(含 alwaysApply frontmatter)
226
233
  ├── .qoder/rules/conventions.md Qoder
227
234
 
235
+ ├── .wl-skills-validate.json ← 可选:validate 项目级豁免配置(kit 不创建)
228
236
  ├── mock/ ← 来自本包 files/mock/(init 自动写入)
229
237
  │ ├── _utils.ts 共享工具(pageResult / ok / paginate / nowStr / pick)
230
238
  │ └── [业务域]/[模块].ts 按域分目录,page-codegen 自动生成
231
239
 
232
- ├── docs/ 12 个组件 API 文档 + mock-architecture.md
233
- ├── demo/ 13 个领域样例
240
+ ├── docs/ 组件 API 文档 + mock-architecture.md
241
+ ├── demo/ 领域样例
234
242
  └── src/
235
243
  ├── components/ 全局/局部/远程组件
236
244
  └── types/ 类型桶文件
@@ -238,7 +246,7 @@ wl-skills-kit/ ← 你正看的这个仓库
238
246
 
239
247
  > **业务项目方准则**:
240
248
  >
241
- > - 主入口是 `.github/copilot-instructions.md`(Copilot 用),**其他根配置文件是它的拷贝 + 各自特化 frontmatter**
249
+ > - 主入口是 `.wl-skills/copilot-instructions-full.md`(Copilot 用),业务项目根的薄壳文件指向它;**其他根配置文件是它的拷贝 + 各自特化 frontmatter**
242
250
  > - 修改规范 → **不要**改业务项目里的副本,**升级 wl-skills-kit 包 + `update`** 才不会被覆盖
243
251
  > - reports/ 里的内容是团队累积数据,`update` 不会覆盖,可放心 commit
244
252
 
@@ -262,11 +270,19 @@ pnpm dlx @agile-team/wl-skills-kit check
262
270
  pnpm dlx @agile-team/wl-skills-kit diff
263
271
 
264
272
  # 静态检查 src/views 页面文件完整性 + AGGrid/cid/skills-ui/mock
273
+ # 内含 AST 语义级检测 R1~R14(正则覆盖不到的语义约束)
274
+ # R13 圈复杂度 / R14 类型错误需 --typecheck 开启
265
275
  pnpm dlx @agile-team/wl-skills-kit validate
266
276
 
277
+ # 含类型检查 R14(vue-tsc/tsc --noEmit,CI / 发版前用)
278
+ pnpm dlx @agile-team/wl-skills-kit validate --typecheck --strict
279
+
267
280
  # 单页/指定目录校验
268
281
  pnpm dlx @agile-team/wl-skills-kit validate-page src/views/mdata/model/mdata-model-config
269
282
 
283
+ # 特殊场景(表单设计器/行内编辑明细表等 BaseTable 受限)批量豁免 R3/R10:
284
+ # 在项目根创建 .wl-skills-validate.json(详见 .wl-skills/docs/validate-exempt.md)
285
+
270
286
  # spec-align:页面目录存在 page-spec.json 时,确定性比对"约定 vs 代码"
271
287
  # (查询字段/表格列顺序、工具栏按钮顺序与颜色、操作列严格对应、label 保真)
272
288
  # 已内置于 validate,无需额外参数
@@ -340,7 +356,7 @@ pnpm dlx @agile-team/wl-skills-kit update
340
356
  2. **迁移清理** — 检测并移除旧版遗留文件(如 `skills/prototype-scan/`、`docs/menu-sync-design.md` 等),避免新旧路径并存产生歧义
341
357
  3. **保护累积数据** — `reports/*.md` 已存在则跳过,团队累积的菜单/字典数据不丢失
342
358
 
343
- > **注意**:如果项目在旧的 `.github/skills/menu-sync/env/env.local.json` 中有自定义配置,`update` 会将其迁移位置(删旧、新路径文件由 `init` 写入默认模板)。**请在 `update` 前备份** 或 `update` 后手动迁移到 `.github/skills/sync/menu-sync/env/env.local.json`。
359
+ > **注意**:如果项目在旧的 `.wl-skills/skills/menu-sync/env/env.local.json` 中有自定义配置,`update` 会将其迁移位置。**请在 `update` 前备份** 或 `update` 后手动迁移到 `.wl-skills/skills/sync/menu-sync/env/env.local.json`。
344
360
 
345
361
  ---
346
362
 
@@ -352,7 +368,7 @@ pnpm dlx @agile-team/wl-skills-kit update
352
368
  | `spec-doc-parse` | ✅ 启用 | `skills/core/spec-doc-parse/` | 规范线:wl-skills-design 标准说明书 → 页面清单 |
353
369
  | `api-contract` | ✅ 启用 | `skills/core/api-contract/` | 生成 api.md 前后端契约 |
354
370
  | `page-codegen` | ✅ 启用 | `skills/core/page-codegen/` | 页面骨架生成 + 模板调度 |
355
- | `convention-audit` | ✅ 启用 | `skills/core/convention-audit/` | 13 条规范扫描 + 双报告 |
371
+ | `convention-audit` | ✅ 启用 | `skills/core/convention-audit/` | 14 条规范扫描 + 双报告 |
356
372
  | `business-doc-extract` | ✅ 启用 | `skills/core/business-doc-extract/` | 语义触发,业务文档抽取与维护 |
357
373
  | `template-extract` | ✅ 启用 | `skills/core/template-extract/` | 现有页面 → 领域模板 |
358
374
  | `menu-sync` | ✅ 启用 | `skills/sync/menu-sync/` | 菜单基线 ↔ 后端接口 |
@@ -389,9 +405,9 @@ pnpm dlx @agile-team/wl-skills-kit update
389
405
 
390
406
  | 命令 | 保护路径 | 说明 |
391
407
  | ---------------------- | -------------------------------- | ------------------------ |
392
- | `init` / `update` | `.github/reports/*.md` | 已存在则跳过,不覆盖累积 |
408
+ | `init` / `update` | `.wl-skills/reports/*.md` | 已存在则跳过,不覆盖累积 |
393
409
  | `clean`(默认) | `src/components/` + `src/types/` | 业务代码必需,永不删除 |
394
- | `clean --keep-reports` | + `.github/reports/` | 保留菜单/字典/权限基线 |
410
+ | `clean --keep-reports` | + `.wl-skills/reports/` | 保留菜单/字典/权限基线 |
395
411
 
396
412
  ---
397
413
 
@@ -424,8 +440,9 @@ AbstractPageQueryHook + BaseQuery + BaseToolbar + BaseTable(render-type="agGrid"
424
440
  - 🔁 Agent Pipeline 运行手册:[docs/agent-pipeline-runbook.md](docs/agent-pipeline-runbook.md)
425
441
  - 🛡️ MCP Tool 风险矩阵:[docs/mcp-tool-risk-matrix.md](docs/mcp-tool-risk-matrix.md)
426
442
  - 📝 业务文档抽取 Skill:[files/.wl-skills/skills/core/business-doc-extract/USAGE.md](files/.wl-skills/skills/core/business-doc-extract/USAGE.md)
427
- - 📚 业务方使用指南:`.github/guides/usage.md`(业务项目内)
428
- - 🏗️ 架构与决策:`.github/guides/architecture.md`(业务项目内)
443
+ - 📚 业务方使用指南:`.wl-skills/guides/usage.md`(业务项目内)
444
+ - 🏗️ 架构与决策:`.wl-skills/guides/architecture.md`(业务项目内)
445
+ - 🛡️ validate 豁免配置:[files/.wl-skills/docs/validate-exempt.md](files/.wl-skills/docs/validate-exempt.md)
429
446
  - 🔧 维护者文档:[kit-internal/README.md](kit-internal/README.md)(仅本仓库)
430
447
  - 🤖 多编辑器适配机制:[files/.wl-skills/skills/\_compat/README.md](files/.wl-skills/skills/_compat/README.md)
431
448
  - 🛠️ Jenkins 流水线参考:[kit-internal/jenkins-pipeline.md](kit-internal/jenkins-pipeline.md)
package/bin/wl-skills.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * wl-skills-kit CLI v2.11.2
4
+ * wl-skills-kit CLI v2.11.4
5
5
  *
6
6
  * 命令:
7
7
  * init 全量安装(默认,向后兼容)
@@ -26,7 +26,6 @@ const crypto = require("crypto");
26
26
  const {
27
27
  runAstRules,
28
28
  getStagedFiles,
29
- hasAstAvailable,
30
29
  runTypeCheck,
31
30
  } = require("../lib/ast-rules");
32
31
 
@@ -382,6 +381,7 @@ function runInstall(incremental) {
382
381
  // 这样即使 early-return(同版本跳过文件复制),hook 也会被创建/更新
383
382
  if (!dryRun) {
384
383
  ensurePreCommitHook(TARGET_DIR);
384
+ ensurePrePushHook(TARGET_DIR);
385
385
  ensureEslintConfig(TARGET_DIR);
386
386
  }
387
387
 
@@ -517,21 +517,20 @@ function runInstall(incremental) {
517
517
  // v2.11 目录重构迁移:.github/skills|standards|guides|reports/ → .wl-skills/
518
518
  for (const prefix of LEGACY_DIR_PREFIXES) {
519
519
  const legacyDir = path.join(TARGET_DIR, prefix);
520
- if (fs.existsSync(legacyDir)) {
521
- const legacyFiles = walkDir(legacyDir, legacyDir);
522
- for (const f of legacyFiles) {
523
- const legacyFile = path.join(legacyDir, f);
524
- if (dryRun) {
525
- console.log(" 迁移清理 " + prefix + f + " (v2.11 目录重构)");
526
- } else {
527
- removeFileAndEmptyParents(legacyFile);
528
- }
529
- migrated++;
530
- }
531
- // 删除空目录
532
- if (!dryRun && fs.existsSync(legacyDir)) {
533
- try { fs.rmSync(legacyDir, { recursive: true, force: true }); } catch {}
520
+ if (!fs.existsSync(legacyDir)) continue;
521
+ const legacyFiles = walkDir(legacyDir, legacyDir);
522
+ for (const f of legacyFiles) {
523
+ const legacyFile = path.join(legacyDir, f);
524
+ if (dryRun) {
525
+ console.log(" 迁移清理 " + prefix + f + " (v2.11 目录重构)");
526
+ } else {
527
+ removeFileAndEmptyParents(legacyFile);
534
528
  }
529
+ migrated++;
530
+ }
531
+ // 删除空目录
532
+ if (!dryRun && fs.existsSync(legacyDir)) {
533
+ try { fs.rmSync(legacyDir, { recursive: true, force: true }); } catch {}
535
534
  }
536
535
  }
537
536
 
@@ -642,7 +641,6 @@ function runInstall(incremental) {
642
641
  */
643
642
  function ensurePreCommitHook(targetDir) {
644
643
  const huskyDir = path.join(targetDir, ".husky");
645
- const preCommitPath = path.join(huskyDir, ".husky/pre-commit");
646
644
 
647
645
  // 只有 git 仓库才创建 husky hook
648
646
  if (!fs.existsSync(path.join(targetDir, ".git"))) return;
@@ -723,6 +721,58 @@ function ensurePreCommitHook(targetDir) {
723
721
  }
724
722
  }
725
723
 
724
+ /**
725
+ * 确保 .husky/pre-push 包含 wl-skills validate --typecheck
726
+ * — pre-push 跑全量类型检查(R14),补 pre-commit 不跑 R14 的缺口
727
+ *
728
+ * 设计理由:pre-commit 跑全量 vue-tsc 太慢(拖慢日常提交),
729
+ * 但 pre-push 频率低、可接受耗时,且 CI 也跑相同命令。
730
+ * 这样 R14 在"推送到远程"和"CI"两个节点都有确定性执行器兜底。
731
+ *
732
+ * 策略同 pre-commit:不存在则创建,存在但无 marker 则追加。
733
+ */
734
+ function ensurePrePushHook(targetDir) {
735
+ const huskyDir = path.join(targetDir, ".husky");
736
+ if (!fs.existsSync(path.join(targetDir, ".git"))) return;
737
+ if (!fs.existsSync(huskyDir)) return;
738
+
739
+ const HOOK_VERSION_TAG = "# wl-skills-prepush-hook-v1";
740
+ const pushContent =
741
+ "#!/usr/bin/env sh\n" +
742
+ HOOK_VERSION_TAG + "\n" +
743
+ "# wl-skills-kit 自动管理:推送前全量类型检查(R14,error 阻断推送)\n" +
744
+ "# 含 vue-tsc/tsc --noEmit,体积较大故放 pre-push 而非 pre-commit\n" +
745
+ "# 如果 node_modules 不存在或 kit 未安装,优雅跳过,不阻断推送\n" +
746
+ 'if [ -f "node_modules/@agile-team/wl-skills-kit/bin/wl-skills.js" ]; then\n' +
747
+ ' node node_modules/@agile-team/wl-skills-kit/bin/wl-skills.js validate --typecheck\n' +
748
+ " if [ $? -ne 0 ]; then\n" +
749
+ ' echo ""\n' +
750
+ ' echo " ✖ 类型检查/规范检测未通过,推送已阻断(R14 类型错误零容忍)"\n' +
751
+ ' echo " → 修复后重新 git push,或单独 commit 后用 --no-verify 跳过(CI 仍会拦截)"\n' +
752
+ " exit 1\n" +
753
+ " fi\n" +
754
+ "else\n" +
755
+ ' echo " ⚠ wl-skills-kit 未安装,跳过推送前类型检查"\n' +
756
+ "fi\n";
757
+
758
+ const pushFile = path.join(huskyDir, "pre-push");
759
+ if (!fs.existsSync(pushFile)) {
760
+ fs.writeFileSync(pushFile, pushContent, "utf8");
761
+ try { fs.chmodSync(pushFile, 0o755); } catch {}
762
+ console.log(" ✔ 已创建 .husky/pre-push(推送前自动运行 wl-skills validate --typecheck)");
763
+ console.log(" → R14 类型检查在 pre-push 兜底(pre-commit 太慢不放这)");
764
+ console.log("");
765
+ } else {
766
+ const existing = fs.readFileSync(pushFile, "utf8");
767
+ if (existing.includes(HOOK_VERSION_TAG)) return;
768
+ const addition = "\n" + pushContent.replace("#!/usr/bin/env sh\n", "");
769
+ fs.writeFileSync(pushFile, existing.trimEnd() + "\n" + addition, "utf8");
770
+ try { fs.chmodSync(pushFile, 0o755); } catch {}
771
+ console.log(" ✔ 已在 .husky/pre-push 追加 wl-skills validate --typecheck(R14 类型检查)");
772
+ console.log("");
773
+ }
774
+ }
775
+
726
776
  /**
727
777
  * 确保业务项目有 ESLint 配置
728
778
  * 策略:如果项目根目录没有 eslint.config.cjs,从 kit 复制模板
@@ -1433,15 +1483,13 @@ function printFixSuggestions(blockingIssues) {
1433
1483
  console.log(" \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
1434
1484
 
1435
1485
  let hasAutoFix = false;
1436
- let hasUnknownIssue = false;
1437
1486
  for (const [rule, ruleIssues] of ruleGroups.entries()) {
1438
1487
  const suggestion = AST_FIX_SUGGESTIONS[rule] || findRegexSuggestion(ruleIssues[0].text);
1439
1488
  const count = ruleIssues.length;
1440
1489
  if (!suggestion) {
1441
1490
  // 免底:未知规则的阻断项也要展示,避免用户看不到任何提示
1442
- hasUnknownIssue = true;
1443
1491
  console.log(" \u2502 " + rule + "\uff08" + count + " \u5904\uff09 [\u2753\u672a\u77e5\u89c4\u5219]");
1444
- console.log(" \u2502 \u2192 \u8bf7\u67e5\u770b .github/standards/ \u76f8\u5173\u89c4\u8303\u6216\u89e6\u53d1\u89c4\u8303\u5ba1\u8ba1");
1492
+ console.log(" \u2502 \u2192 \u8bf7\u67e5\u770b .wl-skills/standards/ \u76f8\u5173\u89c4\u8303\u6216\u89e6\u53d1\u89c4\u8303\u5ba1\u8ba1");
1445
1493
  console.log(" \u2502");
1446
1494
  continue;
1447
1495
  }
@@ -1449,7 +1497,7 @@ function printFixSuggestions(blockingIssues) {
1449
1497
  if (suggestion.auto) hasAutoFix = true;
1450
1498
  console.log(" \u2502 " + rule + "\uff08" + count + " \u5904\uff09" + autoTag);
1451
1499
  console.log(" \u2502 \u2192 " + suggestion.fix);
1452
- console.log(" \u2502 \u53c2\u8003: .github/" + suggestion.ref);
1500
+ console.log(" \u2502 \u53c2\u8003: .wl-skills/" + suggestion.ref);
1453
1501
  console.log(" \u2502");
1454
1502
  }
1455
1503
 
@@ -0,0 +1,113 @@
1
+ # validate 豁免配置(.wl-skills-validate.json)
2
+
3
+ > **版本**:v2.11.3+ 引入。对应执行器 `loadExemptions`(`lib/ast-rules.js`)。
4
+ > **零功能影响**:kit 不主动创建该文件;不存在时 validate 行为完全不变。
5
+
6
+ ---
7
+
8
+ ## 用途
9
+
10
+ 对"标准列表页强制 `BaseTable + AGGrid`"等规则,**批量豁免特殊场景目录**:
11
+ 表单设计器内嵌表格、行内编辑明细表等 BaseTable 受限的场景。
12
+
13
+ 与单文件注释豁免(`<!-- wl-skills:ignore R3 -->`)互补:
14
+
15
+ | 机制 | 粒度 | 适用 |
16
+ |---|---|---|
17
+ | 单文件注释 | 精确到单个文件 | 个别特殊页面 |
18
+ | 项目级配置(本文件) | 批量到目录前缀 | 整片特殊场景(设计器/编辑器域) |
19
+
20
+ ---
21
+
22
+ ## 文件位置
23
+
24
+ 业务项目**根目录**(与 `.wl-skills-manifest.json` 同级):
25
+
26
+ ```
27
+ 你的业务项目/
28
+ ├── .wl-skills-validate.json ← 本文件(可选,按需创建)
29
+ ├── .wl-skills-manifest.json
30
+ └── src/
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Schema
36
+
37
+ ```json
38
+ {
39
+ "exemptions": [
40
+ {
41
+ "paths": ["<页面目录前缀,支持 /**>"],
42
+ "rules": ["<规则编号,如 R3 R10,大小写不敏感>"],
43
+ "reason": "<必填,审计用途,说明为何豁免>"
44
+ }
45
+ ]
46
+ }
47
+ ```
48
+
49
+ ### 字段说明
50
+
51
+ | 字段 | 类型 | 必填 | 说明 |
52
+ |---|---|---|---|
53
+ | `exemptions` | array | 是 | 豁免条目数组 |
54
+ | `exemptions[].paths` | string[] | 是 | 页面目录前缀。支持 `/**` glob;命中该目录**及其子目录**下所有页面 |
55
+ | `exemptions[].rules` | string[] | 是 | 规则编号(`R3`/`R10` 等),大小写不敏感 |
56
+ | `exemptions[].reason` | string | 是 | 审计字段,说明豁免原因,避免滥用 |
57
+
58
+ ### 路径匹配规则
59
+
60
+ - `src/views/produce/designer` → 命中 `src/views/produce/designer` 及 `src/views/produce/designer/**`
61
+ - `src/views/sale/order/**` → 等价于上一行(显式 glob)
62
+ - 路径分隔符自动规范化(Windows `\` → `/`),末尾 `/` 被忽略
63
+
64
+ ---
65
+
66
+ ## 当前可豁免规则
67
+
68
+ | 规则 | 检测内容 | 典型豁免场景 |
69
+ |---|---|---|
70
+ | `R3` | el-table 未用 BaseTable | 表单设计器内嵌表格、行内编辑明细表 |
71
+ | `R10` | el-form/el-select 等原生组件未用平台封装 | 设计器/自定义编辑器内部 |
72
+
73
+ > 其他规则(R1/R2/R4~R9/R11~R14)原则上不豁免;确有需要时用单文件注释豁免。
74
+
75
+ ---
76
+
77
+ ## 完整示例
78
+
79
+ ```json
80
+ {
81
+ "exemptions": [
82
+ {
83
+ "paths": ["src/views/produce/designer"],
84
+ "rules": ["R3", "R10"],
85
+ "reason": "表单设计器内嵌表格 + 自定义编辑器,BaseTable AGGrid 内联编辑受限"
86
+ },
87
+ {
88
+ "paths": ["src/views/sale/order-edit/**"],
89
+ "rules": ["R3"],
90
+ "reason": "订单行内编辑明细表,AGGrid 行编辑成本高于收益"
91
+ },
92
+ {
93
+ "paths": ["src/components/business/rich-table"],
94
+ "rules": ["R3"],
95
+ "reason": "复杂合并单元格/自定义行列布局,AGGrid 不易实现"
96
+ }
97
+ ]
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 验证
104
+
105
+ ```bash
106
+ # 跑 validate,命中豁免的页面不再报对应规则
107
+ wl-skills validate src/views
108
+
109
+ # 豁免配置格式错误时,validate 输出 warn 提示(不阻断),行为退化为无豁免
110
+ ```
111
+
112
+ > 豁免项升级为主列表页时,必须迁移回 `BaseTable + AGGrid` 并从本文件移除豁免。
113
+ > `convention-audit` 审计会列出所有豁免项供人工复核。
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **读者**:团队技术负责人 / wl-skills-kit 维护者 / 对体系设计感兴趣的团队成员
4
4
  > **更新方式**:重大架构变更后追加对应章节,旧章节原文保留(历史可溯)
5
- > **当前版本**:v2.11.2(2026-06-21)
5
+ > **当前版本**:v2.11.4(2026-06-21)
6
6
 
7
7
  ---
8
8
 
@@ -10,7 +10,7 @@ alwaysApply: true
10
10
  ---
11
11
 
12
12
  <!-- Cursor MDC 规则文件。由 @agile-team/wl-skills-kit 自动生成。 -->
13
- <!-- 源文件:.github/copilot-instructions.md,更新方式:npx @agile-team/wl-skills-kit@latest update -->
13
+ <!-- 源文件:.wl-skills/copilot-instructions-full.md,更新方式:npx @agile-team/wl-skills-kit@latest update -->
14
14
 
15
15
  ---
16
16
 
@@ -4,7 +4,7 @@ description: 项目编码规范(14 条标准 + 11 个 Skill 自动调度)
4
4
  ---
5
5
 
6
6
  <!-- Kiro Steering 规则。由 @agile-team/wl-skills-kit 自动生成。-->
7
- <!-- 源文件:.github/copilot-instructions.md,更新方式:npx @agile-team/wl-skills-kit@latest update -->
7
+ <!-- 源文件:.wl-skills/copilot-instructions-full.md,更新方式:npx @agile-team/wl-skills-kit@latest update -->
8
8
 
9
9
  ---
10
10
 
@@ -5,7 +5,7 @@ alwaysApply: true
5
5
  ---
6
6
 
7
7
  <!-- Trae Rules 规则。由 @agile-team/wl-skills-kit 自动生成。-->
8
- <!-- 源文件:.github/copilot-instructions.md,更新方式:npx @agile-team/wl-skills-kit@latest update -->
8
+ <!-- 源文件:.wl-skills/copilot-instructions-full.md,更新方式:npx @agile-team/wl-skills-kit@latest update -->
9
9
 
10
10
  ---
11
11
 
@@ -160,8 +160,9 @@ description: "Use when: auditing project source code against the 14 modular stan
160
160
  - ESLint:是否可执行
161
161
  - TypeScript:`vue-tsc --noEmit`(回退 `tsc --noEmit`)是否可执行、是否 0 error(R14)
162
162
  - Git:当前分支 / 最近提交
163
- - Husky:`.husky/pre-commit`、`.husky/commit-msg` 是否存在
163
+ - Husky:`.husky/pre-commit`、`.husky/pre-push`、`.husky/commit-msg` 是否存在
164
164
  3. 读取 `package.json` 获取项目脚本名称
165
+ 4. **读取豁免配置**(v2.11.4+):如项目根存在 `.wl-skills-validate.json`,解析其中所有豁免条目(paths / rules / reason),在报告中单列"豁免项复核清单",供人工逐条确认是否仍需豁免
165
166
 
166
167
  ### 步骤 3:扫描源码
167
168
 
@@ -132,10 +132,62 @@ subColumnsDef(): TableColumnDesc<any>[] {
132
132
 
133
133
  ### 豁免规则
134
134
 
135
- - 豁免场景仍**优先使用 `BaseTable`**(非 AGGrid 模式),而非裸 `el-table`
136
- - 豁免场景如果支持列持久化,仍**必须有 `cid`**
137
- - 审查报告中豁免项必须标注**原因**,避免滥用
138
- - 如果豁免场景后续升级为主列表,必须迁移到 AGGrid
135
+ > **两层豁免机制**:单文件注释豁免(精确)+ 项目级配置豁免(批量)。标准列表页不受豁免影响,仍强制 `BaseTable + AGGrid`。
136
+
137
+ #### 优先级:标准列表 vs 特殊场景
138
+
139
+ | 场景类型 | 渲染要求 | R3(el-table)/ AGGrid 卡控 |
140
+ |---|---|---|
141
+ | **标准列表页**(分页查询、台账、核心业务列表) | `BaseTable` + `render-type="agGrid"` + `cid` | 🔴 强制,不可豁免 |
142
+ | **BaseTable 可胜任的特殊表格**(弹窗小表、只读展示、嵌套子表) | `BaseTable`(非 AGGrid 也可),尽量带 `cid` | 🟢 豁免 AGGrid(R3 仍建议改 BaseTable) |
143
+ | **BaseTable 受限的复杂场景**(表单/设计器内嵌表格、行内编辑明细表、特殊合并单元格/复杂行列布局) | 优先 `BaseTable`;确实受限时降级 `el-table` | 🟢 可豁免 R3(需登记豁免原因) |
144
+ | **平台封装组件内部** | 按需 | 🟢 豁免 R3(封装层单独评审) |
145
+
146
+ > 判定原则:**先用 BaseTable**;BaseTable 确实受限不能满足时,才降级 `el-table`,不直接裸用。
147
+
148
+ #### 豁免方式一:单文件注释(精确到文件)
149
+
150
+ 在特殊文件内加注释标记,精确豁免该文件的指定规则(适合个别页面):
151
+
152
+ ```vue
153
+ <!-- index.vue -->
154
+ <!-- wl-skills:ignore R3 --> ← 整页豁免 R3(el-table 检测)
155
+ <template>
156
+ <el-table> ... </el-table> <!-- 表单设计器内嵌表格,AGGrid 内联编辑受限 -->
157
+ </template>
158
+ ```
159
+
160
+ ```typescript
161
+ // data.ts
162
+ // wl-skills:ignore R10 ← 豁免该文件的 R10
163
+ ```
164
+
165
+ #### 豁免方式二:项目级配置(批量到目录,推荐用于整片特殊场景)
166
+
167
+ 项目根放 `.wl-skills-validate.json`(kit 不主动创建,零功能影响;详见 `docs/validate-exempt.md`):
168
+
169
+ ```json
170
+ {
171
+ "exemptions": [
172
+ {
173
+ "paths": ["src/views/produce/designer"],
174
+ "rules": ["R3", "R10"],
175
+ "reason": "表单设计器内嵌表格,BaseTable AGGrid 内联编辑受限"
176
+ },
177
+ {
178
+ "paths": ["src/views/sale/order-edit/**"],
179
+ "rules": ["R3"],
180
+ "reason": "订单行内编辑明细表,AGGrid 行编辑成本高于收益"
181
+ }
182
+ ]
183
+ }
184
+ ```
185
+
186
+ - `paths`:页面目录前缀,支持 `/**` glob;命中该目录及其子目录
187
+ - `rules`:规则编号(`R3`/`R10` 等),大小写不敏感
188
+ - `reason`:**必填审计字段**,避免滥用
189
+
190
+ > ⚠️ 豁免不是放任。豁免项升级为主列表页时,必须迁移回 `BaseTable + AGGrid`。`convention-audit` 审计时列出所有豁免项供人工复核。
139
191
 
140
192
  ### 审查报告中的分类
141
193
 
@@ -116,6 +116,7 @@ export function createPage() {
116
116
 
117
117
  - [ ] 查询区使用了 BaseQuery,没有自己写 el-form?
118
118
  - [ ] 表格使用了 BaseTable + agGrid + cid,没有用 el-table?
119
+ (BaseTable 受限的特殊场景如表单/设计器内嵌表格,登记豁免后可用 el-table,见 standards/12 §豁免规则)
119
120
  - [ ] 弹窗使用了 c_formModal / c_listModal,没有手写 el-dialog?
120
121
  - [ ] 日期组件用了 jh-date 系列,没有用 el-date-picker?
121
122
  - [ ] HTTP 请求都走 `this.getAction / postAction`,没有 import axios?
package/lib/ast-rules.js CHANGED
@@ -31,6 +31,7 @@
31
31
  * countEffectiveLines(scriptContent) → number
32
32
  * computeFunctionComplexity(fnNode) → number
33
33
  * runTypeCheck(root) → { issues, ran, errorCount }
34
+ * loadExemptions(targetDir) → { isExempt, source, warnings }
34
35
  * hasAstAvailable() → boolean
35
36
  */
36
37
 
@@ -51,14 +52,6 @@ function ensureAst() {
51
52
  if (_astChecked) return _compilerSfc && _babelParser;
52
53
  _astChecked = true;
53
54
 
54
- // 优先从 kit 自身 node_modules 解析(开发/测试环境)
55
- const tryPaths = [
56
- // 1. kit 自身 node_modules(标准 require 解析)
57
- null,
58
- // 2. 调用方项目(CWD)的 node_modules — 业务项目通过 npx/node 运行时
59
- // kit 本身没装 @vue/compiler-sfc,但业务项目有
60
- ];
61
-
62
55
  // 尝试标准 require(从 kit 目录解析)
63
56
  try {
64
57
  _compilerSfc = require("@vue/compiler-sfc");
@@ -75,7 +68,7 @@ function ensureAst() {
75
68
  if (!_compilerSfc || !_babelParser) {
76
69
  const cwd = process.env.WL_PROJECT_ROOT || process.cwd();
77
70
  try {
78
- const createRequire = require("module").createRequire;
71
+ const {createRequire} = require("module");
79
72
  const cwdRequire = createRequire(cwd + "/package.json");
80
73
  if (!_compilerSfc) {
81
74
  try { _compilerSfc = cwdRequire("@vue/compiler-sfc"); } catch {}
@@ -169,9 +162,87 @@ const CONFIG = {
169
162
  MAX_CYCLOMATIC_COMPLEXITY: 10,
170
163
  // R14: 单页类型错误采集上限(避免输出爆炸)
171
164
  TYPECHECK_ERROR_CAP: 50,
165
+ // 项目级豁免配置文件名(业务项目根,kit 不主动创建,零功能影响)
166
+ EXEMPT_CONFIG_NAME: ".wl-skills-validate.json",
172
167
  SKIP_DIRS: ["node_modules", "dist", ".git", "demo", "template"],
173
168
  };
174
169
 
170
+ // ─── 项目级豁免配置(零功能影响,可选)──────────────────────────────────
171
+ //
172
+ // 业务项目根可放 .wl-skills-validate.json,对指定路径前缀批量豁免规则:
173
+ // {
174
+ // "exemptions": [
175
+ // {
176
+ // "paths": ["src/views/produce/designer"],
177
+ // "rules": ["R3", "R10"],
178
+ // "reason": "表单设计器内嵌表格,BaseTable AGGrid 内联编辑受限"
179
+ // }
180
+ // ]
181
+ // }
182
+ //
183
+ // 与单文件注释豁免(wl-skills:ignore R3)互补:注释精确到单文件,
184
+ // 配置批量到目录。无配置文件时返回空豁免,行为完全不变。
185
+
186
+ /**
187
+ * 加载项目级豁免配置
188
+ * @param {string} targetDir 项目根目录
189
+ * @returns {{ isExempt: (pageDir:string, rule:string)=>boolean, source: string|null, warnings: string[] }}
190
+ */
191
+ function loadExemptions(targetDir) {
192
+ const warnings = [];
193
+ const configPath = path.join(
194
+ targetDir || process.cwd(),
195
+ CONFIG.EXEMPT_CONFIG_NAME,
196
+ );
197
+ if (!fs.existsSync(configPath)) {
198
+ return { isExempt: () => false, source: null, warnings };
199
+ }
200
+ let raw;
201
+ try {
202
+ raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
203
+ } catch (e) {
204
+ warnings.push(
205
+ CONFIG.EXEMPT_CONFIG_NAME +
206
+ " 解析失败,已忽略(" +
207
+ ((e && e.message) || String(e)) +
208
+ ")",
209
+ );
210
+ return { isExempt: () => false, source: configPath, warnings };
211
+ }
212
+ const list = Array.isArray(raw.exemptions) ? raw.exemptions : [];
213
+ // 预编译:每个 path 规范化为前缀,每个 entry 的 rules 转大写 Set
214
+ const compiled = [];
215
+ for (const entry of list) {
216
+ if (!entry || !Array.isArray(entry.paths) || !Array.isArray(entry.rules)) {
217
+ continue;
218
+ }
219
+ const rules = new Set(
220
+ entry.rules.map((r) => String(r).toUpperCase()),
221
+ );
222
+ for (let p of entry.paths) {
223
+ p = String(p).replace(/\\/g, "/").replace(/\/+$/, "");
224
+ if (p.endsWith("/**")) p = p.slice(0, -3);
225
+ if (p.endsWith("/*")) p = p.slice(0, -2);
226
+ compiled.push({ prefix: p, rules });
227
+ }
228
+ }
229
+ function isExempt(pageDir, rule) {
230
+ if (!pageDir || !rule) return false;
231
+ const dir = String(pageDir).replace(/\\/g, "/");
232
+ const r = String(rule).toUpperCase();
233
+ for (const c of compiled) {
234
+ if (
235
+ (c.rules.has(r)) &&
236
+ (dir === c.prefix || dir.startsWith(c.prefix + "/"))
237
+ ) {
238
+ return true;
239
+ }
240
+ }
241
+ return false;
242
+ }
243
+ return { isExempt, source: configPath, warnings };
244
+ }
245
+
175
246
  // ─── 工具函数 ──────────────────────────────────────────────────────────
176
247
 
177
248
  function walkDir(dir, base, results) {
@@ -351,7 +422,7 @@ function resolveFnName(node, parent) {
351
422
  return parent.key.name || parent.key.value || "(anonymous)";
352
423
  }
353
424
  if (parent.type === "AssignmentExpression" && parent.left) {
354
- const left = parent.left;
425
+ const {left} = parent;
355
426
  if (left.property) return left.property.name || left.property.value;
356
427
  if (left.name) return left.name;
357
428
  }
@@ -453,6 +524,75 @@ function isLikelyListPage(template) {
453
524
  return hasBaseTable && hasPagination && !hasForm && !hasTabs;
454
525
  }
455
526
 
527
+ // ─── 单页规则 helper(从 runAstRules 抽出,降低主函数圈复杂度)──────────
528
+
529
+ /**
530
+ * R8: 强制 3 文件分离 — 有 API 调用/大量逻辑但无 data.ts,或有 data.ts 仍泄漏 API 调用
531
+ */
532
+ function checkR8FileSeparation(scriptContent, effectiveLines, hasDataTs, pageDir, fullSource, issues) {
533
+ const hasApiCall = /getAction|postAction|putAction|deleteAction|API_CONFIG/.test(
534
+ stripCommentsAndStrings(scriptContent),
535
+ );
536
+ const ignore = hasIgnoreMarker(fullSource, "R8");
537
+ if (!hasDataTs) {
538
+ if (hasApiCall && !ignore) {
539
+ issues.push({
540
+ level: "error",
541
+ dir: pageDir,
542
+ text: "页面有接口调用但缺 data.ts(业务逻辑必须在 data.ts 中)",
543
+ rule: "R8",
544
+ });
545
+ } else if (effectiveLines > 20 && !ignore) {
546
+ issues.push({
547
+ level: "warn",
548
+ dir: pageDir,
549
+ text: "index.vue 有 " + effectiveLines + " 行逻辑但无 data.ts(建议拆分)",
550
+ rule: "R8",
551
+ });
552
+ }
553
+ }
554
+ // 有 data.ts 但 index.vue 仍然有 API 调用(逻辑泄漏)
555
+ if (hasDataTs && hasApiCall && !ignore) {
556
+ issues.push({
557
+ level: "error",
558
+ dir: pageDir,
559
+ text: "有 data.ts 但 index.vue 中仍含 API 调用(逻辑应全部在 data.ts)",
560
+ rule: "R8",
561
+ });
562
+ }
563
+ }
564
+
565
+ /**
566
+ * R13: 单函数圈复杂度 > MAX_CYCLOMATIC_COMPLEXITY → error
567
+ * 覆盖 index.vue <script> 与 data.ts 的所有函数/方法/箭头函数(嵌套函数独立计)
568
+ */
569
+ function checkR13Complexity(scriptContent, dataPath, pageDir, fullSource, issues) {
570
+ if (hasIgnoreMarker(fullSource, "R13")) return;
571
+ const maxC = CONFIG.MAX_CYCLOMATIC_COMPLEXITY;
572
+ const scanComplexity = (code, label) => {
573
+ const ast = parseScriptAst(code);
574
+ if (!ast) return;
575
+ for (const fn of collectFunctions(ast)) {
576
+ const cc = computeFunctionComplexity(fn.node);
577
+ if (cc > maxC) {
578
+ issues.push({
579
+ level: "error",
580
+ dir: pageDir,
581
+ text:
582
+ label + " 函数 " + fn.name +
583
+ "() 圈复杂度 " + cc + "(阈值 " + maxC +
584
+ "),需拆分为更小函数(standard 04)",
585
+ rule: "R13",
586
+ });
587
+ }
588
+ }
589
+ };
590
+ scanComplexity(scriptContent, "index.vue");
591
+ if (fs.existsSync(dataPath)) {
592
+ scanComplexity(fs.readFileSync(dataPath, "utf8"), "data.ts");
593
+ }
594
+ }
595
+
456
596
  // ─── 主检测函数 ────────────────────────────────────────────────────────
457
597
 
458
598
  /**
@@ -508,6 +648,12 @@ function runAstRules(targetDir, scanRel, options) {
508
648
  const issues = [];
509
649
  const globalCidMap = new Map(); // cid → Set<pageDir>
510
650
 
651
+ // 项目级豁免配置(零功能影响,无配置文件时返回空豁免)
652
+ const exempt = loadExemptions(targetDir);
653
+ for (const w of exempt.warnings) {
654
+ issues.push({ level: "warn", dir: ".", text: w, rule: "EXEMPT" });
655
+ }
656
+
511
657
  for (const page of pages) {
512
658
  const absDir = path.join(targetDir, page.dir);
513
659
 
@@ -547,31 +693,7 @@ function runAstRules(targetDir, scanRel, options) {
547
693
 
548
694
  // R13: 单函数圈复杂度 ≤ MAX_CYCLOMATIC_COMPLEXITY(standard 04,Mcabe)
549
695
  // 覆盖 index.vue <script> 与 data.ts 的所有函数/方法/箭头函数
550
- if (!hasIgnoreMarker(fullSource, "R13")) {
551
- const maxC = CONFIG.MAX_CYCLOMATIC_COMPLEXITY;
552
- const scanComplexity = (code, label) => {
553
- const ast = parseScriptAst(code);
554
- if (!ast) return;
555
- for (const fn of collectFunctions(ast)) {
556
- const cc = computeFunctionComplexity(fn.node);
557
- if (cc > maxC) {
558
- issues.push({
559
- level: "error",
560
- dir: page.dir,
561
- text:
562
- label + " 函数 " + fn.name +
563
- "() 圈复杂度 " + cc + "(阈值 " + maxC +
564
- "),需拆分为更小函数(standard 04)",
565
- rule: "R13",
566
- });
567
- }
568
- }
569
- };
570
- scanComplexity(scriptContent, "index.vue");
571
- if (fs.existsSync(dataPath)) {
572
- scanComplexity(fs.readFileSync(dataPath, "utf8"), "data.ts");
573
- }
574
- }
696
+ checkR13Complexity(scriptContent, dataPath, page.dir, fullSource, issues);
575
697
 
576
698
  // R2: 禁止的 import / 全局 API
577
699
  if (scriptContent) {
@@ -639,11 +761,12 @@ function runAstRules(targetDir, scanRel, options) {
639
761
  }
640
762
  }
641
763
 
642
- // R3: el-table 但未用 BaseTable(检查豁免标记)
764
+ // R3: el-table 但未用 BaseTable(检查注释豁免 + 配置豁免)
643
765
  if (
644
766
  hasRawElTable(template) &&
645
767
  !hasBaseTable(template) &&
646
- !hasIgnoreMarker(fullSource, "R3")
768
+ !hasIgnoreMarker(fullSource, "R3") &&
769
+ !exempt.isExempt(page.dir, "R3")
647
770
  ) {
648
771
  issues.push({
649
772
  level: "error",
@@ -727,38 +850,10 @@ function runAstRules(targetDir, scanRel, options) {
727
850
 
728
851
  // R8: 强制 3 文件分离 — 有 API 调用/大量逻辑但无 data.ts
729
852
  // 任何页面(不分类型)只要有接口调用或超过阈值行数,就应该拆出 data.ts
730
- {
731
- const hasApiCall = /getAction|postAction|putAction|deleteAction|API_CONFIG/.test(
732
- stripCommentsAndStrings(scriptContent),
733
- );
734
- const hasScss = page.names.has("index.scss");
735
- if (!page.names.has("data.ts")) {
736
- if (hasApiCall && !hasIgnoreMarker(fullSource, "R8")) {
737
- issues.push({
738
- level: "error",
739
- dir: page.dir,
740
- text: "页面有接口调用但缺 data.ts(业务逻辑必须在 data.ts 中)",
741
- rule: "R8",
742
- });
743
- } else if (effectiveLines > 20 && !hasIgnoreMarker(fullSource, "R8")) {
744
- issues.push({
745
- level: "warn",
746
- dir: page.dir,
747
- text: "index.vue 有 " + effectiveLines + " 行逻辑但无 data.ts(建议拆分)",
748
- rule: "R8",
749
- });
750
- }
751
- }
752
- // 有 data.ts 但 index.vue 仍然有 API 调用(逻辑泄漏)
753
- if (page.names.has("data.ts") && hasApiCall && !hasIgnoreMarker(fullSource, "R8")) {
754
- issues.push({
755
- level: "error",
756
- dir: page.dir,
757
- text: "有 data.ts 但 index.vue 中仍含 API 调用(逻辑应全部在 data.ts)",
758
- rule: "R8",
759
- });
760
- }
761
- }
853
+ checkR8FileSeparation(
854
+ scriptContent, effectiveLines, page.names.has("data.ts"),
855
+ page.dir, fullSource, issues,
856
+ );
762
857
 
763
858
  // R9: api.md 质量检测 — 有 API_CONFIG 时检查 api.md 结构完整性
764
859
  if (page.names.has("data.ts")) {
@@ -769,10 +864,6 @@ function runAstRules(targetDir, scanRel, options) {
769
864
  if (hasApiConfig && page.names.has("api.md")) {
770
865
  const apiMdPath = path.join(absDir, "api.md");
771
866
  const apiMdContent = fs.readFileSync(apiMdPath, "utf-8");
772
- // 检查 api.md 是否有接口列表表格(核心结构)
773
- const hasInterfaceTable = /\|\s*操作\s*\|.*\|\s*Method\s*\||\|\s*操作\s*\|.*\|\s*URL\s*\|/.test(apiMdContent);
774
- // 检查 api.md 是否有实体定义
775
- const hasEntityDef = /字段|实体|Entity|字段名/.test(apiMdContent);
776
867
  // 检查 api.md 中的 URL 是否与 data.ts API_CONFIG 一致
777
868
  // 正则匹配 URL 路径:支持多段、数字、连字符、下划线
778
869
  // 例:/mdata/mdataModel/list /api/v2/customer-archive/save /sys/user_role/list
@@ -808,7 +899,8 @@ function runAstRules(targetDir, scanRel, options) {
808
899
  const tagRegex = new RegExp("<" + tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[\\s>]");
809
900
  if (
810
901
  tagRegex.test(template) &&
811
- !hasIgnoreMarker(fullSource, "R10")
902
+ !hasIgnoreMarker(fullSource, "R10") &&
903
+ !exempt.isExempt(page.dir, "R10")
812
904
  ) {
813
905
  issues.push({
814
906
  level: "error",
@@ -1013,22 +1105,26 @@ function runTypeCheck(root) {
1013
1105
  if (result.status === 0) {
1014
1106
  return { issues: [], ran: true, errorCount: 0 };
1015
1107
  }
1016
- // 退出码非 0 但未解析出标准 error 行:疑似 tsconfig / 配置级错误
1108
+ // 退出码非 0 但未解析出标准 error 行:区分配置级错误与未知失败
1109
+ // tsconfig 配置问题(extends 不存在、语法错等)归为 warn(非类型错误,不阻断类型门禁)
1110
+ const isConfigError = /tsconfig|Cannot find module.*\.json|error TS6053|error TS5023|File not found/i.test(out);
1017
1111
  return {
1018
1112
  issues: [
1019
1113
  {
1020
- level: "error",
1114
+ level: isConfigError ? "warn" : "error",
1021
1115
  dir: label,
1022
1116
  text:
1023
1117
  checker +
1024
1118
  " --noEmit 退出码 " +
1025
1119
  result.status +
1026
- "(请检查 tsconfig / 类型配置,无标准 TS 错误输出)",
1120
+ (isConfigError
1121
+ ? "(疑似 tsconfig 配置问题,非类型错误,请检查 tsconfig.json)"
1122
+ : "(无标准 TS 错误输出,请检查 tsconfig / 类型配置)"),
1027
1123
  rule: "R14",
1028
1124
  },
1029
1125
  ],
1030
1126
  ran: true,
1031
- errorCount: 1,
1127
+ errorCount: isConfigError ? 0 : 1,
1032
1128
  };
1033
1129
  }
1034
1130
 
@@ -1054,6 +1150,7 @@ module.exports = {
1054
1150
  computeFunctionComplexity,
1055
1151
  collectFunctions,
1056
1152
  runTypeCheck,
1153
+ loadExemptions,
1057
1154
  hasAstAvailable,
1058
1155
  isAstFunctionallyUsable,
1059
1156
  getStagedFiles,
package/lib/page-spec.js CHANGED
@@ -262,7 +262,7 @@ function extractOperationSequence(dataContent) {
262
262
  for (const item of items) {
263
263
  const labelM = item.match(/(?:^|[\s,{])label\s*:\s*["'`]([^"'`]+)["'`]/);
264
264
  const typeM = item.match(/(?:^|[\s,{])type\s*:\s*["'`]([^"'`]+)["'`]/);
265
- let label = labelM ? labelM[1] : typeM ? TYPE_LABEL[typeM[1]] : null;
265
+ const label = labelM ? labelM[1] : typeM ? TYPE_LABEL[typeM[1]] : null;
266
266
  if (label) result.push({ label });
267
267
  }
268
268
  return result;
@@ -288,9 +288,6 @@ function extractBracketBody(source, openIdx) {
288
288
  function seqNames(seq) {
289
289
  return seq.map((x) => x.name).filter(Boolean);
290
290
  }
291
- function seqLabels(seq) {
292
- return seq.map((x) => x.label).filter(Boolean);
293
- }
294
291
 
295
292
  /** 数组顺序是否严格相等 */
296
293
  function arrayEq(a, b) {
@@ -314,6 +311,31 @@ function pushMissingImplementationIssue(issues, level, dir, rule, target) {
314
311
  });
315
312
  }
316
313
 
314
+ /**
315
+ * S3 颜色比对:集合一致时逐个核对 toolbar 按钮颜色(抽出降低 compareSpecToCode 复杂度)
316
+ */
317
+ function pushToolbarColorIssues(specToolbar, actualToolbar, dir, issues) {
318
+ const actualByLabel = new Map(actualToolbar.map((b) => [b.label, b]));
319
+ for (const sb of specToolbar) {
320
+ if (!sb.label || !sb.color) continue;
321
+ const ab = actualByLabel.get(sb.label);
322
+ if (ab && ab.color !== sb.color) {
323
+ issues.push({
324
+ level: "warn",
325
+ dir,
326
+ rule: "S3",
327
+ text:
328
+ '按钮"' +
329
+ sb.label +
330
+ '"颜色与原型不一致:spec=' +
331
+ sb.color +
332
+ " vs code=" +
333
+ ab.color,
334
+ });
335
+ }
336
+ }
337
+ }
338
+
317
339
  /**
318
340
  * 比对 page-spec 与 data.ts 实际实现
319
341
  * @param {object} spec page-spec.json 对象
@@ -444,25 +466,7 @@ function compareSpecToCode(spec, dataContent, dir) {
444
466
  }
445
467
  // 颜色比对(仅在集合一致时逐个核对)
446
468
  if (specLabels.length && actualLabels.length && setEq(specLabels, actualLabels)) {
447
- const actualByLabel = new Map(actual.map((b) => [b.label, b]));
448
- for (const sb of spec.toolbar) {
449
- if (!sb.label || !sb.color) continue;
450
- const ab = actualByLabel.get(sb.label);
451
- if (ab && ab.color !== sb.color) {
452
- issues.push({
453
- level: "warn",
454
- dir,
455
- rule: "S3",
456
- text:
457
- '按钮"' +
458
- sb.label +
459
- '"颜色与原型不一致:spec=' +
460
- sb.color +
461
- " vs code=" +
462
- ab.color,
463
- });
464
- }
465
- }
469
+ pushToolbarColorIssues(spec.toolbar, actual, dir, issues);
466
470
  }
467
471
  }
468
472
  }
@@ -23,8 +23,6 @@
23
23
  * ⑤ Vite 插件层(本插件) → 构建时自动执行,无需额外配置 ← 不可绕开
24
24
  */
25
25
 
26
- const path = require("path");
27
-
28
26
  let astRules = null;
29
27
  try {
30
28
  astRules = require("./ast-rules.js");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agile-team/wl-skills-kit",
3
- "version": "2.11.2",
4
- "description": "AI Skill 模板包 v2.11.2 — 14 条编码规范 + 11 个 AI Skill + 17 个 MCP Tool,一条命令导入 Vue 3 项目(.wl-skills/ 统一隔离架构)",
3
+ "version": "2.11.4",
4
+ "description": "AI Skill 模板包 v2.11.4 — 14 条编码规范 + 11 个 AI Skill + 17 个 MCP Tool,一条命令导入 Vue 3 项目(.wl-skills/ 统一隔离架构)",
5
5
  "main": "./bin/wl-skills.js",
6
6
  "packageManager": "pnpm@11.5.3",
7
7
  "bin": {
@@ -48,11 +48,11 @@
48
48
  "lint:skills": "node scripts/lint-skills.js",
49
49
  "test": "vitest run",
50
50
  "version:verify": "node scripts/verify-version.js",
51
- "verify": "pnpm version:verify && pnpm lint:skills && pnpm test",
51
+ "verify": "pnpm version:verify && pnpm lint && pnpm lint:skills && pnpm test",
52
52
  "ci": "pnpm install --frozen-lockfile && pnpm verify",
53
53
  "pack:dry": "npm pack --dry-run --ignore-scripts",
54
54
  "release:check": "pnpm verify && npm pack --dry-run --ignore-scripts",
55
- "prepublishOnly": "node scripts/verify-version.js && node scripts/lint-skills.js && vitest run"
55
+ "prepublishOnly": "node scripts/verify-version.js && pnpm lint && node scripts/lint-skills.js && vitest run"
56
56
  },
57
57
  "optionalDependencies": {
58
58
  "@babel/parser": "^7.20.0",
@@ -90,7 +90,13 @@
90
90
  }
91
91
  },
92
92
  "lint-staged": {
93
- "src/**/*.{js,jsx,ts,tsx,vue}": [
93
+ "bin/**/*.js": [
94
+ "eslint --fix --no-cache"
95
+ ],
96
+ "lib/**/*.js": [
97
+ "eslint --fix --no-cache"
98
+ ],
99
+ "scripts/**/*.js": [
94
100
  "eslint --fix --no-cache"
95
101
  ]
96
102
  },