@qilitt-mickey/vue3-temp-skill 1.1.41 → 1.1.43

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/parser/index.js CHANGED
@@ -18,6 +18,8 @@
18
18
  * node parser/index.js match --batch <dir> # 项目级批量匹配目录下全部变更集
19
19
  * node parser/index.js list [--scene admin] [--verbose] # 列出全部语义 ID(--verbose 含字段与形态)
20
20
  * node parser/index.js query <semantic.id> # 查询单个 ID 映射详情
21
+ * node parser/index.js audit --project <dir> [<cs…>] # 挂载点存在性 + 死通道静态审计(V3.2)
22
+ * node parser/index.js verify --oracle <o> --actual <a> # 渲染态逐点验收:computed style 期望 vs 实测 diff(V3.2)
21
23
  * node parser/watch.js # 开发态监听 mapping/ 自动重编译(§3.4.3)
22
24
  */
23
25
 
@@ -33,11 +35,26 @@ const ROOT = path.resolve(__dirname, "..");
33
35
 
34
36
  /** 定性值(非色彩类语义描述,如 中等/全圆角/宽松):不参与译表,AI 按字段语义施工,不告警。
35
37
  * 注意:模块级常量必须在 main() 调用前声明(TDZ),勿下移到文件底部。 */
36
- const QUALITATIVE_RE = /^(中等|加粗|常规|细|粗|小圆角|全圆角|直角|宽松|紧凑|标准|大|中|小|等宽数字)$/;
38
+ const QUALITATIVE_RE = /^(中等|加粗|常规|细|粗|小圆角|全圆角|直角|宽松|紧凑|标准|大|中|小|等宽数字|等宽|左对齐|居中|右对齐|两端对齐|不换行|换行)$/;
37
39
  /** 数值字面量:带 CSS 单位的精确尺寸(40px / 1.5rem / 50%)——设计侧已消歧的精确值,透传不提示 */
38
40
  const NUMERIC_VALUE_RE = /^\d+(\.\d+)?(px|rem|em|%|vh|vw|pt)$/i;
39
41
  /** 复合样式字面量:含空格的多段值(如 1px solid #F0F0F0)按已消歧处理(hex 已被禁词校验限定位置) */
40
42
  const COMPOSITE_VALUE_RE = /\s/;
43
+ /** V3.2 落点层级枚举(与词典 validator 的 LAYER_ENUM 一致) */
44
+ const LAYER_VALUES = ["host", "inner"];
45
+ /** 语义 ID 严格格式(apply_via 锚点名须符合) */
46
+ const ID_RE_STRICT = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$/;
47
+
48
+ /** V3.2 audit:审计时跳过的目录(依赖 / 产物 / 缓存) */
49
+ const AUDIT_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".output", ".vite", ".nuxt", "coverage", "public"]);
50
+ /** V3.2 audit:「模板 / 脚本」扩展名:命中 = 结构挂载 */
51
+ const STRUCT_EXT = new Set([".vue", ".html", ".htm", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
52
+ /** V3.2 audit:「样式」扩展名:只命中样式 = 仅样式定义、无结构挂载 */
53
+ const STYLE_EXT = new Set([".scss", ".sass", ".css", ".less", ".styl"]);
54
+ /** V3.2 audit:设计层文件特征(只命中设计层 = 无实际挂载) */
55
+ const DESIGN_LAYER_RE = /design-shell|design-layer|design\.scss/i;
56
+ /** V3.2 audit:组件库运行时渲染的裸类名(无 el- 前缀,但不由业务源码挂载) */
57
+ const BUILTIN_BARE_CLASSES = new Set(["cell", "cell-group", "row", "header", "body", "caret-wrapper", "sort-caret", "ascending", "descending", "expanded-cell", "el-tooltip", "text", "value", "label"]);
41
58
 
42
59
  main();
43
60
 
@@ -63,6 +80,10 @@ function main() {
63
80
  return cmdList(args, dataset);
64
81
  case "query":
65
82
  return cmdQuery(args, dataset);
83
+ case "audit":
84
+ return cmdAudit(args, dataset);
85
+ case "verify":
86
+ return cmdVerify(args);
66
87
  default:
67
88
  printHelp();
68
89
  }
@@ -75,6 +96,9 @@ function printHelp() {
75
96
  node parser/index.js build [--dry] 校验映射表并生成 dist/ 产物
76
97
  node parser/index.js match <changeset.json...> 对设计变更集执行三级匹配(可多文件)
77
98
  node parser/index.js match --batch <dir> 项目级批量匹配:目录下全部变更集 + 跨文件汇总
99
+ node parser/index.js audit --project <dir> [<cs.json…>] 挂载点存在性 + 死通道静态审计(需目标项目源码)
100
+ node parser/index.js verify --oracle <o.json> --actual <a.json> [--sids a,b]
101
+ 渲染态逐点验收:computed style 期望 vs 实测 diff(V3.2)
78
102
  node parser/index.js list [--scene admin] [--verbose] 列出全部语义 ID(--verbose 含字段与形态,供下发词汇约束)
79
103
  node parser/index.js query <semantic.id> 查询单个 ID 的映射详情
80
104
  node parser/watch.js 开发态监听 mapping/ 变更自动重编译
@@ -149,12 +173,12 @@ function cmdMatch(args, dataset) {
149
173
  }
150
174
  if (batchSummary.pendingIds.length > 0) {
151
175
  const uniq = [...new Set(batchSummary.pendingIds)];
152
- console.log(`待补充映射 ID(去重 ${uniq.length}):`);
176
+ console.log(`缺口清单 · 新增点位(去重 ${uniq.length},须当场落地 + 回写临时映射):`);
153
177
  for (const id of uniq)
154
178
  console.log(` - ${id}`);
155
179
  }
156
180
  if (batchSummary.fieldPending.length > 0) {
157
- console.log(`字段级待补充:`);
181
+ console.log(`缺口清单 · 字段级(优先当场落设计层,仅意图缺口回流):`);
158
182
  for (const f of batchSummary.fieldPending)
159
183
  console.log(` - ${f.id}: ${f.fields.join("、")}`);
160
184
  }
@@ -290,32 +314,49 @@ function matchOne(file, dataset, batchSummary) {
290
314
  }
291
315
  }
292
316
 
317
+ // oracle 验收基线(V3.2:设计侧给的效果图 + 待比对点位;项目侧落地后跑 verify 逐点 diff)
318
+ const oracle = changeset.oracle;
319
+ if (oracle && typeof oracle === "object") {
320
+ const sids = Array.isArray(oracle.sids) ? oracle.sids : [];
321
+ console.log(`\n=== 验收基线 oracle(落地后跑 verify 逐点比对)===`);
322
+ console.log(` 效果图:${oracle.mockup || "(未给 mockup 路径)"}`);
323
+ console.log(` 待比对点位:${sids.length} 条${sids.length ? ` —— ${sids.join("、")}` : ""}`);
324
+ }
325
+ else {
326
+ console.log(`\n提示:变更集未提供 oracle 验收基线(场景还原应给)—— 落地后无法做 computed style 逐点比对,还原度不可证明,建议回流设计侧补齐。`);
327
+ }
328
+
293
329
  // 输出转换计划
294
330
  console.log(`\n=== 三级匹配结果(scene=${changeset.scene},${plan.length} 条)===`);
295
331
  for (const p of plan) {
296
332
  if (p.level === 1) {
297
333
  console.log(`\n[L1 完整命中] ${p.id}`);
298
334
  checkFormEnum(p.entry, p.change);
335
+ printApplyPlan(p.entry, p.change, p.id, index);
299
336
  printStyleTranslation(p.entry, p.change, p.id, fieldPending, valueDict);
300
337
  }
301
338
  else if (p.level === 2) {
302
339
  console.log(`\n[L2 父模块命中] ${p.id} → 继承 ${p.parent}(${p.entry.selector ?? p.entry.component ?? "?"})`);
303
340
  console.log(` 结构:定位父容器按相对位置增量插入;样式:复用父模块映射 + 全局组件映射`);
341
+ printApplyPlan(p.entry, p.change, `${p.id}(继承 ${p.parent})`, index);
304
342
  printStyleTranslation(p.entry, p.change, `${p.id}(继承 ${p.parent})`, fieldPending, valueDict);
305
343
  }
306
344
  else {
307
345
  console.log(`\n[L3 全新模块] ${p.id} → 按场景规范生成默认结构,复用全局原子层映射`);
346
+ printStructureOps(p.change);
308
347
  }
309
348
  }
310
349
 
311
350
  if (pending.length > 0) {
312
- console.log(`\n=== 待补充映射清单(${pending.length})===`);
351
+ console.log(`\n=== 缺口清单 · 新增点位(${pending.length})===`);
352
+ console.log(` 处置:当场按 L3 落地并回写临时映射(词典缺 ID 属能力缺口,不是跳过理由);仅设计侧未给意图才回流。`);
313
353
  for (const id of pending)
314
354
  console.log(` - ${id}`);
315
355
  }
316
356
 
317
357
  if (fieldPending.length > 0) {
318
- console.log(`\n=== 字段级待补充(ID 命中但 style 字段两层均未命中,跳过不猜)===`);
358
+ console.log(`\n=== 缺口清单 · 字段级(${fieldPending.length},ID 命中但 style 字段两层映射均未命中)===`);
359
+ console.log(` 处置:优先在条目作用域内生成自定义规则落设计层文件并申报;仅「值非法 / 无法定位 / 设计侧未给值」才回流设计侧。`);
319
360
  for (const f of fieldPending)
320
361
  console.log(` - ${f.id}: ${f.fields.join("、")}`);
321
362
  }
@@ -376,6 +417,24 @@ function checkBannedWords(changeset) {
376
417
  scan(`${label}.form`, c.form);
377
418
  if (c.id)
378
419
  scan(`${label}.id`, c.id);
420
+ // V3.2 三个声明维度:禁词扫描 + 结构校验(apply_via / layer / structure_ops)
421
+ if (c.apply_via !== undefined && c.apply_via !== null)
422
+ scan(`${label}.apply_via`, c.apply_via);
423
+ if (typeof c.layer === "string")
424
+ scan(`${label}.layer`, c.layer);
425
+ if (c.structure_ops !== undefined && c.structure_ops !== null) {
426
+ if (Array.isArray(c.structure_ops)) {
427
+ for (const [j, op] of c.structure_ops.entries()) {
428
+ if (!op || typeof op !== "object")
429
+ continue;
430
+ for (const k of ["node", "source", "from", "to"]) {
431
+ if (op[k] !== undefined)
432
+ scan(`${label}.structure_ops[${j}].${k}`, op[k]);
433
+ }
434
+ }
435
+ }
436
+ }
437
+ checkApplyFields(c, label, violations);
379
438
  }
380
439
  return violations;
381
440
  }
@@ -439,7 +498,7 @@ function checkFormEnum(entry, change) {
439
498
  }
440
499
  }
441
500
 
442
- /** 两层样式转换:css_vars 优先 → custom_style_map 兜底 → 未匹配记待补充 */
501
+ /** 两层样式转换:css_vars 优先 → custom_style_map 兜底(含落点层级解析)→ 未匹配记缺口清单 */
443
502
  function printStyleTranslation(entry, change, idLabel, fieldPending, valueDict) {
444
503
  const styles = change.style || {};
445
504
  if (Object.keys(styles).length === 0)
@@ -453,7 +512,10 @@ function printStyleTranslation(entry, change, idLabel, fieldPending, valueDict)
453
512
  lines.push(` . ${field}: ${valueLabel} → var 覆盖 ${entry.css_vars[field]}(@ ${entry.style_scope})`);
454
513
  }
455
514
  else if (entry.custom_style_map && entry.custom_style_map[field]) {
456
- lines.push(` . ${field}: ${valueLabel} → 自定义 ${entry.custom_style_map[field]}(@ ${entry.style_scope} + :deep() 隔离)`);
515
+ // V3.2:按变更集 layer 指认落点(host 宿主盒 / inner 内层文本盒),从词典 anchors 取真实选择器
516
+ const spot = resolveAnchorTarget(entry, field, layerOf(change, field));
517
+ const layerLabel = spot.layer ? ` · ${spot.layer} 层` : "";
518
+ lines.push(` . ${field}: ${valueLabel} → 自定义 ${entry.custom_style_map[field]}(@ ${spot.target}${layerLabel})`);
457
519
  }
458
520
  else {
459
521
  unmatched.push(field);
@@ -466,12 +528,518 @@ function printStyleTranslation(entry, change, idLabel, fieldPending, valueDict)
466
528
  for (const l of lines)
467
529
  console.log(l);
468
530
  if (unmatched.length > 0) {
469
- console.log(` ⚠ 未匹配字段(跳过):${unmatched.join("、")}`);
531
+ console.log(` ⚠ 未匹配字段:${unmatched.join("、")}(按缺口清单处置:优先在设计层落地并申报,不静默跳过)`);
470
532
  if (fieldPending)
471
533
  fieldPending.push({ id: idLabel || change.id, fields: unmatched });
472
534
  }
473
535
  }
474
536
 
537
+ // ---- V3.2 · 落点 / 结构施工 / 优先级 解析 ----
538
+
539
+ /** 取字段的落点层级:change.layer 支持字符串(整条生效)或对象(按字段指认) */
540
+ function layerOf(change, field) {
541
+ const l = change && change.layer;
542
+ if (!l)
543
+ return null;
544
+ if (typeof l === "string")
545
+ return l;
546
+ if (typeof l === "object" && !Array.isArray(l) && !field.startsWith("$"))
547
+ return l[field] || null;
548
+ return null;
549
+ }
550
+
551
+ /** 按落点层级从词典 anchors 中挑承载选择器;无匹配回退条目 selector(此时 layer 为 null 表示未指定) */
552
+ function resolveAnchorTarget(entry, field, layer) {
553
+ const anchors = Array.isArray(entry.anchors) ? entry.anchors : [];
554
+ const classAnchors = anchors.filter(a => a && a.kind === "class" && typeof a.target === "string");
555
+ if (layer) {
556
+ const hit = classAnchors.find(a => a.layer === layer);
557
+ if (hit)
558
+ return { target: hit.target, layer };
559
+ }
560
+ const host = classAnchors.find(a => a.layer === "host" || a.layer === undefined);
561
+ if (host)
562
+ return { target: host.target, layer: host.layer || null };
563
+ return { target: entry.selector ?? null, layer: null };
564
+ }
565
+
566
+ /** apply_via 解析:指认本声明引用的锚点来源条目(缺省 = 自身) */
567
+ function applyViaOf(change, index) {
568
+ const via = change && change.apply_via;
569
+ if (!via || typeof via !== "string")
570
+ return null;
571
+ return { id: via, resolved: index ? index[via] : undefined };
572
+ }
573
+
574
+ /** 输出施工上下文:承载通道 anchors / 前置条件 / 覆写优先级 / 结构施工 */
575
+ function printApplyPlan(entry, change, idLabel, index) {
576
+ const anchors = Array.isArray(entry.anchors) ? entry.anchors : [];
577
+ if (anchors.length > 0) {
578
+ const host = anchors.find(a => a.kind === "class" && a.layer === "host");
579
+ const inner = anchors.find(a => a.kind === "class" && a.layer === "inner");
580
+ const parts = [];
581
+ if (host)
582
+ parts.push(`host=${host.target}`);
583
+ if (inner)
584
+ parts.push(`inner=${inner.target}`);
585
+ const vars = anchors.filter(a => a.kind === "css_var").map(a => a.target);
586
+ if (vars.length > 0)
587
+ parts.push(`css_var=${vars.join("、")}`);
588
+ if (parts.length > 0)
589
+ console.log(` ▸ 承载通道 anchors:${parts.join(" | ")}`);
590
+ }
591
+
592
+ const via = applyViaOf(change, index);
593
+ if (via) {
594
+ const ok = via.resolved ? "" : "(⚠ 词典中无此条目,锚点来源待补)";
595
+ console.log(` ▸ 承载通道来源 apply_via:${via.id}${ok}`);
596
+ }
597
+
598
+ printStructureOps(change);
599
+
600
+ if (entry.preconditions && Array.isArray(entry.preconditions) && entry.preconditions.length > 0) {
601
+ console.log(` ▸ 前置条件 preconditions(施工前必须满足,否则值对了也不生效):`);
602
+ for (const p of entry.preconditions)
603
+ console.log(` - ${p}`);
604
+ }
605
+
606
+ if (entry.priority && entry.priority.strategy) {
607
+ console.log(` ▸ 覆写优先级 priority:${entry.priority.strategy}${entry.priority.reason ? ` —— ${entry.priority.reason}` : ""}`);
608
+ }
609
+ }
610
+
611
+ /** 输出结构施工声明(structure_ops):数据 / 结构驱动点位,只写 CSS 是无效声明 */
612
+ function printStructureOps(change) {
613
+ const ops = change && change.structure_ops;
614
+ if (!Array.isArray(ops) || ops.length === 0)
615
+ return;
616
+ console.log(` ▸ 结构施工 structure_ops(${ops.length} 项,必须改数据 / 模板结构,不能只写 CSS):`);
617
+ for (const op of ops) {
618
+ if (!op || typeof op !== "object") {
619
+ console.log(` - (非法条目)`);
620
+ continue;
621
+ }
622
+ const parts = [`op=${op.op || "?"}`];
623
+ if (op.node)
624
+ parts.push(`node=${op.node}`);
625
+ if (op.source)
626
+ parts.push(`source=${op.source}`);
627
+ if (op.from)
628
+ parts.push(`from=${op.from}`);
629
+ if (op.to)
630
+ parts.push(`to=${op.to}`);
631
+ console.log(` - ${parts.join(" ")}`);
632
+ }
633
+ }
634
+
635
+ /** 结构施工 / 落点 / 优先级 声明的协议校验(parse 期,追加进 checkBannedWords 违规列表) */
636
+ function checkApplyFields(change, label, violations) {
637
+ const STRUCT_OPS = ["add_node", "remove_node", "move_node", "render_branch", "bind_source"];
638
+ if (change.layer !== undefined && change.layer !== null) {
639
+ const lv = change.layer;
640
+ if (typeof lv === "string") {
641
+ if (!LAYER_VALUES.includes(lv))
642
+ violations.push(`${label}.layer "${lv}" 不在枚举 [${LAYER_VALUES.join("/")}] 内`);
643
+ }
644
+ else if (typeof lv === "object" && !Array.isArray(lv)) {
645
+ for (const [f, v] of Object.entries(lv)) {
646
+ if (!LAYER_VALUES.includes(v))
647
+ violations.push(`${label}.layer.${f} "${v}" 不在枚举 [${LAYER_VALUES.join("/")}] 内`);
648
+ }
649
+ }
650
+ else {
651
+ violations.push(`${label}.layer 应为字符串或「字段 → 层级」对象`);
652
+ }
653
+ }
654
+ if (change.apply_via !== undefined && change.apply_via !== null) {
655
+ if (typeof change.apply_via !== "string" || !ID_RE_STRICT.test(change.apply_via))
656
+ violations.push(`${label}.apply_via "${change.apply_via}" 应为语义锚点名(小写下划线、2~4 段),不是选择器 / 变量名`);
657
+ }
658
+ if (change.structure_ops !== undefined && change.structure_ops !== null) {
659
+ if (!Array.isArray(change.structure_ops)) {
660
+ violations.push(`${label}.structure_ops 必须为数组`);
661
+ }
662
+ else {
663
+ for (const [i, op] of change.structure_ops.entries()) {
664
+ const at = `${label}.structure_ops[${i}]`;
665
+ if (!op || typeof op !== "object") {
666
+ violations.push(`${at} 必须为对象`);
667
+ continue;
668
+ }
669
+ if (!STRUCT_OPS.includes(op.op))
670
+ violations.push(`${at}.op "${op.op}" 不在枚举 [${STRUCT_OPS.join("/")}] 内`);
671
+ if (op.node !== undefined && (typeof op.node !== "string" || op.node.trim() === ""))
672
+ violations.push(`${at}.node 应为非空语义节点名`);
673
+ if (op.source !== undefined && (typeof op.source !== "string" || op.source.trim() === ""))
674
+ violations.push(`${at}.source 应为非空语义数据源名`);
675
+ }
676
+ }
677
+ }
678
+ }
679
+
680
+ // ---- audit:挂载点存在性 + 死通道(V3.2,静态审计,需目标项目源码) ----
681
+ // 本节的模块级常量(AUDIT_SKIP_DIRS / STRUCT_EXT / STYLE_EXT / DESIGN_LAYER_RE /
682
+ // BUILTIN_BARE_CLASSES)已在文件顶部声明——模块级常量必须在 main() 调用前初始化,勿下移。
683
+
684
+ function escapeRe(s) {
685
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
686
+ }
687
+
688
+ /** 递归收集目标项目源码(剔除依赖 / 产物目录),返回 [{ file, text, kind }] */
689
+ function collectProjectSources(root) {
690
+ const out = [];
691
+ const walk = (dir, depth) => {
692
+ if (depth > 10)
693
+ return;
694
+ let items;
695
+ try {
696
+ items = readdirSync(dir, { withFileTypes: true });
697
+ }
698
+ catch {
699
+ return;
700
+ }
701
+ for (const it of items) {
702
+ const name = it.name;
703
+ if (name.startsWith("."))
704
+ continue;
705
+ const p = path.join(dir, name);
706
+ if (it.isDirectory()) {
707
+ if (AUDIT_SKIP_DIRS.has(name))
708
+ continue;
709
+ walk(p, depth + 1);
710
+ continue;
711
+ }
712
+ const ext = path.extname(name).toLowerCase();
713
+ const kind = STRUCT_EXT.has(ext) ? "struct" : (STYLE_EXT.has(ext) ? "style" : null);
714
+ if (!kind)
715
+ continue;
716
+ try {
717
+ out.push({ file: path.relative(root, p), text: readFileSync(p, "utf-8"), kind });
718
+ }
719
+ catch { /* 跳过不可读文件 */ }
720
+ }
721
+ };
722
+ walk(root, 0);
723
+ return out;
724
+ }
725
+
726
+ /** 从选择器串提取需「挂载」的 token(类名 / 属性名);剔除伪类与组件库内置前缀 */
727
+ function extractMountTokens(selector) {
728
+ const tokens = new Set();
729
+ if (typeof selector !== "string" || selector.trim() === "")
730
+ return [];
731
+ const cleaned = selector.replace(/::?[a-zA-Z-]+(\([^)]*\))?/g, " ");
732
+ let m;
733
+ const classRe = /\.([a-zA-Z_][\w-]*)/g;
734
+ while ((m = classRe.exec(cleaned)) !== null) {
735
+ const cls = m[1];
736
+ if (/^(el-|is-|v-modal)/.test(cls) || BUILTIN_BARE_CLASSES.has(cls))
737
+ continue;
738
+ tokens.add(cls);
739
+ }
740
+ const attrRe = /\[([a-zA-Z_][\w-]*)/g;
741
+ while ((m = attrRe.exec(cleaned)) !== null)
742
+ tokens.add(`[${m[1]}]`);
743
+ return [...tokens];
744
+ }
745
+
746
+ /** 在源码集中查找 token 的挂载情况 */
747
+ function findMount(token, sources) {
748
+ const needle = token.startsWith("[") ? token.slice(1, -1) : token;
749
+ const re = new RegExp(`(^|[^\\w-])${escapeRe(needle)}([^\\w-]|$)`);
750
+ const hits = sources.filter(s => re.test(s.text));
751
+ return {
752
+ struct: hits.filter(h => h.kind === "struct" && !DESIGN_LAYER_RE.test(h.file)),
753
+ style: hits.filter(h => h.kind === "style" && !DESIGN_LAYER_RE.test(h.file)),
754
+ designLayer: hits.filter(h => DESIGN_LAYER_RE.test(h.file)),
755
+ };
756
+ }
757
+
758
+ /** 死通道检查:CSS 变量在项目中是否有 var() 消费者 */
759
+ function findVariableConsumer(varName, sources) {
760
+ const re = new RegExp(`var\\(\\s*${escapeRe(varName)}\\s*[,)]`);
761
+ return sources.filter(s => re.test(s.text)).map(s => s.file);
762
+ }
763
+
764
+ /** 依据命中情况判定 token 的挂载状态 */
765
+ function judgeMount(token, where, hit) {
766
+ if (hit.struct.length > 0)
767
+ return [];
768
+ if (hit.style.length > 0)
769
+ return [`挂载点可疑:${where} 的「${token}」只出现在样式文件(${hit.style.slice(0, 2).join("、")}),未在模板 / 脚本里挂载 —— 规则可能永不命中(历史缺口:列未绑 class-name,金额列右对齐 / 缩略图尺寸失效)`];
770
+ if (hit.designLayer.length > 0)
771
+ return [`挂载点缺失:${where} 的「${token}」只出现在设计层样式(${hit.designLayer.slice(0, 2).join("、")}),项目里既无结构挂载也无出厂对应 —— 规则不会生效`];
772
+ return [`挂载点缺失:${where} 的「${token}」在项目中未找到任何出现位置(选择器永不命中)`];
773
+ }
774
+
775
+ /** 单条目审计:selector / anchors 挂载点 + css_var 消费者 */
776
+ function auditEntry(entry, sources) {
777
+ const issues = [];
778
+ for (const t of extractMountTokens(entry.selector))
779
+ issues.push(...judgeMount(t, "selector", findMount(t, sources)));
780
+
781
+ const anchors = Array.isArray(entry.anchors) ? entry.anchors : [];
782
+ for (const a of anchors) {
783
+ if (!a || typeof a.target !== "string")
784
+ continue;
785
+ if (a.kind === "css_var") {
786
+ const consumers = findVariableConsumer(a.target, sources);
787
+ if (consumers.length === 0)
788
+ issues.push(`死通道:CSS 变量 ${a.target} 在项目中无 var() 消费者(只定义不消费 = 通道死路,翻译再准也不生效)`);
789
+ }
790
+ else {
791
+ for (const t of extractMountTokens(a.target))
792
+ issues.push(...judgeMount(t, `anchors(${a.layer || "未标层级"})`, findMount(t, sources)));
793
+ }
794
+ }
795
+ return [...new Set(issues)];
796
+ }
797
+
798
+ function cmdAudit(args, dataset) {
799
+ const projIdx = args.indexOf("--project");
800
+ const projectRoot = projIdx > -1 ? args[projIdx + 1] : null;
801
+ if (!projectRoot) {
802
+ console.error("✖ 缺少目标项目路径:node parser/index.js audit --project <项目根目录> [<changeset.json…>]");
803
+ process.exit(1);
804
+ }
805
+ const root = path.resolve(projectRoot);
806
+ let stat;
807
+ try {
808
+ stat = statSync(root);
809
+ }
810
+ catch {
811
+ console.error(`✖ 目标项目路径不存在:${root}`);
812
+ process.exit(1);
813
+ }
814
+ if (!stat.isDirectory()) {
815
+ console.error(`✖ --project 参数应为目录:${root}`);
816
+ process.exit(1);
817
+ }
818
+
819
+ const files = args.filter((a, i) => !a.startsWith("--") && i !== projIdx + 1);
820
+ const sources = collectProjectSources(root);
821
+ const index = buildMergedIndex(dataset);
822
+
823
+ // 给了变更集 → 只审被引用的点位(含 apply_via 指向的锚点来源);否则审词典全量
824
+ let targetIds;
825
+ if (files.length > 0) {
826
+ const wanted = new Set();
827
+ for (const f of files) {
828
+ let cs;
829
+ try {
830
+ cs = JSON.parse(readFileSync(path.resolve(f), "utf-8"));
831
+ }
832
+ catch (e) {
833
+ console.error(`✖ 变更集读取失败 ${f}:${e.message}`);
834
+ process.exit(1);
835
+ }
836
+ for (const c of cs.changes || []) {
837
+ if (c.id)
838
+ wanted.add(c.id);
839
+ if (typeof c.apply_via === "string")
840
+ wanted.add(c.apply_via);
841
+ }
842
+ }
843
+ targetIds = [...wanted];
844
+ }
845
+ else {
846
+ targetIds = Object.keys(index);
847
+ }
848
+
849
+ console.log(`审计目标:${root}`);
850
+ console.log(`源码文件:${sources.length}(已剔除 node_modules / dist 等产物目录)`);
851
+ console.log(`检查条目:${targetIds.length}${files.length > 0 ? "(仅变更集引用点位)" : "(词典全量)"}`);
852
+
853
+ let problems = 0;
854
+ const clean = [];
855
+ for (const id of targetIds) {
856
+ const entry = index[id];
857
+ if (!entry) {
858
+ console.log(`\n✘ ${id} —— 词典中无此条目(L3 新增点位,须当场落地并回写临时映射)`);
859
+ problems++;
860
+ continue;
861
+ }
862
+ const issues = auditEntry(entry, sources);
863
+ if (issues.length === 0) {
864
+ clean.push(id);
865
+ continue;
866
+ }
867
+ console.log(`\n✘ ${id}`);
868
+ for (const it of issues)
869
+ console.log(` - ${it}`);
870
+ problems += issues.length;
871
+ }
872
+ if (clean.length > 0)
873
+ console.log(`\n✔ 通过:${clean.join("、")}`);
874
+
875
+ console.log(`\n汇总:${targetIds.length} 条点位,${problems} 个挂载 / 通道问题`);
876
+ if (problems > 0) {
877
+ console.log(`处置口径:挂载点缺失 / 死通道属**能力缺口** —— 当场按 structure_ops 补挂载或补消费者,交付说明单列申报;不得以「词典没建」为由跳过。`);
878
+ process.exitCode = 1;
879
+ }
880
+ else {
881
+ console.log(`✔ 无挂载 / 通道问题(渲染态是否一致仍需 §验收比对确认:静态审计查不出「值对了、位置不对」)`);
882
+ }
883
+ }
884
+
885
+ // ---- verify:渲染态逐点验收(V3.2 补丁 3 · 闭环验收回路) ----
886
+ /**
887
+ * 比对两份 computed style 转储:oracle(效果图期望)vs actual(项目实测)。
888
+ * 转储格式:{ "<语义ID>": { "<css属性>": "<值>", ... }, ... }
889
+ * 期望与实测由**浏览器链路**抽取(效果图元素带 data-sid、项目页面同名 data-sid)——
890
+ * 本命令不做抽取,只做归一化 + 逐点 diff + 三类归因。
891
+ *
892
+ * CLI:node parser/index.js verify --oracle <oracle.json> --actual <actual.json> [--sids a,b,c]
893
+ * 退出码:存在缺口 → 1;全部命中 → 0。
894
+ */
895
+ function cmdVerify(args) {
896
+ // 几何 / 盒模型属性:值对不上 = 语境错位(宿主盒 / 内层盒模型 / 层叠上下文不同)
897
+ const GEOMETRY_PROPS = new Set([
898
+ "display", "position", "top", "right", "bottom", "left", "box-sizing",
899
+ "width", "min-width", "max-width", "height", "min-height", "max-height",
900
+ "padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
901
+ "padding-block", "padding-inline", "padding-block-start", "padding-block-end",
902
+ "padding-inline-start", "padding-inline-end",
903
+ "margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
904
+ "margin-block", "margin-inline", "gap", "row-gap", "column-gap",
905
+ "text-align", "vertical-align", "line-height", "letter-spacing", "font-size",
906
+ "font-family", "white-space", "text-overflow", "overflow",
907
+ "border-radius", "border-top-left-radius", "border-top-right-radius",
908
+ "border-bottom-left-radius", "border-bottom-right-radius",
909
+ "flex", "flex-direction", "flex-grow", "flex-shrink", "flex-basis", "flex-wrap",
910
+ "justify-content", "align-items", "align-self", "transform", "inset",
911
+ ]);
912
+
913
+ const get = flag => {
914
+ const i = args.indexOf(flag);
915
+ return i > -1 ? args[i + 1] : null;
916
+ };
917
+ const oraclePath = get("--oracle");
918
+ const actualPath = get("--actual");
919
+ if (!oraclePath || !actualPath) {
920
+ console.error("✖ 缺少参数:node parser/index.js verify --oracle <oracle.json> --actual <actual.json> [--sids a,b,c]");
921
+ process.exit(1);
922
+ }
923
+ const sidsArg = get("--sids");
924
+ const sidFilter = sidsArg ? new Set(sidsArg.split(",").map(s => s.trim()).filter(Boolean)) : null;
925
+
926
+ const readDump = (p, label) => {
927
+ let raw;
928
+ try {
929
+ raw = readFileSync(path.resolve(p), "utf-8");
930
+ }
931
+ catch (e) {
932
+ console.error(`✖ ${label} 读取失败 ${p}:${e.message}`);
933
+ process.exit(1);
934
+ }
935
+ let json;
936
+ try {
937
+ json = JSON.parse(raw);
938
+ }
939
+ catch (e) {
940
+ console.error(`✖ ${label} 不是合法 JSON ${p}:${e.message}`);
941
+ process.exit(1);
942
+ }
943
+ // 允许 { sids: {…} } 或直接 { "<sid>": {…} } 两种外形
944
+ const map = json && json.sids && typeof json.sids === "object" ? json.sids : json;
945
+ if (!map || typeof map !== "object" || Array.isArray(map)) {
946
+ console.error(`✖ ${label} 结构非法:应为 { "<语义ID>": { "<属性>": "<值>" } }`);
947
+ process.exit(1);
948
+ }
949
+ return map;
950
+ };
951
+
952
+ const oracle = readDump(oraclePath, "效果图期望");
953
+ const actual = readDump(actualPath, "项目实测");
954
+
955
+ let sids = Object.keys(oracle);
956
+ if (sidFilter)
957
+ sids = sids.filter(s => sidFilter.has(s));
958
+
959
+ const normProp = p => String(p).trim().toLowerCase().replace(/_/g, "-");
960
+ const normValue = v => {
961
+ let s = String(v == null ? "" : v).trim().toLowerCase().replace(/\s+/g, " ");
962
+ s = s.replace(/\s*,\s*/g, ",");
963
+ // 3 位 hex 展开为 6 位,消除 "#fff" vs "#ffffff" 假缺口
964
+ s = s.replace(/#([0-9a-f]{3})\b/g, (_, h) => `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`);
965
+ return s;
966
+ };
967
+
968
+ console.log(`渲染态验收(computed style 逐点比对)`);
969
+ console.log(`效果图基线:${path.resolve(oraclePath)}`);
970
+ console.log(`项目实测:${path.resolve(actualPath)}`);
971
+ console.log(`比对点位:${sids.length}${sidFilter ? `(--sids 过滤后)` : "(oracle 全量)"}`);
972
+
973
+ let hitProps = 0;
974
+ let missProps = 0;
975
+ const attr = { selector_miss: 0, weight_override: 0, context_mismatch: 0 };
976
+ const clean = [];
977
+
978
+ for (const sid of sids) {
979
+ const exp = oracle[sid];
980
+ if (!exp || typeof exp !== "object") {
981
+ console.log(`\n⚠ ${sid} —— oracle 条目非法(应为属性对象),跳过`);
982
+ continue;
983
+ }
984
+ const actRaw = actual[sid];
985
+ if (!actRaw || typeof actRaw !== "object") {
986
+ console.log(`\n✘ ${sid}\n - 项目页面未抽取到该点位 —— 选择器落空(挂载点缺失 / 元素未渲染 / data-sid 未标注)`);
987
+ attr.selector_miss++;
988
+ missProps++;
989
+ continue;
990
+ }
991
+ const act = {};
992
+ for (const [k, v] of Object.entries(actRaw))
993
+ act[normProp(k)] = v;
994
+
995
+ const lines = [];
996
+ for (const [propRaw, expVal] of Object.entries(exp)) {
997
+ const prop = normProp(propRaw);
998
+ if (!(prop in act)) {
999
+ lines.push(` - ${prop}: 期望 ${expVal} / 实测未取到 —— 选择器落空(该属性所在规则未命中)`);
1000
+ attr.selector_miss++;
1001
+ continue;
1002
+ }
1003
+ if (normValue(expVal) === normValue(act[prop])) {
1004
+ hitProps++;
1005
+ continue;
1006
+ }
1007
+ const isGeom = GEOMETRY_PROPS.has(prop);
1008
+ const kind = isGeom ? "语境错位" : "权重被压";
1009
+ if (isGeom)
1010
+ attr.context_mismatch++;
1011
+ else
1012
+ attr.weight_override++;
1013
+ lines.push(` - ${prop}: 期望 ${expVal} / 实得 ${act[prop]} —— ${kind}(${
1014
+ isGeom ? "几何值对不上:宿主盒 / 内层盒模型或层叠上下文不同" : "同权重被组件 scoped / 行内样式反压,或令牌未生效"
1015
+ })`);
1016
+ missProps++;
1017
+ }
1018
+
1019
+ if (lines.length === 0) {
1020
+ clean.push(sid);
1021
+ }
1022
+ else {
1023
+ console.log(`\n✘ ${sid}`);
1024
+ for (const l of lines)
1025
+ console.log(l);
1026
+ }
1027
+ }
1028
+
1029
+ if (clean.length > 0)
1030
+ console.log(`\n✔ 逐点命中:${clean.join("、")}`);
1031
+
1032
+ console.log(`\n汇总:${sids.length} 点位 · ${hitProps} 属性命中 · ${missProps} 缺口`);
1033
+ console.log(`归因:选择器落空 ${attr.selector_miss} / 权重被压 ${attr.weight_override} / 语境错位 ${attr.context_mismatch}`);
1034
+ if (missProps > 0) {
1035
+ console.log(`处置口径:按归因修正 —— 选择器落空补挂载点(属能力缺口,走 structure_ops / 补通道并申报);权重被压按 priority 声明提权(禁止 !important 掩盖);语境错位校准 layer(host / inner)。`);
1036
+ process.exitCode = 1;
1037
+ }
1038
+ else {
1039
+ console.log(`✔ 渲染态与效果图基线逐点一致(验收通过)`);
1040
+ }
1041
+ }
1042
+
475
1043
  /** 语义值译表索引:mapping 中 semantic-values.json 摊平为 { 语义名(中/英): 精确值 } */
476
1044
  function buildSemanticValueIndex(dataset) {
477
1045
  const dict = {};
@@ -542,6 +1110,11 @@ function cmdList(args, dataset) {
542
1110
  console.log(` style 字段:${fields.join(" ")}`);
543
1111
  if (forms.length > 0)
544
1112
  console.log(` form 形态:${forms.join(" / ")}`);
1113
+ // V3.2:落点层级与覆写优先级(设计侧声明 layer / 项目侧选提权策略时可见)
1114
+ const anchors = Array.isArray(e.anchors) ? e.anchors : [];
1115
+ const layers = [...new Set(anchors.filter(a => a.kind === "class" && a.layer).map(a => a.layer))];
1116
+ if (layers.length > 0 || (e.priority && e.priority.strategy))
1117
+ console.log(` 落点层级:${layers.length > 0 ? layers.join(" / ") : "—"};覆写优先级:${(e.priority && e.priority.strategy) || "—"}`);
545
1118
  }
546
1119
  }
547
1120
  }