@qilitt-mickey/vue3-temp-skill 1.2.6 → 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.
@@ -1,20 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * 契约落地自检 —— 判断「契约 → 样式 → 挂载」三个闭环在哪一环断了
4
+ * 旧格式文本诊断 —— 默认禁用,不提供业务/视觉验收结论。
5
5
  *
6
6
  * 用法:
7
- * node check-landing.mjs --project <项目根目录>
8
- * node check-landing.mjs --project . --json
7
+ * node check-landing.mjs --help
8
+ * node check-landing.mjs --legacy-diagnostics --project <项目根目录> [--json]
9
9
  *
10
- * 退出码: 0 = 八项全过 / 1 = 有 FAIL 项
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 { fileURLToPath } from "node:url";
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",
@@ -30,27 +31,37 @@ const SOURCE_EXT = new Set([".vue", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"
30
31
  const STYLE_EXT = new Set([".css", ".scss", ".less"]);
31
32
 
32
33
  const EXIT_OK = 0;
33
- const EXIT_FAIL = 1;
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。`;
34
43
 
35
44
  // ────────────────────────────── 参数解析
36
45
 
37
46
  function parseArgs(argv) {
38
- const out = { project: process.cwd(), json: false };
47
+ const out = { project: process.cwd(), json: false, legacyDiagnostics: false, help: false };
39
48
  for (let i = 0; i < argv.length; i++) {
40
49
  const a = argv[i];
41
50
  if (a === "--project" || a === "-p") {
42
51
  const v = argv[i + 1];
43
- if (!v || v.startsWith("--")) {
44
- console.error("错误: --project 后面必须跟项目根目录路径");
45
- process.exit(EXIT_FAIL);
52
+ if (!v || v.startsWith("-")) {
53
+ throw new Error("--project 后面必须跟项目根目录路径");
46
54
  }
47
55
  out.project = path.resolve(v);
48
56
  i++;
49
57
  } else if (a === "--json") {
50
58
  out.json = true;
59
+ } else if (a === "--legacy-diagnostics") {
60
+ out.legacyDiagnostics = true;
51
61
  } else if (a === "--help" || a === "-h") {
52
- console.log("用法: node check-landing.mjs --project <项目根目录> [--json]");
53
- process.exit(EXIT_OK);
62
+ out.help = true;
63
+ } else {
64
+ throw new Error(`未知参数: ${a}`);
54
65
  }
55
66
  }
56
67
  return out;
@@ -319,10 +330,9 @@ function resolveLibraryDir() {
319
330
  return { dir: null, tried: candidates };
320
331
  }
321
332
 
322
- const { dir: LIBRARY_DIR } = resolveLibraryDir();
323
-
324
333
  /** 加载角色库名集合:<场景>.json + components.json;目录不可达返回 null(降级 WARN) */
325
334
  function loadRoleLibrary(scene) {
335
+ const { dir: LIBRARY_DIR } = resolveLibraryDir();
326
336
  const merged = new Set();
327
337
  if (!LIBRARY_DIR) return null;
328
338
  for (const f of [`${scene}.json`, "components.json"]) {
@@ -431,6 +441,159 @@ function checkCssRules(cssPathOf, contract, roleNames) {
431
441
  return missing;
432
442
  }
433
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
+
434
597
  // ────────────────────────────── 主流程
435
598
 
436
599
  function run(projectRoot) {
@@ -439,8 +602,7 @@ function run(projectRoot) {
439
602
  checks.push({ id, title, status, detail, fix });
440
603
 
441
604
  if (!exists(projectRoot)) {
442
- console.error(`项目根目录不存在: ${projectRoot}`);
443
- process.exit(EXIT_FAIL);
605
+ throw new Error(`项目根目录不存在: ${projectRoot}`);
444
606
  }
445
607
 
446
608
  // 1. 契约存在
@@ -585,51 +747,76 @@ function run(projectRoot) {
585
747
  );
586
748
  }
587
749
 
588
- const ok = checks.every((c) => c.status === "PASS");
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");
589
761
  return { checks, ok, projectRoot, contractInfo: parsed.info, mount };
590
762
  }
591
763
 
592
764
  // ────────────────────────────── 输出
593
765
 
594
766
  function printHuman(result) {
595
- const L = [];
596
- L.push("");
597
- L.push("契约落地自检");
598
- L.push("=".repeat(64));
599
- L.push(`项目根: ${result.projectRoot}`);
600
- L.push("");
601
- 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}`);
602
771
  for (const c of result.checks) {
603
- L.push(`[${mark[c.status]}] ${c.id} · ${c.title}`);
772
+ L.push(`[LEGACY-TEXT-${c.status}] ${c.id} · ${c.title}`);
604
773
  if (c.detail) L.push(` ${c.detail}`);
605
- if (c.fix) L.push(` 修复 → ${c.fix}`);
606
- }
607
- L.push("");
608
- L.push("-".repeat(64));
609
- const firstFail = result.checks.find((c) => c.status === "FAIL");
610
- if (result.ok) {
611
- L.push("结论: 八项全过 —— 契约、结构、样式、挂载闭环均已接通。");
612
- } else if (firstFail) {
613
- L.push(`结论: 断在「${firstFail.title}」。先修这一项,重跑本脚本看下一项。`);
614
- L.push("");
615
- L.push("提醒: 项目侧的修正动作是「改契约或改 adapter 后重新生成」,");
616
- L.push(" 不要直接改已生成的 CSS —— 下次重新生成会被覆盖。");
617
- } else {
618
- L.push("结论: 有 WARN 项,落地链路未全覆盖。");
774
+ if (c.fix) L.push(` 旧格式建议(非当前实施指令) → ${c.fix}`);
775
+ }
776
+ if (result.diagnosticOk) {
777
+ L.push("旧格式文本检查未发现 FAIL;这不证明业务正确、无 demo 泄漏或视觉还原。");
619
778
  }
620
- L.push("");
779
+ L.push(result.migration);
780
+ L.push("visualStatus=unverified;退出 2,不提供验收通过结论。");
621
781
  process.stdout.write(L.join("\n") + "\n");
622
782
  }
623
783
 
624
- function main() {
625
- const args = parseArgs(process.argv.slice(2));
626
- const result = run(args.project);
627
- if (args.json) {
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) {
628
813
  console.log(JSON.stringify(result, null, 2));
629
814
  } else {
630
815
  printHuman(result);
631
816
  }
632
- process.exit(result.ok ? EXIT_OK : EXIT_FAIL);
817
+ return EXIT_UNVERIFIED;
633
818
  }
634
819
 
635
- main();
820
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
821
+ process.exitCode = main();
822
+ }