@agile-team/wl-skills-kit 2.11.3 → 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 +19 -0
- package/README.md +1 -1
- package/bin/wl-skills.js +69 -21
- package/files/.wl-skills/guides/architecture.md +1 -1
- package/files/.wl-skills/skills/core/convention-audit/SKILL.md +2 -1
- package/lib/ast-rules.js +84 -75
- package/lib/page-spec.js +27 -23
- package/lib/vite-plugin-wl-skills.js +0 -2
- package/package.json +11 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
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
|
+
|
|
3
22
|
## [2.11.3] - 2026-06-21
|
|
4
23
|
|
|
5
24
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @agile-team/wl-skills-kit
|
|
2
2
|
|
|
3
|
-
**AI Skill 模板包 v2.11.
|
|
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
|
|
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.
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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 .
|
|
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: .
|
|
1500
|
+
console.log(" \u2502 \u53c2\u8003: .wl-skills/" + suggestion.ref);
|
|
1453
1501
|
console.log(" \u2502");
|
|
1454
1502
|
}
|
|
1455
1503
|
|
|
@@ -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
|
|
package/lib/ast-rules.js
CHANGED
|
@@ -52,14 +52,6 @@ function ensureAst() {
|
|
|
52
52
|
if (_astChecked) return _compilerSfc && _babelParser;
|
|
53
53
|
_astChecked = true;
|
|
54
54
|
|
|
55
|
-
// 优先从 kit 自身 node_modules 解析(开发/测试环境)
|
|
56
|
-
const tryPaths = [
|
|
57
|
-
// 1. kit 自身 node_modules(标准 require 解析)
|
|
58
|
-
null,
|
|
59
|
-
// 2. 调用方项目(CWD)的 node_modules — 业务项目通过 npx/node 运行时
|
|
60
|
-
// kit 本身没装 @vue/compiler-sfc,但业务项目有
|
|
61
|
-
];
|
|
62
|
-
|
|
63
55
|
// 尝试标准 require(从 kit 目录解析)
|
|
64
56
|
try {
|
|
65
57
|
_compilerSfc = require("@vue/compiler-sfc");
|
|
@@ -76,7 +68,7 @@ function ensureAst() {
|
|
|
76
68
|
if (!_compilerSfc || !_babelParser) {
|
|
77
69
|
const cwd = process.env.WL_PROJECT_ROOT || process.cwd();
|
|
78
70
|
try {
|
|
79
|
-
const createRequire = require("module")
|
|
71
|
+
const {createRequire} = require("module");
|
|
80
72
|
const cwdRequire = createRequire(cwd + "/package.json");
|
|
81
73
|
if (!_compilerSfc) {
|
|
82
74
|
try { _compilerSfc = cwdRequire("@vue/compiler-sfc"); } catch {}
|
|
@@ -430,7 +422,7 @@ function resolveFnName(node, parent) {
|
|
|
430
422
|
return parent.key.name || parent.key.value || "(anonymous)";
|
|
431
423
|
}
|
|
432
424
|
if (parent.type === "AssignmentExpression" && parent.left) {
|
|
433
|
-
const left = parent
|
|
425
|
+
const {left} = parent;
|
|
434
426
|
if (left.property) return left.property.name || left.property.value;
|
|
435
427
|
if (left.name) return left.name;
|
|
436
428
|
}
|
|
@@ -532,6 +524,75 @@ function isLikelyListPage(template) {
|
|
|
532
524
|
return hasBaseTable && hasPagination && !hasForm && !hasTabs;
|
|
533
525
|
}
|
|
534
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
|
+
|
|
535
596
|
// ─── 主检测函数 ────────────────────────────────────────────────────────
|
|
536
597
|
|
|
537
598
|
/**
|
|
@@ -632,31 +693,7 @@ function runAstRules(targetDir, scanRel, options) {
|
|
|
632
693
|
|
|
633
694
|
// R13: 单函数圈复杂度 ≤ MAX_CYCLOMATIC_COMPLEXITY(standard 04,Mcabe)
|
|
634
695
|
// 覆盖 index.vue <script> 与 data.ts 的所有函数/方法/箭头函数
|
|
635
|
-
|
|
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
|
-
}
|
|
696
|
+
checkR13Complexity(scriptContent, dataPath, page.dir, fullSource, issues);
|
|
660
697
|
|
|
661
698
|
// R2: 禁止的 import / 全局 API
|
|
662
699
|
if (scriptContent) {
|
|
@@ -813,38 +850,10 @@ function runAstRules(targetDir, scanRel, options) {
|
|
|
813
850
|
|
|
814
851
|
// R8: 强制 3 文件分离 — 有 API 调用/大量逻辑但无 data.ts
|
|
815
852
|
// 任何页面(不分类型)只要有接口调用或超过阈值行数,就应该拆出 data.ts
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const hasScss = page.names.has("index.scss");
|
|
821
|
-
if (!page.names.has("data.ts")) {
|
|
822
|
-
if (hasApiCall && !hasIgnoreMarker(fullSource, "R8")) {
|
|
823
|
-
issues.push({
|
|
824
|
-
level: "error",
|
|
825
|
-
dir: page.dir,
|
|
826
|
-
text: "页面有接口调用但缺 data.ts(业务逻辑必须在 data.ts 中)",
|
|
827
|
-
rule: "R8",
|
|
828
|
-
});
|
|
829
|
-
} else if (effectiveLines > 20 && !hasIgnoreMarker(fullSource, "R8")) {
|
|
830
|
-
issues.push({
|
|
831
|
-
level: "warn",
|
|
832
|
-
dir: page.dir,
|
|
833
|
-
text: "index.vue 有 " + effectiveLines + " 行逻辑但无 data.ts(建议拆分)",
|
|
834
|
-
rule: "R8",
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
// 有 data.ts 但 index.vue 仍然有 API 调用(逻辑泄漏)
|
|
839
|
-
if (page.names.has("data.ts") && hasApiCall && !hasIgnoreMarker(fullSource, "R8")) {
|
|
840
|
-
issues.push({
|
|
841
|
-
level: "error",
|
|
842
|
-
dir: page.dir,
|
|
843
|
-
text: "有 data.ts 但 index.vue 中仍含 API 调用(逻辑应全部在 data.ts)",
|
|
844
|
-
rule: "R8",
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
}
|
|
853
|
+
checkR8FileSeparation(
|
|
854
|
+
scriptContent, effectiveLines, page.names.has("data.ts"),
|
|
855
|
+
page.dir, fullSource, issues,
|
|
856
|
+
);
|
|
848
857
|
|
|
849
858
|
// R9: api.md 质量检测 — 有 API_CONFIG 时检查 api.md 结构完整性
|
|
850
859
|
if (page.names.has("data.ts")) {
|
|
@@ -855,10 +864,6 @@ function runAstRules(targetDir, scanRel, options) {
|
|
|
855
864
|
if (hasApiConfig && page.names.has("api.md")) {
|
|
856
865
|
const apiMdPath = path.join(absDir, "api.md");
|
|
857
866
|
const apiMdContent = fs.readFileSync(apiMdPath, "utf-8");
|
|
858
|
-
// 检查 api.md 是否有接口列表表格(核心结构)
|
|
859
|
-
const hasInterfaceTable = /\|\s*操作\s*\|.*\|\s*Method\s*\||\|\s*操作\s*\|.*\|\s*URL\s*\|/.test(apiMdContent);
|
|
860
|
-
// 检查 api.md 是否有实体定义
|
|
861
|
-
const hasEntityDef = /字段|实体|Entity|字段名/.test(apiMdContent);
|
|
862
867
|
// 检查 api.md 中的 URL 是否与 data.ts API_CONFIG 一致
|
|
863
868
|
// 正则匹配 URL 路径:支持多段、数字、连字符、下划线
|
|
864
869
|
// 例:/mdata/mdataModel/list /api/v2/customer-archive/save /sys/user_role/list
|
|
@@ -1100,22 +1105,26 @@ function runTypeCheck(root) {
|
|
|
1100
1105
|
if (result.status === 0) {
|
|
1101
1106
|
return { issues: [], ran: true, errorCount: 0 };
|
|
1102
1107
|
}
|
|
1103
|
-
// 退出码非 0 但未解析出标准 error
|
|
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);
|
|
1104
1111
|
return {
|
|
1105
1112
|
issues: [
|
|
1106
1113
|
{
|
|
1107
|
-
level: "error",
|
|
1114
|
+
level: isConfigError ? "warn" : "error",
|
|
1108
1115
|
dir: label,
|
|
1109
1116
|
text:
|
|
1110
1117
|
checker +
|
|
1111
1118
|
" --noEmit 退出码 " +
|
|
1112
1119
|
result.status +
|
|
1113
|
-
|
|
1120
|
+
(isConfigError
|
|
1121
|
+
? "(疑似 tsconfig 配置问题,非类型错误,请检查 tsconfig.json)"
|
|
1122
|
+
: "(无标准 TS 错误输出,请检查 tsconfig / 类型配置)"),
|
|
1114
1123
|
rule: "R14",
|
|
1115
1124
|
},
|
|
1116
1125
|
],
|
|
1117
1126
|
ran: true,
|
|
1118
|
-
errorCount: 1,
|
|
1127
|
+
errorCount: isConfigError ? 0 : 1,
|
|
1119
1128
|
};
|
|
1120
1129
|
}
|
|
1121
1130
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
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.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
|
-
"
|
|
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
|
},
|