@qilitt-mickey/vue3-temp-skill 1.2.5 → 1.2.7
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/README.md +51 -115
- package/SKILL.md +41 -159
- package/adapters/vue-element-plus.json +634 -0
- package/bin/cli.js +297 -612
- package/package.json +6 -2
- package/references/adapters/_new-adapter.md +17 -82
- package/references/adapters/vue-antdv.md +21 -107
- package/references/adapters/vue-element-plus.md +36 -179
- package/references/code-quality.md +52 -168
- package/references/core-kernel.md +12 -3
- package/references/crud-pages.md +10 -11
- package/references/data-screen.md +8 -9
- package/references/design-apply.md +57 -352
- package/references/layout-theme.md +6 -4
- package/references/visual-verification.md +48 -0
- package/scripts/check-landing.mjs +308 -62
- package/scripts/generate-css.mjs +17 -397
- package/scripts/match.py +30 -11
- package/scripts/modules.json +3 -3
- package/scripts/verify-render.mjs +17 -522
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 旧格式文本诊断 —— 默认禁用,不提供业务/视觉验收结论。
|
|
5
5
|
*
|
|
6
6
|
* 用法:
|
|
7
|
-
* node check-landing.mjs --
|
|
8
|
-
* node check-landing.mjs --project
|
|
7
|
+
* node check-landing.mjs --help
|
|
8
|
+
* node check-landing.mjs --legacy-diagnostics --project <项目根目录> [--json]
|
|
9
9
|
*
|
|
10
|
-
* 退出码: 0 =
|
|
10
|
+
* 退出码: 0 = 帮助 / 2 = 未验收(包括旧文本检查没有 FAIL 的情况)
|
|
11
11
|
*
|
|
12
|
-
* 设计原则:
|
|
12
|
+
* 设计原则: 显式启用后仅只读诊断。不修改、不联网。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
|
-
import {
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
19
|
|
|
19
20
|
const IGNORE_DIRS = new Set([
|
|
20
21
|
"node_modules", "dist", ".git", ".vite", "coverage",
|
|
21
22
|
"__pycache__", ".idea", ".vscode", ".workbuddy", "unpackage",
|
|
23
|
+
// AI 工具配置 / 技能分发目录:技能脚本文本含 CSS 文件名与角色类名,
|
|
24
|
+
// 不排除会被 checkImported / checkMounting 自匹配成假 PASS
|
|
25
|
+
".agents", ".codebuddy", ".cursor", ".qoder", ".windsurf", ".gemini",
|
|
26
|
+
".trae", ".claude", ".github",
|
|
22
27
|
]);
|
|
23
28
|
|
|
24
29
|
const CSS_FILES = ["ui-tokens.css", "ui-roles.css", "ui-layout.css"];
|
|
@@ -26,27 +31,37 @@ const SOURCE_EXT = new Set([".vue", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"
|
|
|
26
31
|
const STYLE_EXT = new Set([".css", ".scss", ".less"]);
|
|
27
32
|
|
|
28
33
|
const EXIT_OK = 0;
|
|
29
|
-
const
|
|
34
|
+
const EXIT_UNVERIFIED = 2;
|
|
35
|
+
const NOTICE = "check-landing.mjs 仅为旧格式文本诊断,不是业务/视觉验收,不得据此宣称还原通过。";
|
|
36
|
+
const MIGRATION = "请迁移到 references/design-apply.md 和 references/visual-verification.md,以原稿为事实源手写项目视图,并逐区域、逐实例、逐状态进行浏览器验收。";
|
|
37
|
+
const HELP = `用法: node check-landing.mjs --legacy-diagnostics --project <项目根目录> [--json]
|
|
38
|
+
${NOTICE}
|
|
39
|
+
默认不读取项目;只有显式 --legacy-diagnostics 才运行旧的只读文本诊断。
|
|
40
|
+
旧格式检查与建议仅供理解历史数据,不是当前实施指令。
|
|
41
|
+
${MIGRATION}
|
|
42
|
+
--help / -h 退出 0;其他调用均退出 2,visualStatus=unverified。`;
|
|
30
43
|
|
|
31
44
|
// ────────────────────────────── 参数解析
|
|
32
45
|
|
|
33
46
|
function parseArgs(argv) {
|
|
34
|
-
const out = { project: process.cwd(), json: false };
|
|
47
|
+
const out = { project: process.cwd(), json: false, legacyDiagnostics: false, help: false };
|
|
35
48
|
for (let i = 0; i < argv.length; i++) {
|
|
36
49
|
const a = argv[i];
|
|
37
50
|
if (a === "--project" || a === "-p") {
|
|
38
51
|
const v = argv[i + 1];
|
|
39
|
-
if (!v || v.startsWith("
|
|
40
|
-
|
|
41
|
-
process.exit(EXIT_FAIL);
|
|
52
|
+
if (!v || v.startsWith("-")) {
|
|
53
|
+
throw new Error("--project 后面必须跟项目根目录路径");
|
|
42
54
|
}
|
|
43
55
|
out.project = path.resolve(v);
|
|
44
56
|
i++;
|
|
45
57
|
} else if (a === "--json") {
|
|
46
58
|
out.json = true;
|
|
59
|
+
} else if (a === "--legacy-diagnostics") {
|
|
60
|
+
out.legacyDiagnostics = true;
|
|
47
61
|
} else if (a === "--help" || a === "-h") {
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
out.help = true;
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(`未知参数: ${a}`);
|
|
50
65
|
}
|
|
51
66
|
}
|
|
52
67
|
return out;
|
|
@@ -222,7 +237,24 @@ function expectedClassName(roleName) {
|
|
|
222
237
|
);
|
|
223
238
|
}
|
|
224
239
|
|
|
225
|
-
|
|
240
|
+
/** 页面绑定引用的角色集合(pages.*.nodes 的 role + pages.*.regions 的 role)—— 挂载检查的「必需集」。
|
|
241
|
+
* 契约角色库可能大于单页实际用量(可选复用角色如 card.base),未被任何页面引用的角色缺失不应阻塞落地。 */
|
|
242
|
+
function boundRoles(contract) {
|
|
243
|
+
const pages = (contract && contract.pages) || {};
|
|
244
|
+
const set = new Set();
|
|
245
|
+
for (const page of Object.values(pages)) {
|
|
246
|
+
if (!page || typeof page !== "object") continue;
|
|
247
|
+
for (const n of Array.isArray(page.nodes) ? page.nodes : []) {
|
|
248
|
+
if (n && n.role) set.add(String(n.role));
|
|
249
|
+
}
|
|
250
|
+
for (const b of Array.isArray(page.regions) ? page.regions : []) {
|
|
251
|
+
if (b && b.role) set.add(String(b.role));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return set;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function checkMounting(projectRoot, roleNames, requiredSet) {
|
|
226
258
|
const vueFiles = walk(projectRoot, SOURCE_EXT, []);
|
|
227
259
|
const roleClassRe = /\brole-[a-z0-9]+(?:-[a-z0-9]+)*/g;
|
|
228
260
|
const usedClasses = new Map();
|
|
@@ -239,9 +271,19 @@ function checkMounting(projectRoot, roleNames) {
|
|
|
239
271
|
const expected = roleNames.map(expectedClassName);
|
|
240
272
|
const mounted = [];
|
|
241
273
|
const missing = [];
|
|
274
|
+
const optionalMissing = [];
|
|
275
|
+
let optionalMounted = 0;
|
|
242
276
|
for (let i = 0; i < roleNames.length; i++) {
|
|
243
|
-
|
|
244
|
-
|
|
277
|
+
// 必需集为空时回退旧语义(全部必需)—— 兼容无 pages 绑定的契约
|
|
278
|
+
const required = !requiredSet || requiredSet.size === 0 || requiredSet.has(roleNames[i]);
|
|
279
|
+
if (usedClasses.has(expected[i])) {
|
|
280
|
+
mounted.push(expected[i]);
|
|
281
|
+
if (!required) optionalMounted++;
|
|
282
|
+
} else if (required) {
|
|
283
|
+
missing.push(`${roleNames[i]} → .${expected[i]}`);
|
|
284
|
+
} else {
|
|
285
|
+
optionalMissing.push(`${roleNames[i]} → .${expected[i]}`);
|
|
286
|
+
}
|
|
245
287
|
}
|
|
246
288
|
|
|
247
289
|
const layoutClassRe = /\bui-(?:page|region-[a-z0-9]+(?:-[a-z0-9]+)*)\b/g;
|
|
@@ -256,6 +298,8 @@ function checkMounting(projectRoot, roleNames) {
|
|
|
256
298
|
scannedFileCount: vueFiles.length,
|
|
257
299
|
mounted,
|
|
258
300
|
missing,
|
|
301
|
+
optionalMissing,
|
|
302
|
+
optionalMounted,
|
|
259
303
|
layoutClasses: [...layoutClasses],
|
|
260
304
|
hasLayout: layoutClasses.has("ui-page"),
|
|
261
305
|
};
|
|
@@ -263,14 +307,34 @@ function checkMounting(projectRoot, roleNames) {
|
|
|
263
307
|
|
|
264
308
|
// ────────────────────────────── 结构校验(契约 §7)
|
|
265
309
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
310
|
+
/**
|
|
311
|
+
* 角色库目录多级探测(project-ui-design/references/library):
|
|
312
|
+
* 1. 环境变量 UI_DESIGN_LIBRARY_DIR(显式指定,最高优先)
|
|
313
|
+
* 2. 双包同级(源码仓 skills/ 下、universal/rules 分发后的同级安装目录)
|
|
314
|
+
* 3. 上两层兜底
|
|
315
|
+
* 均不可达时返回 { dir: null, tried } —— 白名单校验降级为 WARN(预期降级,非故障)。
|
|
316
|
+
*/
|
|
317
|
+
function resolveLibraryDir() {
|
|
318
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
319
|
+
const candidates = [];
|
|
320
|
+
if (process.env.UI_DESIGN_LIBRARY_DIR) {
|
|
321
|
+
candidates.push(path.resolve(process.env.UI_DESIGN_LIBRARY_DIR));
|
|
322
|
+
}
|
|
323
|
+
candidates.push(
|
|
324
|
+
path.resolve(here, "../../project-ui-design/references/library"),
|
|
325
|
+
path.resolve(here, "../../../project-ui-design/references/library"),
|
|
326
|
+
);
|
|
327
|
+
for (const dir of candidates) {
|
|
328
|
+
if (fs.existsSync(dir)) return { dir, tried: candidates };
|
|
329
|
+
}
|
|
330
|
+
return { dir: null, tried: candidates };
|
|
331
|
+
}
|
|
270
332
|
|
|
271
333
|
/** 加载角色库名集合:<场景>.json + components.json;目录不可达返回 null(降级 WARN) */
|
|
272
334
|
function loadRoleLibrary(scene) {
|
|
335
|
+
const { dir: LIBRARY_DIR } = resolveLibraryDir();
|
|
273
336
|
const merged = new Set();
|
|
337
|
+
if (!LIBRARY_DIR) return null;
|
|
274
338
|
for (const f of [`${scene}.json`, "components.json"]) {
|
|
275
339
|
const p = path.join(LIBRARY_DIR, f);
|
|
276
340
|
if (!exists(p)) continue;
|
|
@@ -377,6 +441,159 @@ function checkCssRules(cssPathOf, contract, roleNames) {
|
|
|
377
441
|
return missing;
|
|
378
442
|
}
|
|
379
443
|
|
|
444
|
+
// ────────────────────────────── 第 9 项:demo 内容泄漏(no-demo-leak)
|
|
445
|
+
|
|
446
|
+
/** 通用 UI 词 —— 项目自身业务同样会用到,命中不计泄漏 */
|
|
447
|
+
const GENERIC_WORDS = new Set([
|
|
448
|
+
"操作", "查看", "编辑", "删除", "新增", "修改", "保存", "取消", "确认", "确定",
|
|
449
|
+
"搜索", "查询", "重置", "刷新", "导入", "导出", "展开", "收起", "全选", "反选",
|
|
450
|
+
"状态", "类型", "时间", "名称", "编号", "序号", "备注", "图片", "地址", "金额",
|
|
451
|
+
"密度", "列设置", "批量删除", "批量导出", "全部", "启用", "停用", "更多",
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
/** 承载演示文案的键名白名单(其余键是控件类型/变体/对齐/图标等形态值,不判) */
|
|
455
|
+
const TEXT_KEYS = new Set([
|
|
456
|
+
"label", "title", "subtitle", "text", "placeholder", "total", "unit", "options",
|
|
457
|
+
]);
|
|
458
|
+
|
|
459
|
+
/** 整段演示数据的键名 —— 其下字符串全部视为演示内容 */
|
|
460
|
+
const SAMPLE_KEYS = new Set(["rowsSampleOnly", "sampleRows", "sampleRowsOnly"]);
|
|
461
|
+
|
|
462
|
+
/** 从契约 structure 收集 sampleOnly 演示文案:text -> 声明位置。
|
|
463
|
+
* inSample 上下文必须穿透数组/对象层级向下传播 —— 整段演示数据(rowsSampleOnly 等)
|
|
464
|
+
* 内部无论嵌多深、键名叫什么,全部按演示内容处理。 */
|
|
465
|
+
function collectDemoTexts(node, key, out, inSample = false) {
|
|
466
|
+
if (node === null || node === undefined) return out;
|
|
467
|
+
if (Array.isArray(node)) {
|
|
468
|
+
const here = inSample || SAMPLE_KEYS.has(key);
|
|
469
|
+
for (const item of node) collectDemoTexts(item, key, out, here);
|
|
470
|
+
return out;
|
|
471
|
+
}
|
|
472
|
+
if (typeof node === "object") {
|
|
473
|
+
const here = inSample || SAMPLE_KEYS.has(key);
|
|
474
|
+
for (const [k, v] of Object.entries(node)) collectDemoTexts(v, k, out, here);
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
if (typeof node !== "string") return out;
|
|
478
|
+
if (!inSample && !TEXT_KEYS.has(key)) return out;
|
|
479
|
+
const t = node.trim();
|
|
480
|
+
if (t.length < 2) return out; // 单字符不判
|
|
481
|
+
if (/^[\d\s.,:%/+\-–—:]+$/.test(t)) return out; // 纯数字/时间/标点不判
|
|
482
|
+
if (/^(M[\d.]|rect:|circle:)/.test(t)) return out; // icon path 记法不判
|
|
483
|
+
if (GENERIC_WORDS.has(t)) return out; // 通用 UI 词不判
|
|
484
|
+
if (!out.has(t)) out.set(t, key);
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function gitTry(projectRoot, args) {
|
|
489
|
+
try {
|
|
490
|
+
// 禁止刷新索引及调用项目配置的 fsmonitor,保持旧诊断只读。
|
|
491
|
+
return execSync(`git --no-optional-locks -c core.fsmonitor=false ${args}`, {
|
|
492
|
+
cwd: projectRoot,
|
|
493
|
+
encoding: "utf-8",
|
|
494
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
495
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
496
|
+
});
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const SCANNABLE_EXT = new Set([...SOURCE_EXT, ...STYLE_EXT, ".json", ".md"]);
|
|
503
|
+
|
|
504
|
+
/** 解析 git diff 输出 → [{ file, line }](仅新增行,按文件归属) */
|
|
505
|
+
function parseAddedLines(diffText) {
|
|
506
|
+
const out = [];
|
|
507
|
+
let file = null;
|
|
508
|
+
for (const raw of diffText.split("\n")) {
|
|
509
|
+
if (raw.startsWith("+++ b/")) {
|
|
510
|
+
file = raw.slice(6).trim();
|
|
511
|
+
} else if (raw.startsWith("+") && !raw.startsWith("+++")) {
|
|
512
|
+
if (file) out.push({ file, line: raw.slice(1).trim() });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** 第 9 项:设计稿 demo 内容(sampleOnly 文案/数据)不得出现在项目新增代码里 */
|
|
519
|
+
function checkDemoLeak(projectRoot, contract) {
|
|
520
|
+
const structure = contract && typeof contract.structure === "object" ? contract.structure : null;
|
|
521
|
+
if (!structure) {
|
|
522
|
+
return { status: "WARN", detail: "契约没有 structure 字段,无从提取演示文案,跳过本项", fix: null };
|
|
523
|
+
}
|
|
524
|
+
const demoTexts = collectDemoTexts(structure, "", new Map());
|
|
525
|
+
if (demoTexts.size === 0) {
|
|
526
|
+
return { status: "PASS", detail: "契约 structure 无可提取的演示文案,无需拦截", fix: null };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const insideRepo = gitTry(projectRoot, "rev-parse --is-inside-work-tree");
|
|
530
|
+
if (!insideRepo || insideRepo.trim() !== "true") {
|
|
531
|
+
return {
|
|
532
|
+
status: "WARN",
|
|
533
|
+
detail: `项目不是 git 仓库,无法定位新增代码做泄漏比对(已提取 ${demoTexts.size} 条演示文案)。请人工确认:项目里没有出现设计稿的文案/数据/演示页面`,
|
|
534
|
+
fix: null,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const leaks = [];
|
|
539
|
+
const isDesignDir = (f) => f.replace(/\\/g, "/").startsWith(".design/");
|
|
540
|
+
|
|
541
|
+
// ① untracked 文件:整份内容都在比对范围(设计稿整页被生成进项目就是这个形态)
|
|
542
|
+
const statusOut = gitTry(projectRoot, "status --porcelain");
|
|
543
|
+
if (statusOut) {
|
|
544
|
+
for (const raw of statusOut.split("\n")) {
|
|
545
|
+
if (!raw.startsWith("?? ")) continue;
|
|
546
|
+
let f = raw.slice(3).trim();
|
|
547
|
+
if (f.startsWith('"') && f.endsWith('"')) f = f.slice(1, -1);
|
|
548
|
+
f = f.replace(/\\/g, "/").replace(/\/$/, "");
|
|
549
|
+
if (isDesignDir(f)) continue;
|
|
550
|
+
const abs = path.join(projectRoot, f.replace(/\//g, path.sep));
|
|
551
|
+
let isFile = false;
|
|
552
|
+
try { isFile = fs.statSync(abs).isFile(); } catch { continue; }
|
|
553
|
+
const ext = path.extname(f).toLowerCase();
|
|
554
|
+
if (!isFile || !SCANNABLE_EXT.has(ext)) continue;
|
|
555
|
+
const content = readSafe(abs);
|
|
556
|
+
for (const [text, src] of demoTexts) {
|
|
557
|
+
if (content.includes(text)) leaks.push({ text, src, where: `${f}(untracked)` });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ② 已跟踪文件的改动行:禁用外部 diff/textconv,避免执行项目配置的程序。
|
|
563
|
+
const diffOut = gitTry(projectRoot, "diff --no-ext-diff --no-textconv HEAD --unified=0");
|
|
564
|
+
if (diffOut) {
|
|
565
|
+
for (const { file, line } of parseAddedLines(diffOut)) {
|
|
566
|
+
if (isDesignDir(file)) continue;
|
|
567
|
+
const ext = path.extname(file).toLowerCase();
|
|
568
|
+
if (!SCANNABLE_EXT.has(ext)) continue;
|
|
569
|
+
for (const [text, src] of demoTexts) {
|
|
570
|
+
if (line.includes(text)) leaks.push({ text, src, where: `${file} +${line.slice(0, 60)}` });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (leaks.length === 0) {
|
|
576
|
+
return {
|
|
577
|
+
status: "PASS",
|
|
578
|
+
detail: `已提取 ${demoTexts.size} 条演示文案,项目新增代码中零命中`,
|
|
579
|
+
fix: null,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
// 同一文案多处泄漏只报一处
|
|
583
|
+
const seen = new Set();
|
|
584
|
+
const uniq = leaks.filter((l) => (seen.has(l.text) ? false : (seen.add(l.text), true)));
|
|
585
|
+
return {
|
|
586
|
+
status: "FAIL",
|
|
587
|
+
detail:
|
|
588
|
+
`拦截 ${uniq.length} 处设计稿 demo 内容泄漏: ` +
|
|
589
|
+
uniq.slice(0, 6).map((l) => `「${l.text}」← ${l.where}`).join(";") +
|
|
590
|
+
(uniq.length > 6 ? ` …等共 ${uniq.length} 处` : ""),
|
|
591
|
+
fix:
|
|
592
|
+
"把设计稿当需求在项目里开发了一遍 —— 删除泄漏的演示页面/演示数据/新注册的路由与菜单," +
|
|
593
|
+
"把效果还原到项目既有业务页面上(锚点规则见 design-apply.md 第 6 步)。设计稿内容与文案是 demo,一个字都不进项目。",
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
380
597
|
// ────────────────────────────── 主流程
|
|
381
598
|
|
|
382
599
|
function run(projectRoot) {
|
|
@@ -385,8 +602,7 @@ function run(projectRoot) {
|
|
|
385
602
|
checks.push({ id, title, status, detail, fix });
|
|
386
603
|
|
|
387
604
|
if (!exists(projectRoot)) {
|
|
388
|
-
|
|
389
|
-
process.exit(EXIT_FAIL);
|
|
605
|
+
throw new Error(`项目根目录不存在: ${projectRoot}`);
|
|
390
606
|
}
|
|
391
607
|
|
|
392
608
|
// 1. 契约存在
|
|
@@ -435,12 +651,12 @@ function run(projectRoot) {
|
|
|
435
651
|
structStatus === "PASS"
|
|
436
652
|
? `${parsed.roleNames.length} 个角色全部在角色库;regions 与 areas 一致(${parsed.info.areaCount} 个区域);页面绑定无悬空引用`
|
|
437
653
|
: structStatus === "WARN"
|
|
438
|
-
? "角色库目录不可达,跳过「角色名在库」校验(regions↔areas
|
|
654
|
+
? "角色库目录不可达,跳过「角色名在库」校验(regions↔areas 与页面绑定校验已通过)—— 分发形态下 project-ui-design 未与本项目技能同装属预期降级"
|
|
439
655
|
: struct.problems.join(";"),
|
|
440
656
|
structStatus === "FAIL"
|
|
441
657
|
? "设计侧结构违规 —— 回设计侧修正契约(角色名须在库、layout.regions 与 layout.areas 一致、绑定的区域名必须存在),重新出契约。"
|
|
442
658
|
: structStatus === "WARN"
|
|
443
|
-
? "
|
|
659
|
+
? "预期降级,不阻断落地。如需启用角色白名单校验:将 project-ui-design 与 vue3-temp-skill 同级安装,或设置环境变量 UI_DESIGN_LIBRARY_DIR 指向 project-ui-design/references/library 后重跑。"
|
|
444
660
|
: null
|
|
445
661
|
);
|
|
446
662
|
if (structStatus === "FAIL") {
|
|
@@ -481,21 +697,26 @@ function run(projectRoot) {
|
|
|
481
697
|
);
|
|
482
698
|
}
|
|
483
699
|
|
|
484
|
-
// 6. 角色类挂载
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
700
|
+
// 6. 角色类挂载 —— 必需集 = 页面绑定(nodes/regions)引用的角色;
|
|
701
|
+
// 契约角色库大于单页用量时(可选复用角色),未引用角色的缺失只提示不计入告警
|
|
702
|
+
const requiredRoleSet = boundRoles(contract);
|
|
703
|
+
const mount = checkMounting(projectRoot, parsed.roleNames, requiredRoleSet);
|
|
704
|
+
const optionalCount = mount.optionalMounted + mount.optionalMissing.length;
|
|
705
|
+
const requiredTotal = parsed.roleNames.length - optionalCount;
|
|
706
|
+
const requiredMounted = mount.mounted.length - mount.optionalMounted;
|
|
489
707
|
add(
|
|
490
708
|
"roles-mounted",
|
|
491
709
|
"角色类挂到模板上",
|
|
492
|
-
mount.
|
|
710
|
+
mount.missing.length === 0 ? "PASS"
|
|
493
711
|
: mount.mounted.length === 0 ? "FAIL" : "WARN",
|
|
494
|
-
`契约 ${parsed.roleNames.length}
|
|
712
|
+
`契约 ${parsed.roleNames.length} 个角色(页面绑定引用 ${requiredTotal} 个为必需,已挂载 ${requiredMounted} 个);扫描 ${mount.scannedFileCount} 个源文件` +
|
|
713
|
+
(mount.optionalMissing.length
|
|
714
|
+
? `;未引用可选角色缺失 ${mount.optionalMissing.length} 个(${mount.optionalMissing.slice(0, 4).map((s) => s.split(" ")[0]).join("、")}${mount.optionalMissing.length > 4 ? "…" : ""}),不影响落地`
|
|
715
|
+
: ""),
|
|
495
716
|
mount.mounted.length === 0
|
|
496
717
|
? "没有任何 .vue 文件出现 role-* 类名 —— 三份 CSS 全是死样式(写了规则但没元素承载)。按 adapter 的挂载映射给组件加 class。"
|
|
497
718
|
: mount.missing.length
|
|
498
|
-
?
|
|
719
|
+
? `必需角色未挂载 ${mount.missing.length} 个(CSS 已生成但页面上没有对应 class,视觉不会变): ${mount.missing.slice(0, 6).join(" | ")}${mount.missing.length > 6 ? " …" : ""}`
|
|
499
720
|
: null
|
|
500
721
|
);
|
|
501
722
|
|
|
@@ -526,51 +747,76 @@ function run(projectRoot) {
|
|
|
526
747
|
);
|
|
527
748
|
}
|
|
528
749
|
|
|
529
|
-
|
|
750
|
+
// 9. demo 内容泄漏(设计稿 sampleOnly 文案/数据不得进项目)
|
|
751
|
+
const leak = checkDemoLeak(projectRoot, contract);
|
|
752
|
+
add(
|
|
753
|
+
"no-demo-leak",
|
|
754
|
+
"设计稿 demo 内容未泄漏进项目",
|
|
755
|
+
leak.status,
|
|
756
|
+
leak.detail,
|
|
757
|
+
leak.fix
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const ok = checks.every((c) => c.status !== "FAIL");
|
|
530
761
|
return { checks, ok, projectRoot, contractInfo: parsed.info, mount };
|
|
531
762
|
}
|
|
532
763
|
|
|
533
764
|
// ────────────────────────────── 输出
|
|
534
765
|
|
|
535
766
|
function printHuman(result) {
|
|
536
|
-
const L = [];
|
|
537
|
-
L.push(
|
|
538
|
-
L.push(
|
|
539
|
-
L.push(
|
|
540
|
-
L.push(`项目根: ${result.projectRoot}`);
|
|
541
|
-
L.push("");
|
|
542
|
-
const mark = { PASS: "PASS", WARN: "WARN", FAIL: "FAIL" };
|
|
767
|
+
const L = [result.notice];
|
|
768
|
+
if (result.projectRoot) L.push(`项目根: ${result.projectRoot}`);
|
|
769
|
+
if (result.reason) L.push(result.reason);
|
|
770
|
+
if (result.error) L.push(`错误: ${result.error}`);
|
|
543
771
|
for (const c of result.checks) {
|
|
544
|
-
L.push(`[
|
|
772
|
+
L.push(`[LEGACY-TEXT-${c.status}] ${c.id} · ${c.title}`);
|
|
545
773
|
if (c.detail) L.push(` ${c.detail}`);
|
|
546
|
-
if (c.fix) L.push(`
|
|
547
|
-
}
|
|
548
|
-
L.push("");
|
|
549
|
-
L.push("-".repeat(64));
|
|
550
|
-
const firstFail = result.checks.find((c) => c.status === "FAIL");
|
|
551
|
-
if (result.ok) {
|
|
552
|
-
L.push("结论: 八项全过 —— 契约、结构、样式、挂载闭环均已接通。");
|
|
553
|
-
} else if (firstFail) {
|
|
554
|
-
L.push(`结论: 断在「${firstFail.title}」。先修这一项,重跑本脚本看下一项。`);
|
|
555
|
-
L.push("");
|
|
556
|
-
L.push("提醒: 项目侧的修正动作是「改契约或改 adapter 后重新生成」,");
|
|
557
|
-
L.push(" 不要直接改已生成的 CSS —— 下次重新生成会被覆盖。");
|
|
558
|
-
} else {
|
|
559
|
-
L.push("结论: 有 WARN 项,落地链路未全覆盖。");
|
|
774
|
+
if (c.fix) L.push(` 旧格式建议(非当前实施指令) → ${c.fix}`);
|
|
560
775
|
}
|
|
561
|
-
|
|
776
|
+
if (result.diagnosticOk) {
|
|
777
|
+
L.push("旧格式文本检查未发现 FAIL;这不证明业务正确、无 demo 泄漏或视觉还原。");
|
|
778
|
+
}
|
|
779
|
+
L.push(result.migration);
|
|
780
|
+
L.push("visualStatus=unverified;退出 2,不提供验收通过结论。");
|
|
562
781
|
process.stdout.write(L.join("\n") + "\n");
|
|
563
782
|
}
|
|
564
783
|
|
|
565
|
-
function main() {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
784
|
+
export function main(argv = process.argv.slice(2)) {
|
|
785
|
+
const result = {
|
|
786
|
+
mode: "disabled",
|
|
787
|
+
ok: false,
|
|
788
|
+
visualStatus: "unverified",
|
|
789
|
+
notice: NOTICE,
|
|
790
|
+
migration: MIGRATION,
|
|
791
|
+
checks: [],
|
|
792
|
+
};
|
|
793
|
+
let json = argv.includes("--json");
|
|
794
|
+
try {
|
|
795
|
+
const args = parseArgs(argv);
|
|
796
|
+
json = args.json;
|
|
797
|
+
if (args.help) {
|
|
798
|
+
console.log(HELP);
|
|
799
|
+
return EXIT_OK;
|
|
800
|
+
}
|
|
801
|
+
if (args.legacyDiagnostics) {
|
|
802
|
+
result.mode = "legacy-diagnostics";
|
|
803
|
+
const { ok: diagnosticOk, ...diagnostic } = run(args.project);
|
|
804
|
+
Object.assign(result, diagnostic, { diagnosticOk });
|
|
805
|
+
} else {
|
|
806
|
+
result.reason = "默认不读取项目;只有显式 --legacy-diagnostics 才运行旧的只读文本诊断。";
|
|
807
|
+
}
|
|
808
|
+
} catch (error) {
|
|
809
|
+
result.error = error.message;
|
|
810
|
+
result.diagnosticOk = false;
|
|
811
|
+
}
|
|
812
|
+
if (json) {
|
|
569
813
|
console.log(JSON.stringify(result, null, 2));
|
|
570
814
|
} else {
|
|
571
815
|
printHuman(result);
|
|
572
816
|
}
|
|
573
|
-
|
|
817
|
+
return EXIT_UNVERIFIED;
|
|
574
818
|
}
|
|
575
819
|
|
|
576
|
-
|
|
820
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
821
|
+
process.exitCode = main();
|
|
822
|
+
}
|