@agile-team/wl-skills-kit 2.9.4 → 2.10.1

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.10.1] - 2026-06-14
4
+
5
+ ### Added
6
+
7
+ - **pre-commit 阻断修复建议输出**:`wl-skills validate` 阻断时自动输出格式化修复建议框,含 AST R1~R12 规则映射和正则检查建议,标注可自动修复项,引导触发 `规范审计 → 自动修复 → 复扫验证` 流程
8
+ - **code-fix 强制复扫验证**:新增步骤[6]闭环关键步骤,修复完成后必须自动执行 `wl-skills validate` 复扫,用户不可跳过
9
+ - **convention-audit --quick 复扫模式**:仅复查上次报告中的 🔴🟡 项,token 消耗约全量 10%,适用于 code-fix 后轻量级闭环验证
10
+ - **_pipeline.md 闭环强化**:code-fix→validate 从"建议"升级为"强制"约定,新增强制执行 vs 建议执行对照表
11
+ - **_registry.md 加固调度规则 10-12**:闭环强制约定、高风险 Skill 确认机制(page-codegen/sync 类/code-fix)、误触发防护
12
+ - **_best-practices.md 场景 5 闭环补全**:完整闭环流程含强制复扫步骤和触发话术
13
+ - **copilot-instructions.md AI 执行护栏**:新增 5 条强制约定(闭环强制、确认机制、误触发防护、Pre-flight 声明、修复建议输出规范)
14
+
15
+ ### Fixed
16
+
17
+ - 修复 `printFixSuggestions` 对未知规则静默跳过的问题,新增兜底输出
18
+ - 修复 copilot-instructions.md 高风险 Skill 列表与 _registry.md 不一致的问题
19
+
20
+ ## [2.10.0] - 2026-05-31
21
+
22
+ ### Added
23
+
24
+ - **新增 `spec-doc-parse` Skill(规范线)**:专门解构 wl-skills-design 产出的标准《需求设计说明书》(IPO 表 / 功能编码 / 流程五要素),输出与 `prototype-scan` 完全相同格式的 page-spec JSON,自带 Pre-flight 校验 + Parse Validation 五项检查 + 自动修复纠偏 + 解析报告(`SPEC_PARSE_*.md`)
25
+ - **`convention-audit` 新增 `--mode spec-align`(GAP 报告)**:比对 spec 定义字段 vs 代码实际字段,输出 `SPEC_GAP_*.md`,完成「说明书 → 代码」零损耗闭环验证
26
+
27
+ ### Changed
28
+
29
+ - **双线隔离(原型线 / 规范线)**:`_registry.md` 调度规则新增「优先级 0」——输入命中 `docs/spec/` / 功能编码 `/[A-Z]{2,6}[0-9]{3}/` / IPO 表特征时强制路由 `spec-doc-parse`,禁止 `prototype-scan` 接管;两线最终汇聚同一份 page-spec JSON,下游无感知
30
+ - **`prototype-scan` 模式 B 收敛**:由「详设文档」收敛为「非规范零散详设」,加入排除声明,避免与标准说明书混淆
31
+ - 同步更新 `_pipeline.md`(规范线分支 + I/O 契约)、`copilot-instructions.md`(Intent Router)、`kit-internal/architecture.md`(ADR-009)、`lint-skills.js`(spec-doc-parse 纳入写操作 Skill 校验)
32
+ - 启用 Skill 数 10 → 11
33
+
3
34
  ## [2.9.4] - 2026-05-18
4
35
 
5
36
  ### Fixed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @agile-team/wl-skills-kit
2
2
 
3
- **AI Skill 模板包 v2.9.4** — 一键将 14 条规范、10 个 AI Skill、17 个 MCP Tool、编辑器 MCP 配置、文档导入 Vue 3 项目。
3
+ **AI Skill 模板包 v2.10.1** — 一键将 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
 
@@ -184,6 +184,7 @@ wl-skills-kit/ ← 你正看的这个仓库
184
184
  │ │ ├── _compat/ 多 AI 编辑器适配(配置 + headers)
185
185
  │ │ ├── core/ 核心通用 Skill
186
186
  │ │ │ ├── prototype-scan/ { SKILL.md, USAGE.md }
187
+ │ │ │ ├── spec-doc-parse/ { SKILL.md, USAGE.md }
187
188
  │ │ │ ├── api-contract/ { SKILL.md, USAGE.md }
188
189
  │ │ │ ├── page-codegen/ { SKILL.md, USAGE.md, templates/ }
189
190
  │ │ │ ├── convention-audit/ { SKILL.md, USAGE.md }
@@ -329,7 +330,8 @@ npx @agile-team/wl-skills-kit update
329
330
 
330
331
  | Skill | 状态 | 路径 | 核心用途 |
331
332
  | ------------------ | ------- | ------------------------------- | ------------------------------------------- |
332
- | `prototype-scan` | ✅ 启用 | `skills/core/prototype-scan/` | 原型/详设/口述 → 页面清单 |
333
+ | `prototype-scan` | ✅ 启用 | `skills/core/prototype-scan/` | 原型线:Axure/截图/口述/非规范详设 → 页面清单 |
334
+ | `spec-doc-parse` | ✅ 启用 | `skills/core/spec-doc-parse/` | 规范线:wl-skills-design 标准说明书 → 页面清单 |
333
335
  | `api-contract` | ✅ 启用 | `skills/core/api-contract/` | 生成 api.md 前后端契约 |
334
336
  | `page-codegen` | ✅ 启用 | `skills/core/page-codegen/` | 页面骨架生成 + 模板调度 |
335
337
  | `convention-audit` | ✅ 启用 | `skills/core/convention-audit/` | 13 条规范扫描 + 双报告 |
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.9.4
4
+ * wl-skills-kit CLI v2.10.1
5
5
  *
6
6
  * 命令:
7
7
  * init 全量安装(默认,向后兼容)
@@ -22,6 +22,13 @@ const fs = require("fs");
22
22
  const path = require("path");
23
23
  const crypto = require("crypto");
24
24
 
25
+ // ─── AST 规则引擎(v2.10.1+,语义级约束检测)──────────────────────────
26
+ const {
27
+ runAstRules,
28
+ getStagedFiles,
29
+ hasAstAvailable,
30
+ } = require("../lib/ast-rules");
31
+
25
32
  const FILES_DIR = path.resolve(__dirname, "..", "files");
26
33
  const TARGET_DIR = process.cwd();
27
34
  const MANIFEST_NAME = ".wl-skills-manifest.json";
@@ -50,12 +57,16 @@ const KNOWN_FLAGS = new Set([
50
57
  "-h",
51
58
  "--domain",
52
59
  "--all",
60
+ "--pre-commit",
61
+ "--strict",
53
62
  ]);
54
63
 
55
64
  const dryRun = args.includes("--dry-run");
56
65
  const showHelp = args.includes("--help") || args.includes("-h");
57
66
  const keepReports = args.includes("--keep-reports");
58
67
  const force = args.includes("--force");
68
+ const preCommit = args.includes("--pre-commit");
69
+ const strict = args.includes("--strict");
59
70
 
60
71
  // 校验所有 flag 是否已知(--help 优先,跳过校验直接显示帮助)
61
72
  if (!showHelp) {
@@ -100,6 +111,7 @@ if (showHelp) {
100
111
  check 环境预检(Node / 工具链 / MCP 配置 / manifest)
101
112
  diff 对比已安装文件与当前 kit 版本的差异
102
113
  validate 静态检查 src/views 页面文件、AGGrid、skills-ui runtime、mock
114
+ v2.10.1+ 集成 AST 语义级检测(R1~R7),覆盖正则无法检测的规则
103
115
  validate-page validate 的别名,适用于单页/目录检查
104
116
  doctor-ui 检查 @agile-team/wk-skills-ui 接入完整性
105
117
  export 导出 reports/SYS_* 数据为 xlsx
@@ -111,6 +123,8 @@ if (showHelp) {
111
123
  --force 强制执行,跳过同版本检测(忽略已安装状态)
112
124
  --domain <name> mock-clean 指定要清理的业务域(如 sale、mdata)
113
125
  --all mock-clean 清理全部 mock(保留 _utils.ts)
126
+ --pre-commit validate 仅检测 git staged 文件,error 阻断提交,warn 仅提示
127
+ --strict validate 的 error 和 warn 都导致退出码 1(CI 用)
114
128
  --help 显示帮助
115
129
 
116
130
  示例:
@@ -341,6 +355,13 @@ function runInstall(incremental) {
341
355
 
342
356
  const oldManifest = readManifest();
343
357
 
358
+ // ── 约束基础设施:无论版本是否相同,都确保 pre-commit hook 和 eslint 配置就绪 ──
359
+ // 这样即使 early-return(同版本跳过文件复制),hook 也会被创建/更新
360
+ if (!dryRun) {
361
+ ensurePreCommitHook(TARGET_DIR);
362
+ ensureEslintConfig(TARGET_DIR);
363
+ }
364
+
344
365
  // ── 版本去重:同版本跳过,不同版本自动增量更新 ──────────────────────
345
366
  if (oldManifest && !force) {
346
367
  if (oldManifest.version === PKG.version) {
@@ -374,6 +395,9 @@ function runInstall(incremental) {
374
395
  if (dryRun) console.log(" [Step 1] files/ 静态文件:\n");
375
396
 
376
397
  for (const relPath of files) {
398
+ // eslint 模板由 ensureEslintConfig 单独处理,不通过 Step 1 复制
399
+ if (relPath === "eslint.config.wl-skills.cjs") continue;
400
+
377
401
  const src = path.join(FILES_DIR, relPath);
378
402
  const dest = path.join(TARGET_DIR, relPath);
379
403
  const srcHash = fileMd5(src);
@@ -551,10 +575,126 @@ function runInstall(incremental) {
551
575
  " ℹ 规范插件:建议执行 npx @robot-admin/git-standards init 接入代码质量与提交规范。",
552
576
  );
553
577
  console.log("");
578
+
554
579
  }
555
580
 
556
581
  // ─── 命令: clean ────────────────────────────────────────────────────────
557
582
 
583
+ /**
584
+ * 确保 .husky/pre-commit 包含 wl-skills validate --pre-commit
585
+ * — 这是让 AI 生成的代码"绕不开"规范的核心拦截点
586
+ *
587
+ * 策略:
588
+ * 1. 如果 .husky/pre-commit 不存在 → 创建(包含 validate 调用)
589
+ * 2. 如果存在但不含 wl-skills → 追加(不破坏用户已有的 hook 内容)
590
+ * 3. 如果存在且已含但格式过旧 → 刷新为最新格式
591
+ * 4. 如果存在且格式最新 → 跳过
592
+ *
593
+ * hook 使用 npx 动态解析,避免硬编码 node_modules 路径在 pnpm 下失效。
594
+ * 包含存在性守卫:kit 未安装时优雅跳过,不阻断提交。
595
+ */
596
+ function ensurePreCommitHook(targetDir) {
597
+ const huskyDir = path.join(targetDir, ".husky");
598
+ const preCommitPath = path.join(huskyDir, ".husky/pre-commit");
599
+
600
+ // 只有 git 仓库才创建 husky hook
601
+ if (!fs.existsSync(path.join(targetDir, ".git"))) return;
602
+ if (!fs.existsSync(huskyDir)) return;
603
+
604
+ const VALIDATE_MARKER = "wl-skills validate --pre-commit";
605
+ // 最新 hook 版本标记,用于检测旧格式并刷新
606
+ const HOOK_VERSION_TAG = "# wl-skills-hook-v2";
607
+
608
+ const hookContent =
609
+ "#!/usr/bin/env sh\n" +
610
+ HOOK_VERSION_TAG + "\n" +
611
+ "# wl-skills-kit 自动管理:提交前规范检测(error 阻断提交)\n" +
612
+ "# 如果 node_modules 不存在或 kit 未安装,优雅跳过,不阻断提交\n" +
613
+ 'if [ -f "node_modules/@agile-team/wl-skills-kit/bin/wl-skills.js" ]; then\n' +
614
+ ' node node_modules/@agile-team/wl-skills-kit/bin/wl-skills.js validate --pre-commit\n' +
615
+ " if [ $? -ne 0 ]; then\n" +
616
+ ' echo ""\n' +
617
+ ' echo " ✖ 规范检测未通过,提交已阻断。修复后重新 git add + git commit"\n' +
618
+ " exit 1\n" +
619
+ " fi\n" +
620
+ "else\n" +
621
+ ' echo " ⚠ wl-skills-kit 未安装(node_modules 中未找到),跳过提交前检测"\n' +
622
+ "fi\n";
623
+
624
+ const preCommitFile = path.join(huskyDir, "pre-commit");
625
+
626
+ if (!fs.existsSync(preCommitFile)) {
627
+ fs.writeFileSync(preCommitFile, hookContent, "utf8");
628
+ try { fs.chmodSync(preCommitFile, 0o755); } catch {}
629
+ console.log(" ✔ 已创建 .husky/pre-commit(提交前自动运行 wl-skills validate)");
630
+ console.log(" → 每次 git commit 时自动检测页面规范,error 级别阻断提交");
631
+ console.log(" → kit 未安装时自动跳过,不阻断提交");
632
+ console.log("");
633
+ } else {
634
+ const existing = fs.readFileSync(preCommitFile, "utf8");
635
+
636
+ // 已有最新版本标记 → 跳过
637
+ if (existing.includes(HOOK_VERSION_TAG)) return;
638
+
639
+ // 有旧 marker 但格式过旧 → 替换整段 wl-skills 块为最新格式
640
+ if (existing.includes(VALIDATE_MARKER)) {
641
+ // 删除旧的 wl-skills 块(从 VALIDATE_MARKER 前的注释行到对应的 fi)
642
+ const lines = existing.split("\n");
643
+ const filtered = [];
644
+ let skipMode = false;
645
+ for (const line of lines) {
646
+ if (line.includes(VALIDATE_MARKER) || line.includes("wl-skills-kit 自动")) {
647
+ skipMode = true;
648
+ continue;
649
+ }
650
+ if (skipMode && (line.includes("exit 1") || line.trim() === "fi")) {
651
+ skipMode = false;
652
+ continue;
653
+ }
654
+ if (!skipMode) filtered.push(line);
655
+ }
656
+ // 去尾部空行
657
+ while (filtered.length > 0 && filtered[filtered.length - 1].trim() === "") {
658
+ filtered.pop();
659
+ }
660
+ // 追加最新格式
661
+ const updated = filtered.join("\n").trimEnd() + "\n\n" + hookContent.replace("#!/usr/bin/env sh\n", "");
662
+ fs.writeFileSync(preCommitFile, updated, "utf8");
663
+ try { fs.chmodSync(preCommitFile, 0o755); } catch {}
664
+ console.log(" ✔ 已刷新 .husky/pre-commit 为最新格式(v2,含存在性守卫)");
665
+ console.log("");
666
+ return;
667
+ }
668
+
669
+ // 无 marker → 追加
670
+ const addition = "\n" + hookContent.replace("#!/usr/bin/env sh\n", "");
671
+ fs.writeFileSync(preCommitFile, existing.trimEnd() + "\n" + addition, "utf8");
672
+ try { fs.chmodSync(preCommitFile, 0o755); } catch {}
673
+ console.log(" ✔ 已在 .husky/pre-commit 追加 wl-skills validate(提交前规范检测)");
674
+ console.log(" → kit 未安装时自动跳过,不阻断提交");
675
+ console.log("");
676
+ }
677
+ }
678
+
679
+ /**
680
+ * 确保业务项目有 ESLint 配置
681
+ * 策略:如果项目根目录没有 eslint.config.cjs,从 kit 复制模板
682
+ * 如果已有,不覆盖(尊重用户自定义配置)
683
+ */
684
+ function ensureEslintConfig(targetDir) {
685
+ const targetEslint = path.join(targetDir, "eslint.config.cjs");
686
+ if (fs.existsSync(targetEslint)) return; // 用户已有自定义配置
687
+
688
+ const templatePath = path.join(FILES_DIR, "eslint.config.wl-skills.cjs");
689
+ if (!fs.existsSync(templatePath)) return;
690
+
691
+ const content = fs.readFileSync(templatePath, "utf8");
692
+ fs.writeFileSync(targetEslint, content, "utf8");
693
+ console.log(" ✔ 已创建 eslint.config.cjs(wl-skills-kit 模板)");
694
+ console.log(" → 安装依赖后生效:pnpm add -D eslint eslint-plugin-vue vue-eslint-parser @typescript-eslint/parser @typescript-eslint/eslint-plugin");
695
+ console.log("");
696
+ }
697
+
558
698
  function runClean() {
559
699
  console.log("");
560
700
  console.log(" wl-skills-kit v" + PKG.version + " [clean]");
@@ -665,14 +805,36 @@ function runCheck() {
665
805
  const nodeMajor = Number(process.versions.node.split(".")[0]);
666
806
  add("Node 版本", nodeMajor >= 16, process.versions.node + "(要求 >=16)");
667
807
 
668
- const toolFiles = [".prettierrc.js", "eslint.config.ts", ".husky"];
669
- for (const rel of toolFiles) {
670
- add(
671
- rel,
672
- fs.existsSync(path.join(TARGET_DIR, rel)),
673
- fs.existsSync(path.join(TARGET_DIR, rel)) ? "存在" : "缺失",
674
- );
808
+ // 工具链检测:支持多种可能的文件名
809
+ const prettierExists =
810
+ fs.existsSync(path.join(TARGET_DIR, ".prettierrc.js")) ||
811
+ fs.existsSync(path.join(TARGET_DIR, ".prettierrc")) ||
812
+ fs.existsSync(path.join(TARGET_DIR, ".prettierrc.cjs"));
813
+ add(".prettierrc", prettierExists, prettierExists ? "存在" : "缺失");
814
+
815
+ const eslintExists =
816
+ fs.existsSync(path.join(TARGET_DIR, "eslint.config.ts")) ||
817
+ fs.existsSync(path.join(TARGET_DIR, "eslint.config.mjs")) ||
818
+ fs.existsSync(path.join(TARGET_DIR, "eslint.config.cjs")) ||
819
+ fs.existsSync(path.join(TARGET_DIR, "eslint.config.js"));
820
+ add("eslint.config", eslintExists, eslintExists ? "存在" : "缺失");
821
+
822
+ // husky 目录检测
823
+ const huskyExists = fs.existsSync(path.join(TARGET_DIR, ".husky"));
824
+ add(".husky", huskyExists, huskyExists ? "存在" : "缺失");
825
+
826
+ // pre-commit hook 内容检测(不只检查目录存在)
827
+ const preCommitPath = path.join(TARGET_DIR, ".husky", "pre-commit");
828
+ let preCommitHasValidate = false;
829
+ if (fs.existsSync(preCommitPath)) {
830
+ const hookContent = fs.readFileSync(preCommitPath, "utf8");
831
+ preCommitHasValidate = hookContent.includes("wl-skills validate --pre-commit");
675
832
  }
833
+ add(
834
+ ".husky/pre-commit (wl-skills validate)",
835
+ preCommitHasValidate,
836
+ preCommitHasValidate ? "已配置规范检测" : huskyExists ? "存在但未配置 wl-skills validate" : "不存在",
837
+ );
676
838
 
677
839
  const manifest = readManifest();
678
840
  add(
@@ -859,10 +1021,37 @@ function findMockFiles() {
859
1021
  function runValidate() {
860
1022
  const scanPath =
861
1023
  args.find((a) => !a.startsWith("-") && a !== command) || "src/views";
862
- const pages = scanPageDirs(scanPath);
1024
+
1025
+ // --pre-commit 模式:获取 staged 文件列表,用于过滤
1026
+ let stagedSet = null;
1027
+ if (preCommit) {
1028
+ const staged = getStagedFiles(TARGET_DIR);
1029
+ if (staged.length === 0) {
1030
+ console.log("");
1031
+ console.log(" wl-skills-kit v" + PKG.version + " [validate --pre-commit]");
1032
+ console.log(" ⚠ 无 staged 的 .vue/.ts 文件,跳过检测");
1033
+ console.log("");
1034
+ return;
1035
+ }
1036
+ stagedSet = new Set(staged.map((f) => f.replace(/\\/g, "/")));
1037
+ }
1038
+
1039
+ const allPages = scanPageDirs(scanPath);
1040
+ // 在 pre-commit 模式下,只保留包含 staged 文件的页面目录
1041
+ const pages = preCommit
1042
+ ? allPages.filter((page) =>
1043
+ Array.from(stagedSet).some(
1044
+ (f) =>
1045
+ f.startsWith(page.dir + "/") ||
1046
+ f === page.dir + "/index.vue" ||
1047
+ f === page.dir + "/data.ts",
1048
+ ),
1049
+ )
1050
+ : allPages;
1051
+
863
1052
  console.log("");
864
- console.log(" wl-skills-kit v" + PKG.version + " [" + command + "]");
865
- console.log(" 扫描目录: " + scanPath);
1053
+ console.log(" wl-skills-kit v" + PKG.version + " [" + command + "]" + (preCommit ? " [pre-commit]" : ""));
1054
+ console.log(" 扫描目录: " + scanPath + (preCommit ? "(仅 staged 文件)" : ""));
866
1055
  console.log("");
867
1056
 
868
1057
  if (pages.length === 0) {
@@ -1021,18 +1210,185 @@ function runValidate() {
1021
1210
  }
1022
1211
  }
1023
1212
 
1024
- console.log(" 页面目录: " + pages.length);
1213
+ // ── AST 语义级规则检测(v2.10.1+)─────────────────────────────────
1214
+ // 补充正则无法覆盖的 7 条语义规则(R1~R7),与正则规则合并输出
1215
+ // 在 pre-commit 模式下复用上面已计算的 stagedSet
1216
+ const astStagedFiles = preCommit && stagedSet ? Array.from(stagedSet) : undefined;
1217
+ const astResult = runAstRules(TARGET_DIR, scanPath, {
1218
+ stagedFiles: astStagedFiles,
1219
+ });
1220
+ // 合并 AST 结果(降级和正常都 push)
1221
+ issues.push(...astResult.issues);
1222
+
1223
+ // ── 输出 ───────────────────────────────────────────────────────────
1224
+ console.log(" 页面目录: " + pages.length + (astResult.pages ? "(AST 扫描 " + astResult.pages + ")" : ""));
1025
1225
  console.log(" 提示项: " + issues.length);
1026
1226
  console.log("");
1027
1227
  const errors = issues.filter((issue) => issue.level === "error").length;
1228
+ const warns = issues.filter(
1229
+ (issue) => issue.level === "warn" || issue.level === undefined,
1230
+ ).length;
1028
1231
  for (const issue of issues) {
1029
1232
  const icon =
1030
1233
  issue.level === "error" ? "✖" : issue.level === "info" ? "ℹ" : "⚠";
1031
1234
  console.log(" " + icon + " " + issue.dir + " — " + issue.text);
1032
1235
  }
1033
- if (issues.length === 0) console.log(" 页面文件完整性检查通过");
1236
+ if (issues.length === 0) console.log(" \u2714 \u9875\u9762\u6587\u4ef6\u5b8c\u6574\u6027\u68c0\u67e5\u901a\u8fc7");
1034
1237
  console.log("");
1035
- if (errors > 0 || issues.length > 0) process.exitCode = 1;
1238
+
1239
+ // ── \u4fee\u590d\u5efa\u8bae\u8f93\u51fa\uff08P0 \u6539\u8fdb\uff1a\u963b\u65ad\u65f6\u544a\u8bc9\u5f00\u53d1\u8005\u600e\u4e48\u4fee\uff09─────────────────────
1240
+ const blockingIssues = issues.filter((i) => i.level === "error" || (strict && i.level === "warn"));
1241
+ if (blockingIssues.length > 0) {
1242
+ printFixSuggestions(blockingIssues);
1243
+ }
1244
+
1245
+ if (preCommit) {
1246
+ // pre-commit \u6a21\u5f0f\uff1aerror \u963b\u65ad\u63d0\u4ea4
1247
+ // --pre-commit --strict \u7ec4\u5408\uff1aerror + warn \u90fd\u963b\u65ad
1248
+ const failCount = strict ? errors + warns : errors;
1249
+ if (failCount > 0) {
1250
+ console.log(
1251
+ " \u2716 pre-commit \u68c0\u67e5\u53d1\u73b0 " +
1252
+ errors + " \u4e2a error" +
1253
+ (strict && warns > 0 ? " + " + warns + " \u4e2a warn\uff08strict \u6a21\u5f0f\uff09" : "") +
1254
+ "\uff0c\u63d0\u4ea4\u5df2\u963b\u65ad",
1255
+ );
1256
+ console.log(" \u2192 \u8bf7\u4fee\u590d\u540e\u91cd\u65b0 git add + git commit");
1257
+ console.log(" \u2192 \u5982\u9700 AI \u8f85\u52a9\u4fee\u590d\uff0c\u8bf7\u89e6\u53d1\uff1a\u89c4\u8303\u5ba1\u8ba1 \u2192 \u81ea\u52a8\u4fee\u590d \u2192 \u590d\u626b\u9a8c\u8bc1");
1258
+ console.log("");
1259
+ process.exitCode = 1;
1260
+ } else {
1261
+ console.log(" \u2714 pre-commit \u68c0\u67e5\u901a\u8fc7\uff08" + issues.length + " \u4e2a\u63d0\u793a\u9879\u4e0d\u963b\u65ad\u63d0\u4ea4\uff09");
1262
+ console.log("");
1263
+ }
1264
+ } else if (strict) {
1265
+ // --strict \u6a21\u5f0f\uff08CI \u7528\uff09\uff1aerror \u548c warn \u5bfc\u81f4\u5931\u8d25\uff0cinfo \u4e0d\u8ba1\u5165
1266
+ if (errors > 0 || warns > 0) {
1267
+ console.log(
1268
+ " \u2716 strict \u6a21\u5f0f\u68c0\u67e5\u53d1\u73b0 " + errors + " error / " + warns + " warn\uff0cCI \u5df2\u963b\u65ad",
1269
+ );
1270
+ console.log(" \u2192 --strict \u6a21\u5f0f\u4e0b warn \u4e5f\u4f1a\u5931\u8d25\uff0c\u8bf7\u4fee\u590d");
1271
+ process.exitCode = 1;
1272
+ } else {
1273
+ console.log(" \u2714 strict \u6a21\u5f0f\u68c0\u67e5\u5168\u90e8\u901a\u8fc7");
1274
+ }
1275
+ console.log("");
1276
+ } else {
1277
+ // \u666e\u901a\u6a21\u5f0f\uff1a\u53ea\u6709 error \u6216 warn \u624d exit 1\uff0cinfo \u4ec5\u63d0\u793a
1278
+ if (errors > 0 || warns > 0) process.exitCode = 1;
1279
+ }
1280
+ }
1281
+
1282
+ // ── \u4fee\u590d\u5efa\u8bae\u6620\u5c04\u8868\uff08P0\uff1a\u8ba9\u5f00\u53d1\u8005\u77e5\u9053\u600e\u4e48\u4fee\uff09──────────────────────────────
1283
+ const FIX_SUGGESTIONS = {
1284
+ // \u6b63\u5219\u7ea7\u68c0\u67e5
1285
+ 'render-type="agGrid"': {
1286
+ fix: '<BaseTable render-type="agGrid" ...>',
1287
+ ref: 'standards/12-base-table.md',
1288
+ auto: true,
1289
+ },
1290
+ 'cid / :cid': {
1291
+ fix: '\u7ed9 BaseTable \u52a0 cid="{\u6a21\u5757\u7f29\u5199}-{\u529f\u80fd}"\uff0c\u5168\u5c40\u552f\u4e00',
1292
+ ref: 'standards/12-base-table.md',
1293
+ auto: true,
1294
+ },
1295
+ 'defineColumns()': {
1296
+ fix: 'import { defineColumns } from "@agile-team/wk-skills-ui/runtime" \u5e76\u7528\u4e8e\u5217\u5b9a\u4e49',
1297
+ ref: 'standards/12-base-table.md',
1298
+ auto: true,
1299
+ },
1300
+ 'renderOps()': {
1301
+ fix: '\u64cd\u4f5c\u5217\u4f7f\u7528 defaultSlot + renderOps()\uff0c\u7981\u6b62 operations \u6570\u7ec4',
1302
+ ref: 'standards/12-base-table.md',
1303
+ auto: true,
1304
+ },
1305
+ 'C_Splitter': {
1306
+ fix: '\u66ff\u6362\u4e3a jh-drag-col\uff08\u5de6\u53f3\uff09/ jh-drag-row\uff08\u4e0a\u4e0b\uff09',
1307
+ ref: 'standards/14-layout-containers.md',
1308
+ auto: true,
1309
+ },
1310
+ 'onClick: () => {}': {
1311
+ fix: '\u586b\u5145\u5b9e\u9645\u4e8b\u4ef6\u5904\u7406\u903b\u8f91\uff0c\u6216\u8054\u52a8 code-fix \u81ea\u52a8\u4fee\u590d',
1312
+ ref: 'standards/04-coding-basics.md',
1313
+ auto: true,
1314
+ },
1315
+ };
1316
+
1317
+ const AST_FIX_SUGGESTIONS = {
1318
+ R1: { fix: '\u5c06\u4e1a\u52a1\u903b\u8f91\u8fc1\u79fb\u5230 data.ts\uff0cindex.vue \u53ea\u4fdd\u7559\u6a21\u677f+\u89e3\u6784', ref: 'standards/02-code-structure.md', auto: false },
1319
+ R2: { fix: '\u5c06 getAction/postAction/sessionStorage \u79fb\u5230 data.ts \u4e2d\u8c03\u7528', ref: 'standards/02-code-structure.md', auto: true },
1320
+ R3: { fix: '\u66ff\u6362 <el-table> \u4e3a <BaseTable render-type="agGrid" :cid="xxx">', ref: 'standards/12-base-table.md', auto: true },
1321
+ R4: { fix: '\u4fee\u6539\u91cd\u590d cid \u4e3a\u5168\u5c40\u552f\u4e00\u503c\uff08\u683c\u5f0f: {\u6a21\u5757\u7f29\u5199}-{\u529f\u80fd}\uff09', ref: 'standards/12-base-table.md', auto: true },
1322
+ R5: { fix: 'data.ts \u4e2d class extends AbstractPageQueryHook\uff0c\u5b9e\u73b0 queryDef/columnsDef', ref: 'standards/02-code-structure.md', auto: false },
1323
+ R6: { fix: '\u5220\u9664 import axios\uff0c\u6539\u7528 getAction/postAction', ref: 'standards/06-security.md', auto: true },
1324
+ R7: { fix: '\u5220\u9664 eval/new Function\uff0c\u7528\u5b89\u5168\u7684\u66ff\u4ee3\u65b9\u6848', ref: 'standards/06-security.md', auto: false },
1325
+ R8: { fix: '\u521b\u5efa data.ts\uff0c\u5c06\u63a5\u53e3\u8c03\u7528\u548c\u4e1a\u52a1\u903b\u8f91\u79fb\u5165\uff1b\u786e\u4fdd index.vue \u65e0 API \u8c03\u7528', ref: 'standards/02-code-structure.md', auto: true },
1326
+ R9: { fix: '\u66f4\u65b0 api.md\uff0c\u786e\u4fdd URL \u4e0e data.ts API_CONFIG \u4e00\u81f4', ref: 'standards/02-code-structure.md', auto: true },
1327
+ R10: { fix: '\u66ff\u6362\u539f\u751f el-* \u7ec4\u4ef6\u4e3a\u5e73\u53f0\u5c01\u88c5\uff08jh-select/jh-date/jh-pagination \u7b49\uff09', ref: 'standards/13-platform-components.md', auto: true },
1328
+ R11: { fix: '\u4ece data.ts \u4e2d\u79fb\u9664 Pinia Store import\uff0cStore \u5e94\u5728\u7ec4\u4ef6\u5c42\u4f7f\u7528', ref: 'standards/10-pinia.md', auto: true },
1329
+ R12: { fix: '\u5c06\u786c\u7f16\u7801 IP/URL \u79fb\u81f3 .env.* \u73af\u5883\u53d8\u91cf', ref: 'standards/07-config.md', auto: true },
1330
+ };
1331
+
1332
+ function printFixSuggestions(blockingIssues) {
1333
+ // \u6309\u89c4\u5219\u5206\u7ec4\u53bb\u91cd
1334
+ const ruleGroups = new Map();
1335
+ for (const issue of blockingIssues) {
1336
+ const key = issue.rule || guessRuleFromText(issue.text);
1337
+ if (!key) continue;
1338
+ if (!ruleGroups.has(key)) ruleGroups.set(key, []);
1339
+ ruleGroups.get(key).push(issue);
1340
+ }
1341
+
1342
+ if (ruleGroups.size === 0) return;
1343
+
1344
+ console.log(" \u250c\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\u2510");
1345
+ console.log(" \u2502 \ud83d\udd27 \u4fee\u590d\u5efa\u8bae \u2502");
1346
+ 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");
1347
+
1348
+ let hasAutoFix = false;
1349
+ let hasUnknownIssue = false;
1350
+ for (const [rule, ruleIssues] of ruleGroups.entries()) {
1351
+ const suggestion = AST_FIX_SUGGESTIONS[rule] || findRegexSuggestion(ruleIssues[0].text);
1352
+ const count = ruleIssues.length;
1353
+ if (!suggestion) {
1354
+ // 免底:未知规则的阻断项也要展示,避免用户看不到任何提示
1355
+ hasUnknownIssue = true;
1356
+ console.log(" \u2502 " + rule + "\uff08" + count + " \u5904\uff09 [\u2753\u672a\u77e5\u89c4\u5219]");
1357
+ console.log(" \u2502 \u2192 \u8bf7\u67e5\u770b .github/standards/ \u76f8\u5173\u89c4\u8303\u6216\u89e6\u53d1\u89c4\u8303\u5ba1\u8ba1");
1358
+ console.log(" \u2502");
1359
+ continue;
1360
+ }
1361
+ const autoTag = suggestion.auto ? " [\u2705\u53ef\u81ea\u52a8\u4fee]" : " [\u270b\u9700\u4eba\u5de5]";
1362
+ if (suggestion.auto) hasAutoFix = true;
1363
+ console.log(" \u2502 " + rule + "\uff08" + count + " \u5904\uff09" + autoTag);
1364
+ console.log(" \u2502 \u2192 " + suggestion.fix);
1365
+ console.log(" \u2502 \u53c2\u8003: .github/" + suggestion.ref);
1366
+ console.log(" \u2502");
1367
+ }
1368
+
1369
+ 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");
1370
+ if (hasAutoFix) {
1371
+ console.log(" \u2502 \ud83d\ude80 \u5feb\u901f\u4fee\u590d\uff1a\u5728 AI \u7f16\u8f91\u5668\u4e2d\u8f93\u5165\uff1a \u2502");
1372
+ console.log(" \u2502 \"\u89c4\u8303\u5ba1\u8ba1\" \u2192 \"\u81ea\u52a8\u4fee\u590d\" \u2192 \"\u590d\u626b\u9a8c\u8bc1\" \u2502");
1373
+ } else {
1374
+ console.log(" \u2502 \ud83d\udcdd \u8bf7\u53c2\u7167\u4e0a\u8ff0\u89c4\u8303\u6587\u6863\u624b\u52a8\u4fee\u590d \u2502");
1375
+ }
1376
+ console.log(" \u2514\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\u2518");
1377
+ console.log("");
1378
+ }
1379
+
1380
+ function guessRuleFromText(text) {
1381
+ for (const key of Object.keys(FIX_SUGGESTIONS)) {
1382
+ if (text.includes(key)) return key;
1383
+ }
1384
+ return null;
1385
+ }
1386
+
1387
+ function findRegexSuggestion(text) {
1388
+ for (const [key, suggestion] of Object.entries(FIX_SUGGESTIONS)) {
1389
+ if (text.includes(key)) return suggestion;
1390
+ }
1391
+ return null;
1036
1392
  }
1037
1393
 
1038
1394
  function readJsonSafe(filePath) {
@@ -266,6 +266,7 @@ onMounted(() => select());
266
266
  | 用户自然表达 | 自动触发 |
267
267
  | ------------ | -------- |
268
268
  | 生成页面 / 做个页面 / 列表页 / 管理页 / 台账 / 根据原型 / 根据截图 / 补页面 | `page-codegen`,必要时先 `prototype-scan` + `api-contract` |
269
+ | 解析说明书 / 规范文档转页面 / 根据说明书生成 / IPO 转页面 / docs/spec 路径 / 功能编码(PMMB001) | `spec-doc-parse`(**规范线**,wl-skills-design 标准说明书专属,禁止 prototype-scan 接管) |
269
270
  | mock / 假数据 / 后端没好 / 先能跑 / 联调前 | `page-codegen` 的 mock-first 规则 |
270
271
  | 菜单 / 注册页面 / 点击进不来 / 同步菜单 / 补菜单 | `menu-sync` + `route-check` |
271
272
  | 风格 / 样式不生效 / skills-ui / 操作列 / 状态标签 / AGGrid | `page-codegen` + `wk-skills-ui runtime` + `doctor-ui` |
@@ -372,6 +373,103 @@ AI 生成的所有报告类文件统一写入 `.github/reports/`,**全部追
372
373
 
373
374
  ---
374
375
 
376
+ ## 小修改 / 零散变更的约束(同样不可绕过)
377
+
378
+ > 即使不是"生成新页面",仅做小修改(加字段、改样式、修 bug、加按钮)也必须遵守:
379
+
380
+ | 规则 | 说明 |
381
+ |------|------|
382
+ | ❌ 不在 index.vue 写业务逻辑 | 新加的方法/状态必须放在 data.ts |
383
+ | ❌ 不直接用 axios / getAction / sessionStorage | 这些只能出现在 data.ts |
384
+ | ❌ 不用 el-table / el-form 替代平台组件 | 用 BaseTable / BaseForm |
385
+ | ❌ 不引入新依赖不经团队确认 | 需在 PR 中说明理由 |
386
+ | ✅ 改完即验证 | 触发 `wls_validate_page` 或 `wl-skills validate` |
387
+ | ✅ git cz 提交 | pre-commit 会自动检测规范,error 级别阻断提交 |
388
+
389
+ > **提交即验证**:`.husky/pre-commit` 会自动运行 `wl-skills validate --pre-commit`,检测 staged 文件中的规范偏差。error 阻断提交,warn 仅提示。这是**机器强制**,不是建议。
390
+ >
391
+ > **CI 兜底**:即使 `git commit --no-verify` 绕过 pre-commit,CI 流水线中的 `wl-skills validate --strict`(正则 + AST 全量检测)会再次拦截,error 和 warn 都导致 CI 失败、阻断 PR 合并。这是不可绕开的最后一道防线。
392
+
393
+ ### 豁免标记(特殊场景)
394
+
395
+ 极少数场景下确实需要绕过某条规则(如弹窗内确实需要原生 `<el-table>`)时,在文件中添加精确豁免标记:
396
+
397
+ ```vue
398
+ <!-- wl-skills:ignore R3 -->
399
+ <el-table :data="dialogData">...</el-table>
400
+ ```
401
+
402
+ 或在 `<script>` 中:
403
+
404
+ ```typescript
405
+ // wl-skills:ignore R2
406
+ const data = sessionStorage.getItem("key");
407
+ ```
408
+
409
+ **规则**:
410
+ - 必须带规则编号(R1~R9),精确豁免,不支持全局豁免
411
+ - 标记放在被豁免的代码附近,便于 review
412
+ - CI `--strict` 模式下豁免标记仍然生效(豁免不是违规)
413
+
414
+ ---
415
+
416
+ ## AI 执行护栏(强制约定)
417
+
418
+ 以下规则对所有 AI 助手(Copilot / Claude / GPT 等)在本项目中执行任务时**强制生效**,不可被用户口头覆盖。
419
+
420
+ ### 1. 闭环强制约定
421
+
422
+ | 触发场景 | 强制动作 | 不可跳过原因 |
423
+ |----------|----------|-------------|
424
+ | code-fix 执行完毕 | 自动执行 `wl-skills validate` 复扫 | 确保修复未引入新偏差 |
425
+ | 复扫发现新问题 | 继续修复 → 再次复扫,直到通过 | 闭环不允许断开 |
426
+ | 用户说“不用验证了” | **仍然执行**复扫,只是不再追问 | 规范高于口头指令 |
427
+
428
+ ### 2. 高风险 Skill 确认机制
429
+
430
+ 以下 Skill 触发前必须向用户**二次确认**,不可静默执行:
431
+
432
+ - `page-codegen`(生成整页代码,不可逆)
433
+ - `menu-sync` / `dict-sync` / `permission-sync`(跨系统同步,影响后端数据)
434
+ - `code-fix`(批量修改源码文件,需确认范围)
435
+
436
+ 确认话术模板:
437
+
438
+ ```
439
+ 即将执行 [Skill名称],影响范围:[文件列表 / 后端数据类型]
440
+ 是否继续?(Y/n)
441
+ ```
442
+
443
+ ### 3. 误触发防护
444
+
445
+ | 情况 | 处理方式 |
446
+ |------|----------|
447
+ | 用户意图匹配 2+ 个 Skill | 必须列出候选并询问用户意图 |
448
+ | 用户意图模糊无法映射到 Skill | 询问澄清,不猜测执行 |
449
+ | 仅匹配 1 个 Skill 且置信度高 | 直接执行,无需确认 |
450
+
451
+ ### 4. Pre-flight 声明完整性
452
+
453
+ AI 在执行任何 Skill 前必须输出 Pre-flight 声明:
454
+
455
+ ```
456
+ 📋 Pre-flight:
457
+ - Skill: [skill-name]
458
+ - 触发依据: [用户原话 / 管道上游输出]
459
+ - 影响文件: [文件列表]
460
+ - 预期结果: [一句话描述]
461
+ ```
462
+
463
+ ### 5. 修复建议输出规范
464
+
465
+ 当 `wl-skills validate` 阻断时,AI 必须:
466
+ 1. 完整展示阻断项(error + strict 模式下的 warn)
467
+ 2. 对每项给出**具体修复建议**(而非泛泛的“请修复”)
468
+ 3. 标注是否可自动修复(auto: true/false)
469
+ 4. 引导用户触发修复流程:`规范审计 → 自动修复 → 复扫验证`
470
+
471
+ ---
472
+
375
473
  > 📚 完整指南:`.github/guides/usage.md`
376
474
  > 🏗️ 架构设计:`.github/guides/architecture.md`
377
475
  > 🔧 维护者文档:`kit-internal/`(仓库内,不安装到业务项目)
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **读者**:团队技术负责人 / wl-skills-kit 维护者 / 对体系设计感兴趣的团队成员
4
4
  > **更新方式**:重大架构变更后追加对应章节,旧章节原文保留(历史可溯)
5
- > **当前版本**:v2.9.4(2026-05-18
5
+ > **当前版本**:v2.10.1(2026-06-14
6
6
 
7
7
  ---
8
8