@agile-team/wl-skills-kit 2.11.1 → 2.11.3

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +38 -21
  3. package/bin/wl-skills.js +27 -3
  4. package/files/.wl-skills/docs/jh-pagination.md +505 -505
  5. package/files/.wl-skills/docs/request.md +940 -940
  6. package/files/.wl-skills/docs/validate-exempt.md +113 -0
  7. package/files/.wl-skills/guides/architecture.md +1 -1
  8. package/files/.wl-skills/skills/_compat/headers/cursor-mdc.txt +1 -1
  9. package/files/.wl-skills/skills/_compat/headers/kiro.txt +1 -1
  10. package/files/.wl-skills/skills/_compat/headers/trae.txt +1 -1
  11. package/files/.wl-skills/skills/core/convention-audit/SKILL.md +3 -3
  12. package/files/.wl-skills/skills/core/spec-doc-parse/SKILL.md +332 -332
  13. package/files/.wl-skills/skills/core/spec-doc-parse/USAGE.md +97 -97
  14. package/files/.wl-skills/skills/sync/permission-sync/USAGE.md +107 -107
  15. package/files/.wl-skills/src/components/global/C_ParentView/index.vue +3 -3
  16. package/files/.wl-skills/src/components/global/C_RightToolbar/index.vue +157 -157
  17. package/files/.wl-skills/src/components/global/C_SvgIcon/index.vue +31 -31
  18. package/files/.wl-skills/src/components/global/C_SvgIcon/svgicon.js +10 -10
  19. package/files/.wl-skills/src/components/global/C_TagStatus/README.md +264 -264
  20. package/files/.wl-skills/src/components/global/C_TagStatus/config.ts +192 -192
  21. package/files/.wl-skills/src/components/global/C_TagStatus/index.vue +106 -106
  22. package/files/.wl-skills/src/components/global/C_TagStatus/types.ts +64 -64
  23. package/files/.wl-skills/src/components/global/C_Tree/README.md +153 -153
  24. package/files/.wl-skills/src/components/global/C_Tree/index.scss +42 -42
  25. package/files/.wl-skills/src/components/global/C_Tree/index.vue +78 -78
  26. package/files/.wl-skills/src/components/global/C_Tree/types.ts +59 -59
  27. package/files/.wl-skills/src/components/local/c_formModal/README.md +235 -235
  28. package/files/.wl-skills/src/components/local/c_formModal/data.ts +95 -95
  29. package/files/.wl-skills/src/components/local/c_formModal/index.scss +8 -8
  30. package/files/.wl-skills/src/components/local/c_formModal/index.vue +107 -107
  31. package/files/.wl-skills/src/components/local/c_formSections/data.ts +175 -175
  32. package/files/.wl-skills/src/components/local/c_formSections/index.scss +280 -280
  33. package/files/.wl-skills/src/components/local/c_formSections/index.vue +429 -429
  34. package/files/.wl-skills/src/components/local/c_listModal/data.ts +41 -41
  35. package/files/.wl-skills/src/components/local/c_listModal/index.vue +136 -136
  36. package/files/.wl-skills/src/components/local/c_spliterTitle/index.scss +25 -25
  37. package/files/.wl-skills/src/components/local/c_spliterTitle/index.vue +21 -21
  38. package/files/.wl-skills/src/components/remote/AGGrid/README.md +530 -530
  39. package/files/.wl-skills/src/components/remote/BaseForm/README.md +508 -508
  40. package/files/.wl-skills/src/components/remote/BaseQuery/README.md +865 -865
  41. package/files/.wl-skills/src/components/remote/BaseTable/README.md +941 -941
  42. package/files/.wl-skills/src/components/remote/BaseToolbar/README.md +496 -496
  43. package/files/.wl-skills/src/types/page.ts +24 -24
  44. package/files/.wl-skills/standards/04-coding-basics.md +39 -1
  45. package/files/.wl-skills/standards/09-typescript.md +26 -3
  46. package/files/.wl-skills/standards/12-base-table.md +56 -4
  47. package/files/.wl-skills/standards/13-platform-components.md +1 -0
  48. package/files/.wl-skills/standards/index.md +2 -2
  49. package/files/.wl-skills/templates/README.md +44 -44
  50. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/api.md +54 -54
  51. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/data.ts +346 -346
  52. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.scss +1 -1
  53. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.vue +28 -28
  54. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/data.ts +115 -115
  55. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.scss +44 -44
  56. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.vue +43 -43
  57. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/data.ts +338 -338
  58. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.scss +1 -1
  59. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.vue +28 -28
  60. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/data.ts +115 -115
  61. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.scss +44 -44
  62. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.vue +43 -43
  63. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/api.md +88 -88
  64. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/data.ts +601 -601
  65. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.scss +1 -1
  66. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.vue +64 -64
  67. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/api.md +67 -67
  68. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/data.ts +286 -286
  69. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.scss +139 -139
  70. package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.vue +318 -318
  71. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/api.md +98 -98
  72. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/data.ts +543 -543
  73. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.scss +1 -1
  74. package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.vue +52 -52
  75. package/files/.wl-skills/templates/sale/demo/add-demo/data.ts +518 -518
  76. package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/data.ts +524 -524
  77. package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.scss +154 -154
  78. package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.vue +117 -117
  79. package/files/.wl-skills/templates/sale/demo/domestic-trade-order/data.ts +308 -308
  80. package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.scss +99 -99
  81. package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.vue +77 -77
  82. package/files/.wl-skills/templates/sale/demo/heat-batch-return/data.ts +367 -367
  83. package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.scss +100 -100
  84. package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.vue +170 -170
  85. package/files/.wl-skills/templates/sale/demo/heat-batch-return/meltDialog.vue +320 -320
  86. package/files/.wl-skills/templates/sale/demo/metallurgical-spec/data.ts +824 -824
  87. package/lib/ast-rules.js +395 -12
  88. package/mcp/config.js +46 -46
  89. package/mcp/registry.js +6 -1
  90. package/mcp/tools/projectTools.js +9 -1
  91. package/package.json +2 -2
package/lib/ast-rules.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * 依赖:@vue/compiler-sfc(解析 .vue)、@babel/parser(解析 <script> AST)
8
8
  * 这两个包在 Vue 3 + Vite 项目中天然存在,用 try-require 做优雅降级。
9
9
  *
10
- * 规则编号 R1~R12 对应 standards 02/06/07/10/12/13 中的语义约束:
10
+ * 规则编号 R1~R14 对应 standards 02/04/06/07/09/10/12/13 中的语义约束:
11
11
  * R1: index.vue <script setup> 业务逻辑行数超阈值 → warn(02)
12
12
  * R2: index.vue 含禁止 import(getAction/postAction/sessionStorage 等)→ error(02/06)
13
13
  * R3: 页面用 <el-table> 但未用 <BaseTable> → error(12/13)
@@ -20,17 +20,24 @@
20
20
  * R10: 平台组件替换检测 — el-form/el-select/el-date-picker 等应替换为平台封装 → error(13)
21
21
  * R11: data.ts 禁止 import Pinia Store → error(10)
22
22
  * R12: 硬编码 IP/URL 检测 → error/warn(07)
23
+ * R13: 单函数圈复杂度 > 10(Mcabe,与 ESLint complexity 定义一致)→ error(04)
24
+ * R14: 文件类型错误零容忍 — vue-tsc/tsc --noEmit 产物解析 → error(09)
25
+ * 注:R14 为项目级检查,体积较大,validate 默认不跑,
26
+ * 需显式 --typecheck(CLI)/ typecheck:true(MCP)触发,优雅降级为 warn
23
27
  *
24
28
  * 导出函数:
25
29
  * runAstRules(targetDir, scanRel, { stagedFiles }) → { issues, pages }
26
30
  * parseVueScript(absPath) → { content, template, source } | null
27
31
  * countEffectiveLines(scriptContent) → number
32
+ * computeFunctionComplexity(fnNode) → number
33
+ * runTypeCheck(root) → { issues, ran, errorCount }
34
+ * loadExemptions(targetDir) → { isExempt, source, warnings }
28
35
  * hasAstAvailable() → boolean
29
36
  */
30
37
 
31
38
  const fs = require("fs");
32
39
  const path = require("path");
33
- const { execFileSync } = require("child_process");
40
+ const { execFileSync, spawnSync } = require("child_process");
34
41
 
35
42
  // ─── AST 依赖探测(优雅降级)──────────────────────────────────────────
36
43
  //
@@ -159,9 +166,91 @@ const CONFIG = {
159
166
  ],
160
167
  FORBIDDEN_GLOBALS: ["sessionStorage", "localStorage"],
161
168
  WARN_IMPORTS: ["useRoute"],
169
+ // R13: 单函数圈复杂度上限(Mcabe),与 ESLint complexity 规则阈值一致
170
+ MAX_CYCLOMATIC_COMPLEXITY: 10,
171
+ // R14: 单页类型错误采集上限(避免输出爆炸)
172
+ TYPECHECK_ERROR_CAP: 50,
173
+ // 项目级豁免配置文件名(业务项目根,kit 不主动创建,零功能影响)
174
+ EXEMPT_CONFIG_NAME: ".wl-skills-validate.json",
162
175
  SKIP_DIRS: ["node_modules", "dist", ".git", "demo", "template"],
163
176
  };
164
177
 
178
+ // ─── 项目级豁免配置(零功能影响,可选)──────────────────────────────────
179
+ //
180
+ // 业务项目根可放 .wl-skills-validate.json,对指定路径前缀批量豁免规则:
181
+ // {
182
+ // "exemptions": [
183
+ // {
184
+ // "paths": ["src/views/produce/designer"],
185
+ // "rules": ["R3", "R10"],
186
+ // "reason": "表单设计器内嵌表格,BaseTable AGGrid 内联编辑受限"
187
+ // }
188
+ // ]
189
+ // }
190
+ //
191
+ // 与单文件注释豁免(wl-skills:ignore R3)互补:注释精确到单文件,
192
+ // 配置批量到目录。无配置文件时返回空豁免,行为完全不变。
193
+
194
+ /**
195
+ * 加载项目级豁免配置
196
+ * @param {string} targetDir 项目根目录
197
+ * @returns {{ isExempt: (pageDir:string, rule:string)=>boolean, source: string|null, warnings: string[] }}
198
+ */
199
+ function loadExemptions(targetDir) {
200
+ const warnings = [];
201
+ const configPath = path.join(
202
+ targetDir || process.cwd(),
203
+ CONFIG.EXEMPT_CONFIG_NAME,
204
+ );
205
+ if (!fs.existsSync(configPath)) {
206
+ return { isExempt: () => false, source: null, warnings };
207
+ }
208
+ let raw;
209
+ try {
210
+ raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
211
+ } catch (e) {
212
+ warnings.push(
213
+ CONFIG.EXEMPT_CONFIG_NAME +
214
+ " 解析失败,已忽略(" +
215
+ ((e && e.message) || String(e)) +
216
+ ")",
217
+ );
218
+ return { isExempt: () => false, source: configPath, warnings };
219
+ }
220
+ const list = Array.isArray(raw.exemptions) ? raw.exemptions : [];
221
+ // 预编译:每个 path 规范化为前缀,每个 entry 的 rules 转大写 Set
222
+ const compiled = [];
223
+ for (const entry of list) {
224
+ if (!entry || !Array.isArray(entry.paths) || !Array.isArray(entry.rules)) {
225
+ continue;
226
+ }
227
+ const rules = new Set(
228
+ entry.rules.map((r) => String(r).toUpperCase()),
229
+ );
230
+ for (let p of entry.paths) {
231
+ p = String(p).replace(/\\/g, "/").replace(/\/+$/, "");
232
+ if (p.endsWith("/**")) p = p.slice(0, -3);
233
+ if (p.endsWith("/*")) p = p.slice(0, -2);
234
+ compiled.push({ prefix: p, rules });
235
+ }
236
+ }
237
+ function isExempt(pageDir, rule) {
238
+ if (!pageDir || !rule) return false;
239
+ const dir = String(pageDir).replace(/\\/g, "/");
240
+ const r = String(rule).toUpperCase();
241
+ for (const c of compiled) {
242
+ if (
243
+ (c.rules.has(r)) &&
244
+ (dir === c.prefix || dir.startsWith(c.prefix + "/"))
245
+ ) {
246
+ return true;
247
+ }
248
+ }
249
+ return false;
250
+ }
251
+ return { isExempt, source: configPath, warnings };
252
+ }
253
+
165
254
  // ─── 工具函数 ──────────────────────────────────────────────────────────
166
255
 
167
256
  function walkDir(dir, base, results) {
@@ -238,22 +327,30 @@ function countEffectiveLines(scriptContent) {
238
327
  }
239
328
 
240
329
  /**
241
- * 用 babel/parser 解析 script,提取 import 标识符和来源
330
+ * 用 babel/parser 解析 script AST(复用给 extractScriptInfo / 圈复杂度计算)
242
331
  */
243
- function extractScriptInfo(scriptContent) {
244
- if (!ensureAst() || !scriptContent) {
245
- return { specifiers: [], sources: [] };
246
- }
247
- let ast;
332
+ function parseScriptAst(scriptContent) {
333
+ if (!ensureAst() || !scriptContent) return null;
248
334
  try {
249
- ast = _babelParser.parse(scriptContent, {
335
+ return _babelParser.parse(scriptContent, {
250
336
  sourceType: "module",
251
337
  plugins: ["typescript", "jsx"],
252
338
  errorRecovery: true,
253
339
  });
254
340
  } catch {
341
+ return null;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * 用 babel/parser 解析 script,提取 import 标识符和来源
347
+ */
348
+ function extractScriptInfo(scriptContent) {
349
+ if (!ensureAst() || !scriptContent) {
255
350
  return { specifiers: [], sources: [] };
256
351
  }
352
+ const ast = parseScriptAst(scriptContent);
353
+ if (!ast) return { specifiers: [], sources: [] };
257
354
  const specifiers = [];
258
355
  const sources = [];
259
356
  for (const node of ast.program.body) {
@@ -272,6 +369,114 @@ function extractScriptInfo(scriptContent) {
272
369
  return { specifiers, sources };
273
370
  }
274
371
 
372
+ // ─── R13 圈复杂度(Mcabe)──────────────────────────────────────────────
373
+ //
374
+ // 定义与 ESLint `complexity` 规则一致:复杂度 = 1 + 决策点数。
375
+ // 不依赖 @babel/traverse,自写轻量遍历,避免新增运行时依赖。
376
+
377
+ const COMPLEXITY_NODE_TYPES = new Set([
378
+ "IfStatement", // if / else if(每个 IfStatement 计 1)
379
+ "SwitchCase", // 每个 case / default
380
+ "ForStatement",
381
+ "ForInStatement",
382
+ "ForOfStatement",
383
+ "WhileStatement",
384
+ "DoWhileStatement",
385
+ "CatchClause",
386
+ "ConditionalExpression", // 三元 ?:
387
+ "LogicalExpression", // && / || / ??
388
+ ]);
389
+
390
+ const FUNCTION_NODE_TYPES = new Set([
391
+ "FunctionDeclaration",
392
+ "FunctionExpression",
393
+ "ArrowFunctionExpression",
394
+ ]);
395
+
396
+ function isFunctionNode(node) {
397
+ return Boolean(node) && FUNCTION_NODE_TYPES.has(node.type);
398
+ }
399
+
400
+ // 取节点所有子 AST 节点(跳过位置/范围元信息)
401
+ function getChildNodes(node) {
402
+ const out = [];
403
+ for (const key of Object.keys(node)) {
404
+ if (key === "loc" || key === "range" || key === "start" || key === "end") {
405
+ continue;
406
+ }
407
+ const child = node[key];
408
+ if (Array.isArray(child)) {
409
+ for (const c of child) {
410
+ if (c && typeof c.type === "string") out.push(c);
411
+ }
412
+ } else if (child && typeof child.type === "string") {
413
+ out.push(child);
414
+ }
415
+ }
416
+ return out;
417
+ }
418
+
419
+ // 尽力推断函数名(变量赋值 / 类方法 / 对象方法 / 命名函数表达式)
420
+ function resolveFnName(node, parent) {
421
+ if (node.id && node.id.name) return node.id.name;
422
+ if (!parent) return "(anonymous)";
423
+ if (parent.type === "VariableDeclarator" && parent.id && parent.id.name) {
424
+ return parent.id.name;
425
+ }
426
+ if (
427
+ (parent.type === "MethodDefinition" || parent.type === "Property") &&
428
+ parent.key
429
+ ) {
430
+ return parent.key.name || parent.key.value || "(anonymous)";
431
+ }
432
+ if (parent.type === "AssignmentExpression" && parent.left) {
433
+ const left = parent.left;
434
+ if (left.property) return left.property.name || left.property.value;
435
+ if (left.name) return left.name;
436
+ }
437
+ return "(anonymous)";
438
+ }
439
+
440
+ // 收集整棵 AST 中的所有函数节点 + 名字(含嵌套函数,各独立计 R13)
441
+ function collectFunctions(ast) {
442
+ const fns = [];
443
+ const stack = [];
444
+ function walk(node) {
445
+ if (!node || typeof node.type !== "string") return;
446
+ let pushed = false;
447
+ if (isFunctionNode(node)) {
448
+ const parent = stack.length ? stack[stack.length - 1] : null;
449
+ fns.push({ node, name: resolveFnName(node, parent) });
450
+ // 不再下钻到该函数体内部去发现"孙函数"——
451
+ // 嵌套函数会在遍历其父函数体时自然被收集
452
+ }
453
+ stack.push(node);
454
+ pushed = true;
455
+ for (const child of getChildNodes(node)) walk(child);
456
+ if (pushed) stack.pop();
457
+ }
458
+ walk(ast);
459
+ return fns;
460
+ }
461
+
462
+ /**
463
+ * 计算单个函数的圈复杂度(不下钻进嵌套函数体,嵌套函数独立计)
464
+ * @param {object} fnNode FunctionDeclaration / FunctionExpression / ArrowFunctionExpression
465
+ * @returns {number}
466
+ */
467
+ function computeFunctionComplexity(fnNode) {
468
+ if (!fnNode) return 0;
469
+ let complexity = 1;
470
+ function walk(node) {
471
+ if (!node || typeof node.type !== "string") return;
472
+ if (node !== fnNode && isFunctionNode(node)) return; // 嵌套函数边界
473
+ if (COMPLEXITY_NODE_TYPES.has(node.type)) complexity++;
474
+ for (const child of getChildNodes(node)) walk(child);
475
+ }
476
+ walk(fnNode);
477
+ return complexity;
478
+ }
479
+
275
480
  /**
276
481
  * 检测模板中是否有 el-table(非 BaseTable)
277
482
  */
@@ -382,6 +587,12 @@ function runAstRules(targetDir, scanRel, options) {
382
587
  const issues = [];
383
588
  const globalCidMap = new Map(); // cid → Set<pageDir>
384
589
 
590
+ // 项目级豁免配置(零功能影响,无配置文件时返回空豁免)
591
+ const exempt = loadExemptions(targetDir);
592
+ for (const w of exempt.warnings) {
593
+ issues.push({ level: "warn", dir: ".", text: w, rule: "EXEMPT" });
594
+ }
595
+
385
596
  for (const page of pages) {
386
597
  const absDir = path.join(targetDir, page.dir);
387
598
 
@@ -419,6 +630,34 @@ function runAstRules(targetDir, scanRel, options) {
419
630
  });
420
631
  }
421
632
 
633
+ // R13: 单函数圈复杂度 ≤ MAX_CYCLOMATIC_COMPLEXITY(standard 04,Mcabe)
634
+ // 覆盖 index.vue <script> 与 data.ts 的所有函数/方法/箭头函数
635
+ if (!hasIgnoreMarker(fullSource, "R13")) {
636
+ const maxC = CONFIG.MAX_CYCLOMATIC_COMPLEXITY;
637
+ const scanComplexity = (code, label) => {
638
+ const ast = parseScriptAst(code);
639
+ if (!ast) return;
640
+ for (const fn of collectFunctions(ast)) {
641
+ const cc = computeFunctionComplexity(fn.node);
642
+ if (cc > maxC) {
643
+ issues.push({
644
+ level: "error",
645
+ dir: page.dir,
646
+ text:
647
+ label + " 函数 " + fn.name +
648
+ "() 圈复杂度 " + cc + "(阈值 " + maxC +
649
+ "),需拆分为更小函数(standard 04)",
650
+ rule: "R13",
651
+ });
652
+ }
653
+ }
654
+ };
655
+ scanComplexity(scriptContent, "index.vue");
656
+ if (fs.existsSync(dataPath)) {
657
+ scanComplexity(fs.readFileSync(dataPath, "utf8"), "data.ts");
658
+ }
659
+ }
660
+
422
661
  // R2: 禁止的 import / 全局 API
423
662
  if (scriptContent) {
424
663
  const { specifiers, sources } = extractScriptInfo(scriptContent);
@@ -485,11 +724,12 @@ function runAstRules(targetDir, scanRel, options) {
485
724
  }
486
725
  }
487
726
 
488
- // R3: el-table 但未用 BaseTable(检查豁免标记)
727
+ // R3: el-table 但未用 BaseTable(检查注释豁免 + 配置豁免)
489
728
  if (
490
729
  hasRawElTable(template) &&
491
730
  !hasBaseTable(template) &&
492
- !hasIgnoreMarker(fullSource, "R3")
731
+ !hasIgnoreMarker(fullSource, "R3") &&
732
+ !exempt.isExempt(page.dir, "R3")
493
733
  ) {
494
734
  issues.push({
495
735
  level: "error",
@@ -654,7 +894,8 @@ function runAstRules(targetDir, scanRel, options) {
654
894
  const tagRegex = new RegExp("<" + tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "[\\s>]");
655
895
  if (
656
896
  tagRegex.test(template) &&
657
- !hasIgnoreMarker(fullSource, "R10")
897
+ !hasIgnoreMarker(fullSource, "R10") &&
898
+ !exempt.isExempt(page.dir, "R10")
658
899
  ) {
659
900
  issues.push({
660
901
  level: "error",
@@ -754,11 +995,153 @@ function getStagedFiles(targetDir) {
754
995
  }
755
996
  }
756
997
 
998
+ // ─── R14 类型错误零容忍(项目级,vue-tsc / tsc 委托)──────────────────
999
+ //
1000
+ // 体积较大,validate 默认不触发,由 CLI --typecheck / MCP typecheck:true 显式开启。
1001
+ // 无 tsconfig / 无 checker → 优雅降级为 warn(与 AST 依赖降级策略一致)。
1002
+ // 该函数不进入 page 粒度,整项目执行一次,结果按文件归并到 issues。
1003
+
1004
+ function runTypeCheck(root) {
1005
+ const safeRoot = root || process.cwd();
1006
+ const label = path.basename(safeRoot) || ".";
1007
+ const tsconfigPath = path.join(safeRoot, "tsconfig.json");
1008
+
1009
+ if (!fs.existsSync(tsconfigPath)) {
1010
+ return {
1011
+ issues: [
1012
+ {
1013
+ level: "warn",
1014
+ dir: label,
1015
+ text: "未发现 tsconfig.json,跳过类型检查 R14",
1016
+ rule: "R14",
1017
+ },
1018
+ ],
1019
+ ran: false,
1020
+ errorCount: 0,
1021
+ };
1022
+ }
1023
+
1024
+ const nmBin = path.join(safeRoot, "node_modules", ".bin");
1025
+ const envPath =
1026
+ nmBin + path.delimiter + (process.env.PATH || process.env.Path || "");
1027
+ const env = Object.assign({}, process.env, { PATH: envPath });
1028
+
1029
+ // 优先 vue-tsc(.vue 项目),回退 tsc;shell:true 让 Windows 找到 .cmd
1030
+ let checker = null;
1031
+ for (const bin of ["vue-tsc", "tsc"]) {
1032
+ try {
1033
+ const probe = spawnSync(bin, ["--version"], {
1034
+ shell: true,
1035
+ env,
1036
+ encoding: "utf8",
1037
+ timeout: 20000,
1038
+ });
1039
+ if (probe.status === 0) {
1040
+ checker = bin;
1041
+ break;
1042
+ }
1043
+ } catch {
1044
+ // ignore, try next
1045
+ }
1046
+ }
1047
+ if (!checker) {
1048
+ return {
1049
+ issues: [
1050
+ {
1051
+ level: "warn",
1052
+ dir: label,
1053
+ text: "未发现 vue-tsc / tsc,跳过类型检查 R14(建议安装后纳入 CI)",
1054
+ rule: "R14",
1055
+ },
1056
+ ],
1057
+ ran: false,
1058
+ errorCount: 0,
1059
+ };
1060
+ }
1061
+
1062
+ let result;
1063
+ try {
1064
+ result = spawnSync(checker, ["--noEmit"], {
1065
+ cwd: safeRoot,
1066
+ shell: true,
1067
+ env,
1068
+ encoding: "utf8",
1069
+ timeout: 180000,
1070
+ });
1071
+ } catch (e) {
1072
+ return {
1073
+ issues: [
1074
+ {
1075
+ level: "warn",
1076
+ dir: label,
1077
+ text:
1078
+ "类型检查执行异常:" + ((e && e.message) || String(e)),
1079
+ rule: "R14",
1080
+ },
1081
+ ],
1082
+ ran: false,
1083
+ errorCount: 0,
1084
+ };
1085
+ }
1086
+
1087
+ const out =
1088
+ String(result.stdout || "") + String(result.stderr || "");
1089
+ // 标准格式:path(line,col): error TS1234: message
1090
+ // 捕获组:1=file 2=line 3=col 4=code 5=msg
1091
+ const errRe = /^(.+?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s*(.+)$/gm;
1092
+ const errors = [];
1093
+ let m;
1094
+ while ((m = errRe.exec(out)) !== null) {
1095
+ errors.push({ file: m[1], line: m[2], code: m[4], msg: m[5] });
1096
+ if (errors.length >= CONFIG.TYPECHECK_ERROR_CAP) break;
1097
+ }
1098
+
1099
+ if (errors.length === 0) {
1100
+ if (result.status === 0) {
1101
+ return { issues: [], ran: true, errorCount: 0 };
1102
+ }
1103
+ // 退出码非 0 但未解析出标准 error 行:疑似 tsconfig / 配置级错误
1104
+ return {
1105
+ issues: [
1106
+ {
1107
+ level: "error",
1108
+ dir: label,
1109
+ text:
1110
+ checker +
1111
+ " --noEmit 退出码 " +
1112
+ result.status +
1113
+ "(请检查 tsconfig / 类型配置,无标准 TS 错误输出)",
1114
+ rule: "R14",
1115
+ },
1116
+ ],
1117
+ ran: true,
1118
+ errorCount: 1,
1119
+ };
1120
+ }
1121
+
1122
+ const issues = errors.map((e) => {
1123
+ const rel = path.relative(safeRoot, e.file).replace(/\\/g, "/") || e.file;
1124
+ return {
1125
+ level: "error",
1126
+ dir: path.dirname(rel) || ".",
1127
+ text:
1128
+ e.code + " " + e.msg + " (" + path.basename(rel) + ":" + e.line + ")",
1129
+ rule: "R14",
1130
+ };
1131
+ });
1132
+ return { issues, ran: true, errorCount: errors.length };
1133
+ }
1134
+
757
1135
  module.exports = {
758
1136
  runAstRules,
759
1137
  parseVueScript,
760
1138
  countEffectiveLines,
761
1139
  extractScriptInfo,
1140
+ parseScriptAst,
1141
+ computeFunctionComplexity,
1142
+ collectFunctions,
1143
+ runTypeCheck,
1144
+ loadExemptions,
762
1145
  hasAstAvailable,
763
1146
  isAstFunctionallyUsable,
764
1147
  getStagedFiles,
package/mcp/config.js CHANGED
@@ -1,47 +1,47 @@
1
- 'use strict'
2
-
3
- const fs = require('fs')
4
- const path = require('path')
5
-
6
- /**
7
- * 从项目的 .wl-skills/skills/sync/env.local.json 加载 MCP 运行配置
8
- * 项目根目录通过环境变量 WL_PROJECT_ROOT 传入(由 .cursor/mcp.json 注入)
9
- */
10
- function loadConfig() {
11
- const projectRoot = process.env.WL_PROJECT_ROOT
12
- ? path.resolve(process.env.WL_PROJECT_ROOT)
13
- : process.cwd()
14
-
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ /**
7
+ * 从项目的 .wl-skills/skills/sync/env.local.json 加载 MCP 运行配置
8
+ * 项目根目录通过环境变量 WL_PROJECT_ROOT 传入(由 .cursor/mcp.json 注入)
9
+ */
10
+ function loadConfig() {
11
+ const projectRoot = process.env.WL_PROJECT_ROOT
12
+ ? path.resolve(process.env.WL_PROJECT_ROOT)
13
+ : process.cwd()
14
+
15
15
  const configPath = path.join(projectRoot, '.wl-skills', 'skills', 'sync', 'env.local.json')
16
-
17
- if (!fs.existsSync(configPath)) {
18
- throw new Error(
19
- `配置文件不存在: ${configPath}\n` +
20
- `请先执行 pnpm dlx @agile-team/wl-skills-kit init,然后填写 .wl-skills/skills/sync/env.local.json`
21
- )
22
- }
23
-
24
- let raw
25
- try {
26
- raw = JSON.parse(fs.readFileSync(configPath, 'utf8'))
27
- } catch (e) {
28
- throw new Error(`配置文件解析失败: ${e.message}`)
29
- }
30
-
31
- if (!raw.gatewayPath || raw.gatewayPath.includes('你的网关')) {
32
- throw new Error('请在 env.local.json 中填写真实的 gatewayPath(当前为占位值)')
33
- }
34
- if (!raw.token || raw.token.includes('Bearer Token')) {
35
- throw new Error('请在 env.local.json 中填写真实的 token(当前为占位值)')
36
- }
37
-
38
- return {
39
- gatewayPath: raw.gatewayPath.replace(/\/$/, ''), // 去掉尾部斜杠
40
- token: raw.token,
41
- sysAppNo: raw.sysAppNo || '',
42
- menu: raw.menu || {},
43
- dict: raw.dict || {},
44
- }
45
- }
46
-
47
- module.exports = { loadConfig }
16
+
17
+ if (!fs.existsSync(configPath)) {
18
+ throw new Error(
19
+ `配置文件不存在: ${configPath}\n` +
20
+ `请先执行 pnpm dlx @agile-team/wl-skills-kit init,然后填写 .wl-skills/skills/sync/env.local.json`
21
+ )
22
+ }
23
+
24
+ let raw
25
+ try {
26
+ raw = JSON.parse(fs.readFileSync(configPath, 'utf8'))
27
+ } catch (e) {
28
+ throw new Error(`配置文件解析失败: ${e.message}`)
29
+ }
30
+
31
+ if (!raw.gatewayPath || raw.gatewayPath.includes('你的网关')) {
32
+ throw new Error('请在 env.local.json 中填写真实的 gatewayPath(当前为占位值)')
33
+ }
34
+ if (!raw.token || raw.token.includes('Bearer Token')) {
35
+ throw new Error('请在 env.local.json 中填写真实的 token(当前为占位值)')
36
+ }
37
+
38
+ return {
39
+ gatewayPath: raw.gatewayPath.replace(/\/$/, ''), // 去掉尾部斜杠
40
+ token: raw.token,
41
+ sysAppNo: raw.sysAppNo || '',
42
+ menu: raw.menu || {},
43
+ dict: raw.dict || {},
44
+ }
45
+ }
46
+
47
+ module.exports = { loadConfig }
package/mcp/registry.js CHANGED
@@ -296,11 +296,16 @@ const DESCRIPTORS = [
296
296
  {
297
297
  name: "wls_validate_page",
298
298
  description:
299
- "校验页面是否符合 wl-skills-kit 最新页面规范:BaseTable+AGGrid+cid、defineColumns、renderOps、mock-first、api.md 等。",
299
+ "校验页面是否符合 wl-skills-kit 最新页面规范:BaseTable+AGGrid+cid、defineColumns、renderOps、mock-first、api.md 等。开启 typecheck 额外执行 vue-tsc/tsc 类型检查(R14)。",
300
300
  inputSchema: {
301
301
  type: "object",
302
302
  properties: {
303
303
  path: { type: "string", description: "页面或目录路径,默认 src/views" },
304
+ typecheck: {
305
+ type: "boolean",
306
+ description:
307
+ "是否额外执行 vue-tsc/tsc --noEmit 类型检查(R14,体积较大,CI 场景开启)",
308
+ },
304
309
  },
305
310
  required: [],
306
311
  },
@@ -4,7 +4,7 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { execFileSync } = require("child_process");
6
6
  const https = require("https");
7
- const { runAstRules } = require("../../lib/ast-rules");
7
+ const { runAstRules, runTypeCheck } = require("../../lib/ast-rules");
8
8
  const { alignPage } = require("../../lib/page-spec");
9
9
 
10
10
  function getProjectRoot() {
@@ -213,6 +213,14 @@ async function handleValidatePage(args) {
213
213
  }
214
214
  }
215
215
 
216
+ // ── 类型检查 R14(v2.11.2+,仅当 typecheck:true 触发)─────────────────
217
+ if (args && args.typecheck) {
218
+ const tc = runTypeCheck(root);
219
+ for (const iss of tc.issues) {
220
+ issues.push([iss.dir, iss.level, `[${iss.rule}] ${iss.text}`]);
221
+ }
222
+ }
223
+
216
224
  const errors = issues.filter((item) => item[1] === "error").length;
217
225
  const lines = [
218
226
  `✅ 页面校验完成:${scanPath}`,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agile-team/wl-skills-kit",
3
- "version": "2.11.1",
4
- "description": "AI Skill 模板包 v2.11.1 — 14 条编码规范 + 11 个 AI Skill + 17 个 MCP Tool,一条命令导入 Vue 3 项目(.wl-skills/ 统一隔离架构)",
3
+ "version": "2.11.3",
4
+ "description": "AI Skill 模板包 v2.11.3 — 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": {