@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.
- package/CHANGELOG.md +25 -0
- package/README.md +38 -21
- package/bin/wl-skills.js +27 -3
- package/files/.wl-skills/docs/jh-pagination.md +505 -505
- package/files/.wl-skills/docs/request.md +940 -940
- package/files/.wl-skills/docs/validate-exempt.md +113 -0
- package/files/.wl-skills/guides/architecture.md +1 -1
- package/files/.wl-skills/skills/_compat/headers/cursor-mdc.txt +1 -1
- package/files/.wl-skills/skills/_compat/headers/kiro.txt +1 -1
- package/files/.wl-skills/skills/_compat/headers/trae.txt +1 -1
- package/files/.wl-skills/skills/core/convention-audit/SKILL.md +3 -3
- package/files/.wl-skills/skills/core/spec-doc-parse/SKILL.md +332 -332
- package/files/.wl-skills/skills/core/spec-doc-parse/USAGE.md +97 -97
- package/files/.wl-skills/skills/sync/permission-sync/USAGE.md +107 -107
- package/files/.wl-skills/src/components/global/C_ParentView/index.vue +3 -3
- package/files/.wl-skills/src/components/global/C_RightToolbar/index.vue +157 -157
- package/files/.wl-skills/src/components/global/C_SvgIcon/index.vue +31 -31
- package/files/.wl-skills/src/components/global/C_SvgIcon/svgicon.js +10 -10
- package/files/.wl-skills/src/components/global/C_TagStatus/README.md +264 -264
- package/files/.wl-skills/src/components/global/C_TagStatus/config.ts +192 -192
- package/files/.wl-skills/src/components/global/C_TagStatus/index.vue +106 -106
- package/files/.wl-skills/src/components/global/C_TagStatus/types.ts +64 -64
- package/files/.wl-skills/src/components/global/C_Tree/README.md +153 -153
- package/files/.wl-skills/src/components/global/C_Tree/index.scss +42 -42
- package/files/.wl-skills/src/components/global/C_Tree/index.vue +78 -78
- package/files/.wl-skills/src/components/global/C_Tree/types.ts +59 -59
- package/files/.wl-skills/src/components/local/c_formModal/README.md +235 -235
- package/files/.wl-skills/src/components/local/c_formModal/data.ts +95 -95
- package/files/.wl-skills/src/components/local/c_formModal/index.scss +8 -8
- package/files/.wl-skills/src/components/local/c_formModal/index.vue +107 -107
- package/files/.wl-skills/src/components/local/c_formSections/data.ts +175 -175
- package/files/.wl-skills/src/components/local/c_formSections/index.scss +280 -280
- package/files/.wl-skills/src/components/local/c_formSections/index.vue +429 -429
- package/files/.wl-skills/src/components/local/c_listModal/data.ts +41 -41
- package/files/.wl-skills/src/components/local/c_listModal/index.vue +136 -136
- package/files/.wl-skills/src/components/local/c_spliterTitle/index.scss +25 -25
- package/files/.wl-skills/src/components/local/c_spliterTitle/index.vue +21 -21
- package/files/.wl-skills/src/components/remote/AGGrid/README.md +530 -530
- package/files/.wl-skills/src/components/remote/BaseForm/README.md +508 -508
- package/files/.wl-skills/src/components/remote/BaseQuery/README.md +865 -865
- package/files/.wl-skills/src/components/remote/BaseTable/README.md +941 -941
- package/files/.wl-skills/src/components/remote/BaseToolbar/README.md +496 -496
- package/files/.wl-skills/src/types/page.ts +24 -24
- package/files/.wl-skills/standards/04-coding-basics.md +39 -1
- package/files/.wl-skills/standards/09-typescript.md +26 -3
- package/files/.wl-skills/standards/12-base-table.md +56 -4
- package/files/.wl-skills/standards/13-platform-components.md +1 -0
- package/files/.wl-skills/standards/index.md +2 -2
- package/files/.wl-skills/templates/README.md +44 -44
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/api.md +54 -54
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/data.ts +346 -346
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.scss +1 -1
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add/index.vue +28 -28
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/data.ts +115 -115
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.scss +44 -44
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-add-form/index.vue +43 -43
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/data.ts +338 -338
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.scss +1 -1
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change/index.vue +28 -28
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/data.ts +115 -115
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.scss +44 -44
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-apply-change-form/index.vue +43 -43
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/api.md +88 -88
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/data.ts +601 -601
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.scss +1 -1
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-archive/index.vue +64 -64
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/api.md +67 -67
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/data.ts +286 -286
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.scss +139 -139
- package/files/.wl-skills/templates/produce/aiflow/mmwr-customer-detail/index.vue +318 -318
- package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/api.md +98 -98
- package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/data.ts +543 -543
- package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.scss +1 -1
- package/files/.wl-skills/templates/produce/aiflow/mmwr-temp-customer-archive/index.vue +52 -52
- package/files/.wl-skills/templates/sale/demo/add-demo/data.ts +518 -518
- package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/data.ts +524 -524
- package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.scss +154 -154
- package/files/.wl-skills/templates/sale/demo/billet-flame-cut-plan/index.vue +117 -117
- package/files/.wl-skills/templates/sale/demo/domestic-trade-order/data.ts +308 -308
- package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.scss +99 -99
- package/files/.wl-skills/templates/sale/demo/domestic-trade-order/index.vue +77 -77
- package/files/.wl-skills/templates/sale/demo/heat-batch-return/data.ts +367 -367
- package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.scss +100 -100
- package/files/.wl-skills/templates/sale/demo/heat-batch-return/index.vue +170 -170
- package/files/.wl-skills/templates/sale/demo/heat-batch-return/meltDialog.vue +320 -320
- package/files/.wl-skills/templates/sale/demo/metallurgical-spec/data.ts +824 -824
- package/lib/ast-rules.js +395 -12
- package/mcp/config.js +46 -46
- package/mcp/registry.js +6 -1
- package/mcp/tools/projectTools.js +9 -1
- 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~
|
|
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
|
|
330
|
+
* 用 babel/parser 解析 script 为 AST(复用给 extractScriptInfo / 圈复杂度计算)
|
|
242
331
|
*/
|
|
243
|
-
function
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "AI Skill 模板包 v2.11.
|
|
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": {
|