@qilitt-mickey/vue3-temp-skill 1.2.1 → 1.2.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.
@@ -0,0 +1,576 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 契约落地自检 —— 判断「契约 → 样式 → 挂载」三个闭环在哪一环断了
5
+ *
6
+ * 用法:
7
+ * node check-landing.mjs --project <项目根目录>
8
+ * node check-landing.mjs --project . --json
9
+ *
10
+ * 退出码: 0 = 八项全过 / 1 = 有 FAIL 项
11
+ *
12
+ * 设计原则: 纯读取 + 确定性判断。不猜测、不修改、不联网。
13
+ */
14
+
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const IGNORE_DIRS = new Set([
20
+ "node_modules", "dist", ".git", ".vite", "coverage",
21
+ "__pycache__", ".idea", ".vscode", ".workbuddy", "unpackage",
22
+ ]);
23
+
24
+ const CSS_FILES = ["ui-tokens.css", "ui-roles.css", "ui-layout.css"];
25
+ const SOURCE_EXT = new Set([".vue", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
26
+ const STYLE_EXT = new Set([".css", ".scss", ".less"]);
27
+
28
+ const EXIT_OK = 0;
29
+ const EXIT_FAIL = 1;
30
+
31
+ // ────────────────────────────── 参数解析
32
+
33
+ function parseArgs(argv) {
34
+ const out = { project: process.cwd(), json: false };
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const a = argv[i];
37
+ if (a === "--project" || a === "-p") {
38
+ const v = argv[i + 1];
39
+ if (!v || v.startsWith("--")) {
40
+ console.error("错误: --project 后面必须跟项目根目录路径");
41
+ process.exit(EXIT_FAIL);
42
+ }
43
+ out.project = path.resolve(v);
44
+ i++;
45
+ } else if (a === "--json") {
46
+ out.json = true;
47
+ } else if (a === "--help" || a === "-h") {
48
+ console.log("用法: node check-landing.mjs --project <项目根目录> [--json]");
49
+ process.exit(EXIT_OK);
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+
55
+ // ────────────────────────────── 文件遍历
56
+
57
+ function walk(dir, extSet, acc = []) {
58
+ let entries;
59
+ try {
60
+ entries = fs.readdirSync(dir, { withFileTypes: true });
61
+ } catch {
62
+ return acc;
63
+ }
64
+ for (const e of entries) {
65
+ if (e.isDirectory()) {
66
+ if (IGNORE_DIRS.has(e.name)) continue;
67
+ walk(path.join(dir, e.name), extSet, acc);
68
+ } else if (e.isFile()) {
69
+ if (extSet === null || extSet.has(path.extname(e.name).toLowerCase())) {
70
+ acc.push(path.join(dir, e.name));
71
+ }
72
+ }
73
+ }
74
+ return acc;
75
+ }
76
+
77
+ function readSafe(p) {
78
+ try {
79
+ return fs.readFileSync(p, "utf-8");
80
+ } catch {
81
+ return "";
82
+ }
83
+ }
84
+
85
+ function rel(root, p) {
86
+ return path.relative(root, p).split(path.sep).join("/");
87
+ }
88
+
89
+ function exists(p) {
90
+ try {
91
+ fs.statSync(p);
92
+ return true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ // ────────────────────────────── 契约侧
99
+
100
+ function findContract(projectRoot) {
101
+ const designDir = path.join(projectRoot, ".design");
102
+ if (!exists(designDir)) return { file: null, reason: "项目根下没有 .design/ 目录" };
103
+
104
+ let names;
105
+ try {
106
+ names = fs.readdirSync(designDir);
107
+ } catch {
108
+ return { file: null, reason: ".design/ 目录无法读取" };
109
+ }
110
+ const hit = names.find((n) => /^visual-contract.*\.json$/.test(n));
111
+ if (!hit) {
112
+ return { file: null, reason: ".design/ 下没有 visual-contract-*.json" };
113
+ }
114
+ return { file: path.join(designDir, hit), name: hit, reason: null };
115
+ }
116
+
117
+ function inspectContract(contract) {
118
+ const problems = [];
119
+ const warns = [];
120
+ const info = {};
121
+
122
+ if (!contract || typeof contract !== "object") {
123
+ return { ok: false, problems: ["契约内容不是合法 JSON 对象"], warns, info };
124
+ }
125
+
126
+ info.schema = contract.schema || "(缺失)";
127
+ if (contract.schema !== "ui.visual-contract/1.0") {
128
+ warns.push(`schema 是 "${contract.schema}",期望 "ui.visual-contract/1.0"`);
129
+ }
130
+
131
+ const meta = contract.meta || {};
132
+ info.scene = meta.scene || "(缺失)";
133
+ info.pageTypes = Array.isArray(meta.pageTypes) ? meta.pageTypes.join(",") : "(缺失)";
134
+ info.mockup = (meta.source && meta.source.mockup) || "(缺失)";
135
+ if (!meta.scene) problems.push("meta.scene 缺失 —— 项目侧无法判断用哪套角色与布局预期");
136
+ if (!meta.source || !meta.source.mockup) {
137
+ problems.push("meta.source.mockup 缺失 —— 目视对照没有比对目标");
138
+ }
139
+
140
+ const colorTokens = (contract.tokens && contract.tokens.color) || {};
141
+ info.tokenCount = Object.keys(colorTokens).length;
142
+ if (info.tokenCount === 0) problems.push("tokens.color 为空 —— 令牌层没有值");
143
+
144
+ const roles = contract.roles && typeof contract.roles === "object" ? contract.roles : {};
145
+ const roleNames = Object.keys(roles);
146
+ info.roleCount = roleNames.length;
147
+ const emptyRoles = roleNames.filter((n) => {
148
+ const r = roles[n] || {};
149
+ const hasHost = Array.isArray(r.host) && r.host.length > 0;
150
+ const hasInner = Array.isArray(r.inner) && r.inner.length > 0;
151
+ return !hasHost && !hasInner;
152
+ });
153
+ info.emptyRoles = emptyRoles;
154
+ if (roleNames.length === 0) {
155
+ problems.push("roles 为空 —— 角色层没抽出来,三份 CSS 只能生成空壳");
156
+ } else if (emptyRoles.length > 0) {
157
+ problems.push(`这些角色没有任何声明: ${emptyRoles.join(", ")}`);
158
+ }
159
+ if (roleNames.length > 0 && roleNames.length < 5) {
160
+ warns.push(`契约只有 ${roleNames.length} 个角色,很可能设计侧抽取不完整(一页通常 20+ 个角色)`);
161
+ }
162
+
163
+ const layout = contract.layout || {};
164
+ const areas = Array.isArray(layout.areas) ? layout.areas : [];
165
+ info.areaCount = areas.length;
166
+ info.grid = layout.grid || "(缺失)";
167
+ if (areas.length === 0) {
168
+ problems.push("layout.areas 为空 —— 布局层没定,骨架无法生成");
169
+ }
170
+
171
+ const pages = contract.pages || {};
172
+ info.pageTypesDeclared = Object.keys(pages).join(",");
173
+ if (Object.keys(pages).length === 0) {
174
+ warns.push("pages 为空 —— 没有区域到角色的绑定,区域内部不知道该用什么角色");
175
+ }
176
+
177
+ return { ok: problems.length === 0, problems, warns, info, roleNames };
178
+ }
179
+
180
+ // ────────────────────────────── CSS 侧
181
+
182
+ function findGeneratedCss(projectRoot) {
183
+ const found = { files: [], pathOf: {} };
184
+ const allFiles = walk(projectRoot, null, []);
185
+ for (const name of CSS_FILES) {
186
+ const hits = allFiles.filter((p) => path.basename(p) === name);
187
+ if (hits.length > 0) {
188
+ found.files.push(hits[0]);
189
+ found.pathOf[name] = hits[0];
190
+ }
191
+ }
192
+ return found;
193
+ }
194
+
195
+ function checkImported(projectRoot, cssFileAbs) {
196
+ const base = path.basename(cssFileAbs);
197
+ const sources = walk(projectRoot, SOURCE_EXT, []);
198
+ const refs = [];
199
+ for (const s of sources) {
200
+ const content = readSafe(s);
201
+ if (content.includes(base)) refs.push(rel(projectRoot, s));
202
+ }
203
+ const styleRefs = [];
204
+ for (const s of walk(projectRoot, STYLE_EXT, [])) {
205
+ if (path.basename(s) === base) continue;
206
+ const content = readSafe(s);
207
+ if (content.includes(base)) styleRefs.push(rel(projectRoot, s));
208
+ }
209
+ return { imported: refs.length > 0 || styleRefs.length > 0, refs, styleRefs };
210
+ }
211
+
212
+ // ────────────────────────────── 挂载侧
213
+
214
+ function expectedClassName(roleName) {
215
+ return (
216
+ "role-" +
217
+ roleName
218
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
219
+ .replace(/\./g, "-")
220
+ .replace(/_/g, "-")
221
+ .toLowerCase()
222
+ );
223
+ }
224
+
225
+ function checkMounting(projectRoot, roleNames) {
226
+ const vueFiles = walk(projectRoot, SOURCE_EXT, []);
227
+ const roleClassRe = /\brole-[a-z0-9]+(?:-[a-z0-9]+)*/g;
228
+ const usedClasses = new Map();
229
+ for (const s of vueFiles) {
230
+ const content = readSafe(s);
231
+ const hits = content.match(roleClassRe);
232
+ if (!hits) continue;
233
+ for (const h of hits) {
234
+ if (!usedClasses.has(h)) usedClasses.set(h, new Set());
235
+ usedClasses.get(h).add(rel(projectRoot, s));
236
+ }
237
+ }
238
+
239
+ const expected = roleNames.map(expectedClassName);
240
+ const mounted = [];
241
+ const missing = [];
242
+ for (let i = 0; i < roleNames.length; i++) {
243
+ if (usedClasses.has(expected[i])) mounted.push(expected[i]);
244
+ else missing.push(`${roleNames[i]} → .${expected[i]}`);
245
+ }
246
+
247
+ const layoutClassRe = /\bui-(?:page|region-[a-z0-9]+(?:-[a-z0-9]+)*)\b/g;
248
+ const layoutClasses = new Set();
249
+ for (const s of vueFiles) {
250
+ const content = readSafe(s);
251
+ const hits = content.match(layoutClassRe);
252
+ if (hits) for (const h of hits) layoutClasses.add(h);
253
+ }
254
+
255
+ return {
256
+ scannedFileCount: vueFiles.length,
257
+ mounted,
258
+ missing,
259
+ layoutClasses: [...layoutClasses],
260
+ hasLayout: layoutClasses.has("ui-page"),
261
+ };
262
+ }
263
+
264
+ // ────────────────────────────── 结构校验(契约 §7)
265
+
266
+ const LIBRARY_DIR = path.resolve(
267
+ path.dirname(fileURLToPath(import.meta.url)),
268
+ "../../project-ui-design/references/library"
269
+ );
270
+
271
+ /** 加载角色库名集合:<场景>.json + components.json;目录不可达返回 null(降级 WARN) */
272
+ function loadRoleLibrary(scene) {
273
+ const merged = new Set();
274
+ for (const f of [`${scene}.json`, "components.json"]) {
275
+ const p = path.join(LIBRARY_DIR, f);
276
+ if (!exists(p)) continue;
277
+ try {
278
+ const lib = JSON.parse(readSafe(p));
279
+ for (const k of Object.keys(lib.roles || {})) merged.add(k);
280
+ } catch {
281
+ /* 单个角色库文件损坏时跳过,由调用方按降级处理 */
282
+ }
283
+ }
284
+ return merged.size > 0 ? merged : null;
285
+ }
286
+
287
+ /** 契约 §7 结构四条中可机判的三条:角色名在库 / regions↔areas 一致 / 绑定区域存在 */
288
+ function checkContractStructure(contract, roleNames, library) {
289
+ const problems = [];
290
+
291
+ if (library) {
292
+ const unknown = roleNames.filter((n) => !library.has(n));
293
+ if (unknown.length > 0) {
294
+ problems.push(`角色不在角色库: ${unknown.join(", ")}`);
295
+ }
296
+ }
297
+
298
+ const layout = contract.layout || {};
299
+ const areaNames = (Array.isArray(layout.areas) ? layout.areas : [])
300
+ .map((a) => a && a.name)
301
+ .filter(Boolean);
302
+ const areasSet = new Set(areaNames);
303
+ const regionKeys = Object.keys(layout.regions || {});
304
+ const onlyRegions = regionKeys.filter((k) => !areasSet.has(k));
305
+ const onlyAreas = areaNames.filter((n) => !regionKeys.includes(n));
306
+ if (onlyRegions.length > 0) {
307
+ problems.push(`layout.regions 有而 layout.areas 没有: ${onlyRegions.join(", ")}`);
308
+ }
309
+ if (onlyAreas.length > 0) {
310
+ problems.push(`layout.areas 有而 layout.regions 没有: ${onlyAreas.join(", ")}`);
311
+ }
312
+
313
+ const pages = contract.pages || {};
314
+ for (const [pt, page] of Object.entries(pages)) {
315
+ const p = page || {};
316
+ for (const b of Array.isArray(p.regions) ? p.regions : []) {
317
+ if (b && b.region && !areasSet.has(b.region)) {
318
+ problems.push(`pages.${pt}.regions 绑定了不存在的区域: ${b.region}`);
319
+ }
320
+ }
321
+ for (const nd of Array.isArray(p.nodes) ? p.nodes : []) {
322
+ if (nd && nd.in && !areasSet.has(nd.in)) {
323
+ problems.push(`pages.${pt}.nodes 绑定了不存在的区域: ${nd.in}`);
324
+ }
325
+ }
326
+ }
327
+ return { problems };
328
+ }
329
+
330
+ // ────────────────────────────── CSS 规则完整性
331
+
332
+ /** 逐条核对:ui-roles 含每个角色类、ui-layout 含每个区域类、ui-tokens 含每个令牌变量 */
333
+ function checkCssRules(cssPathOf, contract, roleNames) {
334
+ const missing = [];
335
+
336
+ const rolesCss = readSafe(cssPathOf["ui-roles.css"] || "");
337
+ if (rolesCss) {
338
+ for (const rn of roleNames) {
339
+ const cls = "." + expectedClassName(rn);
340
+ if (!rolesCss.includes(cls)) missing.push(`ui-roles.css 缺 ${cls}`);
341
+ }
342
+ } else {
343
+ missing.push("ui-roles.css 无法读取");
344
+ }
345
+
346
+ const layoutCss = readSafe(cssPathOf["ui-layout.css"] || "");
347
+ if (layoutCss) {
348
+ const areas = Array.isArray(contract.layout && contract.layout.areas)
349
+ ? contract.layout.areas
350
+ : [];
351
+ for (const a of areas) {
352
+ if (!a || !a.name) continue;
353
+ const cls = `.ui-region-${a.name}`;
354
+ if (!layoutCss.includes(cls)) missing.push(`ui-layout.css 缺 ${cls}`);
355
+ }
356
+ } else {
357
+ missing.push("ui-layout.css 无法读取");
358
+ }
359
+
360
+ const tokensCss = readSafe(cssPathOf["ui-tokens.css"] || "");
361
+ if (tokensCss) {
362
+ const tokens = contract.tokens || {};
363
+ for (const [family, names] of Object.entries(tokens)) {
364
+ if (!names || typeof names !== "object") continue;
365
+ for (const n of Object.keys(names)) {
366
+ const v = `--${family}-${String(n).replace(/\./g, "-")}`;
367
+ const alt = `--${family}-${String(n).replace(/\./g, "_")}`;
368
+ if (!tokensCss.includes(v) && !tokensCss.includes(alt)) {
369
+ missing.push(`ui-tokens.css 缺 ${v}`);
370
+ }
371
+ }
372
+ }
373
+ } else {
374
+ missing.push("ui-tokens.css 无法读取");
375
+ }
376
+
377
+ return missing;
378
+ }
379
+
380
+ // ────────────────────────────── 主流程
381
+
382
+ function run(projectRoot) {
383
+ const checks = [];
384
+ const add = (id, title, status, detail, fix) =>
385
+ checks.push({ id, title, status, detail, fix });
386
+
387
+ if (!exists(projectRoot)) {
388
+ console.error(`项目根目录不存在: ${projectRoot}`);
389
+ process.exit(EXIT_FAIL);
390
+ }
391
+
392
+ // 1. 契约存在
393
+ const cf = findContract(projectRoot);
394
+ if (!cf.file) {
395
+ add("contract-found", "契约文件存在", "FAIL", cf.reason,
396
+ "设计侧没有落盘契约。检查是否在项目根目录运行、是否完成了第 6 步「出契约」。");
397
+ return { checks, ok: false, projectRoot };
398
+ }
399
+ add("contract-found", "契约文件存在", "PASS", `.design/${cf.name}`, null);
400
+
401
+ // 2. 契约完整
402
+ let contract = null;
403
+ let parsed = null;
404
+ try {
405
+ contract = JSON.parse(readSafe(cf.file));
406
+ } catch (e) {
407
+ add("contract-valid", "契约内容完整", "FAIL", `JSON 解析失败: ${e.message}`,
408
+ "契约是手工编辑过的吗?重新用设计侧生成一份。");
409
+ return { checks, ok: false, projectRoot };
410
+ }
411
+ parsed = inspectContract(contract);
412
+ add(
413
+ "contract-valid",
414
+ "契约内容完整",
415
+ parsed.ok ? "PASS" : "FAIL",
416
+ `场景=${parsed.info.scene} 页型=${parsed.info.pageTypes} 角色=${parsed.info.roleCount} 区域=${parsed.info.areaCount}`,
417
+ parsed.problems.length ? parsed.problems.join(";") : null
418
+ );
419
+ for (const w of parsed.warns || []) {
420
+ add(`contract-warn-${checks.length}`, "契约告警", "WARN", w, null);
421
+ }
422
+ if (!parsed.ok) {
423
+ return { checks, ok: false, projectRoot, contractInfo: parsed.info };
424
+ }
425
+
426
+ // 3. 契约结构(角色名在库 / regions↔areas 一致 / 页面绑定不悬空)
427
+ const library = loadRoleLibrary(parsed.info.scene);
428
+ const struct = checkContractStructure(contract, parsed.roleNames, library);
429
+ const structStatus =
430
+ struct.problems.length > 0 ? "FAIL" : library ? "PASS" : "WARN";
431
+ add(
432
+ "contract-structure",
433
+ "契约结构符合角色库与布局约束",
434
+ structStatus,
435
+ structStatus === "PASS"
436
+ ? `${parsed.roleNames.length} 个角色全部在角色库;regions 与 areas 一致(${parsed.info.areaCount} 个区域);页面绑定无悬空引用`
437
+ : structStatus === "WARN"
438
+ ? "角色库目录不可达,跳过「角色名在库」校验(regions↔areas 与页面绑定校验已通过)"
439
+ : struct.problems.join(";"),
440
+ structStatus === "FAIL"
441
+ ? "设计侧结构违规 —— 回设计侧修正契约(角色名须在库、layout.regions 与 layout.areas 一致、绑定的区域名必须存在),重新出契约。"
442
+ : structStatus === "WARN"
443
+ ? "确认技能包完整(project-ui-design/references/library 存在)后重跑。"
444
+ : null
445
+ );
446
+ if (structStatus === "FAIL") {
447
+ return { checks, ok: false, projectRoot, contractInfo: parsed.info };
448
+ }
449
+
450
+ // 4. 三份 CSS 生成
451
+ const css = findGeneratedCss(projectRoot);
452
+ const missingCss = CSS_FILES.filter((n) => !css.pathOf[n]);
453
+ add(
454
+ "css-generated",
455
+ "三份 CSS 已生成",
456
+ missingCss.length === 0 ? "PASS" : "FAIL",
457
+ missingCss.length === 0
458
+ ? CSS_FILES.map((n) => rel(projectRoot, css.pathOf[n])).join(" ")
459
+ : `缺: ${missingCss.join(", ")}`,
460
+ missingCss.length
461
+ ? "项目侧未完成第 3/4/5 步。按 design-apply.md 的翻译规则生成这三份文件。"
462
+ : null
463
+ );
464
+ if (missingCss.length === 0) {
465
+ // 5. CSS 被引入
466
+ const unimported = [];
467
+ const importDetail = [];
468
+ for (const n of CSS_FILES) {
469
+ const r = checkImported(projectRoot, css.pathOf[n]);
470
+ if (!r.imported) unimported.push(n);
471
+ importDetail.push(`${n}:${r.imported ? "已引入 " + (r.refs[0] || r.styleRefs[0]) : "未引入"}`);
472
+ }
473
+ add(
474
+ "css-imported",
475
+ "三份 CSS 被入口引入",
476
+ unimported.length === 0 ? "PASS" : "FAIL",
477
+ importDetail.join(" | "),
478
+ unimported.length
479
+ ? `${unimported.join(", ")} 生成了但没被任何文件 import —— 浏览器不会加载它们。在样式入口 import 一次。`
480
+ : null
481
+ );
482
+ }
483
+
484
+ // 6. 角色类挂载
485
+ const mount = checkMounting(projectRoot, parsed.roleNames);
486
+ const mountRatio = parsed.roleNames.length
487
+ ? mount.mounted.length / parsed.roleNames.length
488
+ : 0;
489
+ add(
490
+ "roles-mounted",
491
+ "角色类挂到模板上",
492
+ mount.mounted.length === parsed.roleNames.length ? "PASS"
493
+ : mount.mounted.length === 0 ? "FAIL" : "WARN",
494
+ `契约 ${parsed.roleNames.length} 个角色,模板挂载 ${mount.mounted.length} 个(覆盖率 ${(mountRatio * 100).toFixed(0)}%);扫描 ${mount.scannedFileCount} 个源文件`,
495
+ mount.mounted.length === 0
496
+ ? "没有任何 .vue 文件出现 role-* 类名 —— 三份 CSS 全是死样式(写了规则但没元素承载)。按 adapter 的挂载映射给组件加 class。"
497
+ : mount.missing.length
498
+ ? `未挂载 ${mount.missing.length} 个角色(CSS 已生成但页面上没有对应 class,视觉不会变): ${mount.missing.slice(0, 6).join(" | ")}${mount.missing.length > 6 ? " …" : ""}`
499
+ : null
500
+ );
501
+
502
+ // 7. 布局类挂载
503
+ add(
504
+ "layout-mounted",
505
+ "布局类挂到容器上",
506
+ mount.hasLayout ? "PASS" : "FAIL",
507
+ mount.hasLayout
508
+ ? `布局类: ${mount.layoutClasses.join(", ")}`
509
+ : "没有任何 .vue 文件出现 .ui-page —— 布局层未接入,骨架仍是项目原有结构",
510
+ mount.hasLayout ? null : "布局组件需加 .ui-page 与 .ui-region-* 类,见 adapter 的「布局占位容器」。"
511
+ );
512
+
513
+ // 8. CSS 规则完整性(模板挂了类但 CSS 没规则 = 生成不完整或被手改)
514
+ if (missingCss.length === 0) {
515
+ const missingRules = checkCssRules(css.pathOf, contract, parsed.roleNames);
516
+ add(
517
+ "css-rules",
518
+ "CSS 规则覆盖契约全部声明",
519
+ missingRules.length === 0 ? "PASS" : "FAIL",
520
+ missingRules.length === 0
521
+ ? `ui-tokens / ui-roles / ui-layout 已覆盖契约全部令牌、${parsed.info.roleCount} 个角色与 ${parsed.info.areaCount} 个区域`
522
+ : `缺 ${missingRules.length} 条: ${missingRules.slice(0, 8).join(" | ")}${missingRules.length > 8 ? " …" : ""}`,
523
+ missingRules.length
524
+ ? "模板挂了类但 CSS 里没有对应规则 —— 生成不完整或生成后被手工改动。回第 3/4/5 步重新生成,禁止手补 CSS。"
525
+ : null
526
+ );
527
+ }
528
+
529
+ const ok = checks.every((c) => c.status === "PASS");
530
+ return { checks, ok, projectRoot, contractInfo: parsed.info, mount };
531
+ }
532
+
533
+ // ────────────────────────────── 输出
534
+
535
+ function printHuman(result) {
536
+ const L = [];
537
+ L.push("");
538
+ L.push("契约落地自检");
539
+ L.push("=".repeat(64));
540
+ L.push(`项目根: ${result.projectRoot}`);
541
+ L.push("");
542
+ const mark = { PASS: "PASS", WARN: "WARN", FAIL: "FAIL" };
543
+ for (const c of result.checks) {
544
+ L.push(`[${mark[c.status]}] ${c.id} · ${c.title}`);
545
+ if (c.detail) L.push(` ${c.detail}`);
546
+ if (c.fix) L.push(` 修复 → ${c.fix}`);
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 项,落地链路未全覆盖。");
560
+ }
561
+ L.push("");
562
+ process.stdout.write(L.join("\n") + "\n");
563
+ }
564
+
565
+ function main() {
566
+ const args = parseArgs(process.argv.slice(2));
567
+ const result = run(args.project);
568
+ if (args.json) {
569
+ console.log(JSON.stringify(result, null, 2));
570
+ } else {
571
+ printHuman(result);
572
+ }
573
+ process.exit(result.ok ? EXIT_OK : EXIT_FAIL);
574
+ }
575
+
576
+ main();