@qilitt-mickey/vue3-temp-skill 1.1.43 → 1.1.45
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/SKILL.md +1 -0
- package/bin/cli.js +1 -1
- package/mapping/component/button.json +8 -3
- package/mapping/component/card.json +31 -0
- package/mapping/component/input.json +4 -1
- package/mapping/component/pagination.json +34 -0
- package/mapping/component/tag.json +2 -1
- package/mapping/scenes/admin/content.json +34 -6
- package/mapping/scenes/admin/layout.json +15 -5
- package/package.json +1 -1
- package/parser/fixtures/changeset-registry-gap.json +29 -0
- package/parser/fixtures/changeset-v32-fields.json +9 -2
- package/parser/fixtures/registry-bad.json +11 -0
- package/parser/fixtures/registry-sample.json +15 -0
- package/parser/index.js +416 -31
- package/parser/regression.js +30 -6
- package/parser/validator.js +4 -4
- package/references/cobalt-admin-restore.changeset.json +468 -0
- package/references/code-quality.md +8 -3
- package/references/design-apply.md +74 -13
package/parser/index.js
CHANGED
|
@@ -18,12 +18,13 @@
|
|
|
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…>] # 挂载点存在性 +
|
|
22
|
-
* node parser/index.js verify --oracle <o> --actual <a> # 渲染态逐点验收:computed style 期望 vs 实测 diff
|
|
21
|
+
* node parser/index.js audit --project <dir> [<cs…>] # 挂载点存在性 + 死通道静态审计()
|
|
22
|
+
* node parser/index.js verify --oracle <o> --actual <a> # 渲染态逐点验收:computed style 期望 vs 实测 diff()
|
|
23
|
+
* node parser/index.js registry <registry.json> # 登记表通道核验:登记项 sid / dims 是否在词典有落点()
|
|
23
24
|
* node parser/watch.js # 开发态监听 mapping/ 自动重编译(§3.4.3)
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
|
-
import { readFileSync, statSync, readdirSync } from "node:fs";
|
|
27
|
+
import { readFileSync, statSync, readdirSync, existsSync } from "node:fs";
|
|
27
28
|
import path from "node:path";
|
|
28
29
|
import { fileURLToPath } from "node:url";
|
|
29
30
|
import { buildMappingDataset } from "./loader.js";
|
|
@@ -40,20 +41,20 @@ const QUALITATIVE_RE = /^(中等|加粗|常规|细|粗|小圆角|全圆角|直
|
|
|
40
41
|
const NUMERIC_VALUE_RE = /^\d+(\.\d+)?(px|rem|em|%|vh|vw|pt)$/i;
|
|
41
42
|
/** 复合样式字面量:含空格的多段值(如 1px solid #F0F0F0)按已消歧处理(hex 已被禁词校验限定位置) */
|
|
42
43
|
const COMPOSITE_VALUE_RE = /\s/;
|
|
43
|
-
/**
|
|
44
|
+
/** 落点层级枚举(与词典 validator 的 LAYER_ENUM 一致) */
|
|
44
45
|
const LAYER_VALUES = ["host", "inner"];
|
|
45
46
|
/** 语义 ID 严格格式(apply_via 锚点名须符合) */
|
|
46
47
|
const ID_RE_STRICT = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$/;
|
|
47
48
|
|
|
48
|
-
/**
|
|
49
|
+
/** audit:审计时跳过的目录(依赖 / 产物 / 缓存) */
|
|
49
50
|
const AUDIT_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".output", ".vite", ".nuxt", "coverage", "public"]);
|
|
50
|
-
/**
|
|
51
|
+
/** audit:「模板 / 脚本」扩展名:命中 = 结构挂载 */
|
|
51
52
|
const STRUCT_EXT = new Set([".vue", ".html", ".htm", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
52
|
-
/**
|
|
53
|
+
/** audit:「样式」扩展名:只命中样式 = 仅样式定义、无结构挂载 */
|
|
53
54
|
const STYLE_EXT = new Set([".scss", ".sass", ".css", ".less", ".styl"]);
|
|
54
|
-
/**
|
|
55
|
+
/** audit:设计层文件特征(只命中设计层 = 无实际挂载) */
|
|
55
56
|
const DESIGN_LAYER_RE = /design-shell|design-layer|design\.scss/i;
|
|
56
|
-
/**
|
|
57
|
+
/** audit:组件库运行时渲染的裸类名(无 el- 前缀,但不由业务源码挂载) */
|
|
57
58
|
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"]);
|
|
58
59
|
|
|
59
60
|
main();
|
|
@@ -84,6 +85,8 @@ function main() {
|
|
|
84
85
|
return cmdAudit(args, dataset);
|
|
85
86
|
case "verify":
|
|
86
87
|
return cmdVerify(args);
|
|
88
|
+
case "registry":
|
|
89
|
+
return cmdRegistry(args, dataset);
|
|
87
90
|
default:
|
|
88
91
|
printHelp();
|
|
89
92
|
}
|
|
@@ -95,10 +98,14 @@ function printHelp() {
|
|
|
95
98
|
用法:
|
|
96
99
|
node parser/index.js build [--dry] 校验映射表并生成 dist/ 产物
|
|
97
100
|
node parser/index.js match <changeset.json...> 对设计变更集执行三级匹配(可多文件)
|
|
101
|
+
node parser/index.js match <cs.json> --registry <reg.json>
|
|
102
|
+
匹配 + 登记表覆盖率当场重算(子项级细闸,有缺口 exit 1)
|
|
98
103
|
node parser/index.js match --batch <dir> 项目级批量匹配:目录下全部变更集 + 跨文件汇总
|
|
99
104
|
node parser/index.js audit --project <dir> [<cs.json…>] 挂载点存在性 + 死通道静态审计(需目标项目源码)
|
|
100
105
|
node parser/index.js verify --oracle <o.json> --actual <a.json> [--sids a,b]
|
|
101
|
-
渲染态逐点验收:computed style 期望 vs 实测 diff
|
|
106
|
+
渲染态逐点验收:computed style 期望 vs 实测 diff()
|
|
107
|
+
node parser/index.js registry <registry.json> [--verbose]
|
|
108
|
+
登记表通道核验:登记项 sid 是否在词典、dims 是否有落点()
|
|
102
109
|
node parser/index.js list [--scene admin] [--verbose] 列出全部语义 ID(--verbose 含字段与形态,供下发词汇约束)
|
|
103
110
|
node parser/index.js query <semantic.id> 查询单个 ID 的映射详情
|
|
104
111
|
node parser/watch.js 开发态监听 mapping/ 变更自动重编译
|
|
@@ -120,7 +127,11 @@ function cmdBuild(args, warnings) {
|
|
|
120
127
|
|
|
121
128
|
// ---- match:三级匹配 ----
|
|
122
129
|
function cmdMatch(args, dataset) {
|
|
123
|
-
|
|
130
|
+
// --registry <path>:登记表路径(子项级覆盖率细闸)。缺省时回落到 changeset.coverage.registry / oracle.registry。
|
|
131
|
+
const regIdx = args.indexOf("--registry");
|
|
132
|
+
const registryArg = regIdx >= 0 ? args[regIdx + 1] : null;
|
|
133
|
+
const consumed = new Set(registryArg ? [registryArg] : []);
|
|
134
|
+
const files = args.filter(a => !a.startsWith("--") && !consumed.has(a));
|
|
124
135
|
if (files.length === 0) {
|
|
125
136
|
console.error("✖ 缺少变更集文件参数:node parser/index.js match <changeset.json...|--batch <dir>>");
|
|
126
137
|
process.exit(1);
|
|
@@ -151,19 +162,23 @@ function cmdMatch(args, dataset) {
|
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
// 跨文件汇总(项目级模式对账用)
|
|
154
|
-
const batchSummary = { files: 0, l1: 0, l2: 0, l3: 0, pendingIds: [], fieldPending: [], invalid: [] };
|
|
165
|
+
const batchSummary = { files: 0, l1: 0, l2: 0, l3: 0, pendingIds: [], fieldPending: [], invalid: [], registryGaps: 0 };
|
|
155
166
|
|
|
156
167
|
for (const target of targets) {
|
|
157
168
|
batchSummary.files++;
|
|
158
169
|
if (targets.length > 1)
|
|
159
170
|
console.log(`\n════ 变更集 ${batchSummary.files}/${targets.length}:${path.basename(target)} ════`);
|
|
160
|
-
matchOne(target, dataset, batchSummary);
|
|
171
|
+
matchOne(target, dataset, batchSummary, { registryArg, filePath: target, verbose: args.includes("--verbose") });
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
// 任一文件被拒(schema/版本/禁词校验失败)→ 进程退出码 1(v1.0 修正:单文件模式此前误返回 0)
|
|
164
175
|
if (batchSummary.invalid.length > 0)
|
|
165
176
|
process.exitCode = 1;
|
|
166
177
|
|
|
178
|
+
// 登记表覆盖率有缺口(子项被静默丢失)→ 也是「交付不完整」,退出码 1
|
|
179
|
+
if (batchSummary.registryGaps > 0)
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
|
|
167
182
|
// 项目级汇总输出
|
|
168
183
|
if (targets.length > 1) {
|
|
169
184
|
console.log(`\n════════ 项目级汇总(${targets.length} 份变更集)════════`);
|
|
@@ -182,11 +197,13 @@ function cmdMatch(args, dataset) {
|
|
|
182
197
|
for (const f of batchSummary.fieldPending)
|
|
183
198
|
console.log(` - ${f.id}: ${f.fields.join("、")}`);
|
|
184
199
|
}
|
|
200
|
+
if (batchSummary.registryGaps > 0)
|
|
201
|
+
console.log(`✖ 登记表覆盖率缺口合计 ${batchSummary.registryGaps} 条(子项级欠声明,详见各文件段落)`);
|
|
185
202
|
}
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
/** 单份变更集匹配(cmdMatch 内部复用) */
|
|
189
|
-
function matchOne(file, dataset, batchSummary) {
|
|
206
|
+
function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
190
207
|
let changeset;
|
|
191
208
|
try {
|
|
192
209
|
changeset = JSON.parse(readFileSync(file, "utf-8"));
|
|
@@ -314,17 +331,13 @@ function matchOne(file, dataset, batchSummary) {
|
|
|
314
331
|
}
|
|
315
332
|
}
|
|
316
333
|
|
|
317
|
-
// oracle
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
console.log(`\n提示:变更集未提供 oracle 验收基线(场景还原应给)—— 落地后无法做 computed style 逐点比对,还原度不可证明,建议回流设计侧补齐。`);
|
|
327
|
-
}
|
|
334
|
+
// oracle 验收基线(:设计侧给的效果图 + 待比对点位;项目侧落地后跑 verify 逐点 diff)
|
|
335
|
+
checkOracle(changeset, plan, valueDict);
|
|
336
|
+
checkCoverage(changeset);
|
|
337
|
+
// 登记表覆盖率(子项级细闸):以登记表为权威、当场重算 A/C,设计侧自报的 coverage 不参与
|
|
338
|
+
const regGaps = checkRegistryCoverage(changeset, plan, opts);
|
|
339
|
+
if (batchSummary)
|
|
340
|
+
batchSummary.registryGaps += regGaps;
|
|
328
341
|
|
|
329
342
|
// 输出转换计划
|
|
330
343
|
console.log(`\n=== 三级匹配结果(scene=${changeset.scene},${plan.length} 条)===`);
|
|
@@ -417,7 +430,7 @@ function checkBannedWords(changeset) {
|
|
|
417
430
|
scan(`${label}.form`, c.form);
|
|
418
431
|
if (c.id)
|
|
419
432
|
scan(`${label}.id`, c.id);
|
|
420
|
-
//
|
|
433
|
+
// 三个声明维度:禁词扫描 + 结构校验(apply_via / layer / structure_ops)
|
|
421
434
|
if (c.apply_via !== undefined && c.apply_via !== null)
|
|
422
435
|
scan(`${label}.apply_via`, c.apply_via);
|
|
423
436
|
if (typeof c.layer === "string")
|
|
@@ -512,7 +525,7 @@ function printStyleTranslation(entry, change, idLabel, fieldPending, valueDict)
|
|
|
512
525
|
lines.push(` . ${field}: ${valueLabel} → var 覆盖 ${entry.css_vars[field]}(@ ${entry.style_scope})`);
|
|
513
526
|
}
|
|
514
527
|
else if (entry.custom_style_map && entry.custom_style_map[field]) {
|
|
515
|
-
//
|
|
528
|
+
// :按变更集 layer 指认落点(host 宿主盒 / inner 内层文本盒),从词典 anchors 取真实选择器
|
|
516
529
|
const spot = resolveAnchorTarget(entry, field, layerOf(change, field));
|
|
517
530
|
const layerLabel = spot.layer ? ` · ${spot.layer} 层` : "";
|
|
518
531
|
lines.push(` . ${field}: ${valueLabel} → 自定义 ${entry.custom_style_map[field]}(@ ${spot.target}${layerLabel})`);
|
|
@@ -534,7 +547,7 @@ function printStyleTranslation(entry, change, idLabel, fieldPending, valueDict)
|
|
|
534
547
|
}
|
|
535
548
|
}
|
|
536
549
|
|
|
537
|
-
// ----
|
|
550
|
+
// ---- · 落点 / 结构施工 / 优先级 解析 ----
|
|
538
551
|
|
|
539
552
|
/** 取字段的落点层级:change.layer 支持字符串(整条生效)或对象(按字段指认) */
|
|
540
553
|
function layerOf(change, field) {
|
|
@@ -605,7 +618,379 @@ function printApplyPlan(entry, change, idLabel, index) {
|
|
|
605
618
|
|
|
606
619
|
if (entry.priority && entry.priority.strategy) {
|
|
607
620
|
console.log(` ▸ 覆写优先级 priority:${entry.priority.strategy}${entry.priority.reason ? ` —— ${entry.priority.reason}` : ""}`);
|
|
621
|
+
console.log(` 落地要求:${priorityRequirement(entry.priority.strategy)}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* 覆写优先级 → 落地写法要求()
|
|
627
|
+
* 该维度不是「提示」,是硬要求:写错写法 = 规则写了也不生效(同权重被组件 scoped 后注入覆盖,或被出厂 !important 压制)。
|
|
628
|
+
*/
|
|
629
|
+
function priorityRequirement(strategy) {
|
|
630
|
+
switch (strategy) {
|
|
631
|
+
case "scope_qualified":
|
|
632
|
+
return "词典 selector 已含作用域限定,按原样落;若实测仍被覆盖,按 deep_qualified 处理。";
|
|
633
|
+
case "deep_qualified":
|
|
634
|
+
return "禁止裸类名。必须 ≥3 层作用域限定(如 html .a .b)或叠加属性/伪类,否则与组件 scoped 平手、被后注入覆盖 → 选择器“写了不生效”。";
|
|
635
|
+
case "self_plus_important":
|
|
636
|
+
return "对手带 !important,本点位也必须 !important(或改走通道变量);只抬特异度无法取胜。";
|
|
637
|
+
default:
|
|
638
|
+
return "(未知策略,按 deep_qualified 保守处理)";
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* oracle 验收基线校验()
|
|
644
|
+
* 两类事故都出在「基线本身不可用」,而不是落地写法:
|
|
645
|
+
* ① 主题错配:效果图是 A 主题、变更集是 B 主题 → 逐点比对必然全红(搬错基线)。
|
|
646
|
+
* ② 覆盖度缺口:效果图里有、changes[] 却未声明的点位 → 永远不可能被还原(欠声明);
|
|
647
|
+
* 反之声明了却不在 sids 里的点位 → 落地后无法机器验收。
|
|
648
|
+
* 两者都属「交付不完整」,与颜色红线同级,必须回流设计侧补齐。
|
|
649
|
+
*/
|
|
650
|
+
function checkOracle(changeset, plan, valueDict) {
|
|
651
|
+
const oracle = changeset.oracle;
|
|
652
|
+
if (!oracle || typeof oracle !== "object") {
|
|
653
|
+
console.log(`\n提示:变更集未提供 oracle 验收基线(场景还原应给)—— 落地后无法做 computed style 逐点比对,还原度不可证明,建议回流设计侧补齐。`);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const sids = Array.isArray(oracle.sids) ? oracle.sids : [];
|
|
658
|
+
const mockup = typeof oracle.mockup === "string" ? oracle.mockup : "";
|
|
659
|
+
console.log(`\n=== 验收基线 oracle(落地后跑 verify 逐点比对)===`);
|
|
660
|
+
console.log(` 效果图:${mockup || "(未给 mockup 路径)"}`);
|
|
661
|
+
console.log(` 待比对点位:${sids.length} 条${sids.length ? ` —— ${sids.join("、")}` : ""}`);
|
|
662
|
+
|
|
663
|
+
const issues = [];
|
|
664
|
+
|
|
665
|
+
// ① 主题一致性:效果图文件名中的主题词 vs global.brand_primary 的主题词
|
|
666
|
+
const themes = new Set(
|
|
667
|
+
Object.keys(valueDict || {})
|
|
668
|
+
.map(k => /^(.+?)-\d+$/.exec(k))
|
|
669
|
+
.filter(Boolean)
|
|
670
|
+
.map(m => m[1]),
|
|
671
|
+
);
|
|
672
|
+
const themeFromValue = (v) => {
|
|
673
|
+
const m = typeof v === "string" ? /^(.+?)-\d+$/.exec(v.trim()) : null;
|
|
674
|
+
return m ? m[1] : null;
|
|
675
|
+
};
|
|
676
|
+
const declaredTheme = themeFromValue(changeset.global && changeset.global.brand_primary);
|
|
677
|
+
const mockupTheme = [...themes].find(t => mockup.includes(t)) || null;
|
|
678
|
+
if (declaredTheme && mockupTheme && declaredTheme !== mockupTheme) {
|
|
679
|
+
issues.push(`主题错配:效果图是「${mockupTheme}」、本变更集主色是「${declaredTheme}」—— 跨主题复用效果图会让 verify 逐点比对必然全红,请换成本主题自己的效果图。`);
|
|
680
|
+
}
|
|
681
|
+
if (!mockupTheme && mockup)
|
|
682
|
+
issues.push(`效果图文件名不含任何已知主题词(${[...themes].join("/")}),无法确认主题一致性 —— 请核对是否复用了其它主题的效果图。`);
|
|
683
|
+
|
|
684
|
+
// ② 覆盖度(双向)
|
|
685
|
+
const declaredIds = plan.map(p => p.id);
|
|
686
|
+
const sidSet = new Set(sids);
|
|
687
|
+
const sidNotDeclared = sids.filter(s => !declaredIds.includes(s));
|
|
688
|
+
const declaredWithoutSid = declaredIds.filter(id => !sidSet.has(id));
|
|
689
|
+
if (sidNotDeclared.length > 0)
|
|
690
|
+
issues.push(`欠声明 ${sidNotDeclared.length} 处:效果图里有、changes[] 未声明 → 该点位永远不可能被还原。\n ${sidNotDeclared.join("、")}`);
|
|
691
|
+
if (declaredWithoutSid.length > 0)
|
|
692
|
+
issues.push(`无基线 ${declaredWithoutSid.length} 处:changes[] 已声明、效果图 sids 未覆盖 → 落地后无法机器验收。\n ${declaredWithoutSid.join("、")}`);
|
|
693
|
+
|
|
694
|
+
if (issues.length === 0) {
|
|
695
|
+
console.log(` ✔ oracle 校验通过:主题一致、覆盖度双向闭合`);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
console.log(`\n⚠ oracle 基线问题(${issues.length} 类):`);
|
|
699
|
+
for (const it of issues)
|
|
700
|
+
console.log(` ✘ ${it}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* 覆盖率校验(场景还原子项级细闸)
|
|
705
|
+
* 设计侧用「登记表」把规范里每条带样式结论的子项变成必登项,交付时以 coverage 记账。
|
|
706
|
+
* 这里只做结构一致性校验:covered 必须等于 applicable、gaps 必须为空。
|
|
707
|
+
* 缺 coverage 时无法判断「是否有组内子项被静默丢失」——属交付完整性的信号缺口。
|
|
708
|
+
*/
|
|
709
|
+
function checkCoverage(changeset) {
|
|
710
|
+
const cov = changeset.coverage;
|
|
711
|
+
if (!cov || typeof cov !== "object") {
|
|
712
|
+
console.log(`\n提示:变更集未提供 coverage 覆盖率记账(场景还原应给)—— 无法判断是否有规范子项被静默丢失,完备性不可数,建议回流设计侧补齐。`);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const num = v => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
716
|
+
const R = num(cov.registrable);
|
|
717
|
+
const A = num(cov.applicable);
|
|
718
|
+
const C = num(cov.covered);
|
|
719
|
+
const gaps = Array.isArray(cov.gaps) ? cov.gaps : [];
|
|
720
|
+
console.log(`\n=== 覆盖率 coverage(子项级细闸)===`);
|
|
721
|
+
console.log(` 登记表:${cov.registry || "(未给路径)"}`);
|
|
722
|
+
console.log(` 登记项 R=${R ?? "?"} / 适用 A=${A ?? "?"} / 已兑现 C=${C ?? "?"}`);
|
|
723
|
+
|
|
724
|
+
const issues = [];
|
|
725
|
+
if (A === null || C === null)
|
|
726
|
+
issues.push("缺 applicable / covered 计数,无法核验完备性。");
|
|
727
|
+
else if (C < A)
|
|
728
|
+
issues.push(`欠声明:已兑现 ${C} < 适用 ${A} —— 有 ${A - C} 条登记项未落进 changes[] / visible / pending。`);
|
|
729
|
+
if (gaps.length > 0)
|
|
730
|
+
issues.push(`gaps 非空(${gaps.length} 条):${gaps.join("、")}`);
|
|
731
|
+
|
|
732
|
+
if (issues.length === 0) {
|
|
733
|
+
console.log(` ✔ 覆盖率校验通过:C = A 且无缺口`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
console.log(`\n⚠ 覆盖率问题(${issues.length} 类):`);
|
|
737
|
+
for (const it of issues)
|
|
738
|
+
console.log(` ✘ ${it}`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** 依次尝试候选路径,返回首个存在者(相对路径基准:变更集所在目录 → 当前工作目录 → 技能仓库根) */
|
|
742
|
+
function resolveFirstExisting(raw, filePath) {
|
|
743
|
+
if (!raw || typeof raw !== "string")
|
|
744
|
+
return null;
|
|
745
|
+
if (path.isAbsolute(raw))
|
|
746
|
+
return existsSync(raw) ? raw : null;
|
|
747
|
+
const bases = [];
|
|
748
|
+
if (filePath)
|
|
749
|
+
bases.push(path.dirname(filePath));
|
|
750
|
+
bases.push(process.cwd(), ROOT);
|
|
751
|
+
for (const base of bases) {
|
|
752
|
+
const candidate = path.resolve(base, raw);
|
|
753
|
+
if (existsSync(candidate))
|
|
754
|
+
return candidate;
|
|
755
|
+
}
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** 词典条目的可用字段集:css_vars + custom_style_map + forms 键 + 结构/存在维度 */
|
|
760
|
+
function entryFieldSet(entry) {
|
|
761
|
+
const fields = new Set(["form", "visible", "position"]);
|
|
762
|
+
for (const bag of ["css_vars", "custom_style_map", "forms"]) {
|
|
763
|
+
for (const k of Object.keys(entry?.[bag] || {}))
|
|
764
|
+
fields.add(k);
|
|
765
|
+
}
|
|
766
|
+
return fields;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** 登记项引用的语义 ID(支持单 sid 与多 sids 两种写法) */
|
|
770
|
+
function registrySids(item) {
|
|
771
|
+
if (Array.isArray(item?.sids))
|
|
772
|
+
return item.sids;
|
|
773
|
+
return item?.sid ? [item.sid] : [];
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** 登记表里唯一允许的「非词典点位」:全局令牌通道(对应变更集顶层 global 节点,无 css selector 故不入词典) */
|
|
777
|
+
const GLOBAL_PSEUDO_ID = "global";
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* 登记表覆盖率(子项级细闸,)
|
|
781
|
+
* 组级台账(restore-checklist)只能防「整组忘光」;组内子项是否被静默丢失,必须由登记表
|
|
782
|
+
* (规范里每条带样式结论的子项 → 一条登记项)来计数。这里以登记表为权威**当场重算**:
|
|
783
|
+
* 适用 A = always 子项 ∪ 效果图已标注同名 data-sid 的子项
|
|
784
|
+
* 兑现 C = A 中在 changes[] / visible:false / pending[].proposed_id 出现者
|
|
785
|
+
* 设计侧自报的 coverage 数字不参与计算,因此无法用「把 applicable 写小」蒙混过关。
|
|
786
|
+
* 有缺口 → 返回缺口数(调用方置退出码 1)。
|
|
787
|
+
*/
|
|
788
|
+
function checkRegistryCoverage(changeset, plan, opts = {}) {
|
|
789
|
+
const raw = opts.registryArg || changeset.coverage?.registry || changeset.oracle?.registry;
|
|
790
|
+
const registryPath = resolveFirstExisting(raw, opts.filePath);
|
|
791
|
+
if (!registryPath) {
|
|
792
|
+
if (opts.registryArg) {
|
|
793
|
+
console.error(`\n✖ 登记表文件不存在:${opts.registryArg}`);
|
|
794
|
+
return 1;
|
|
795
|
+
}
|
|
796
|
+
console.log(`\n提示:未提供登记表(--registry 或 coverage.registry)—— 组级台账只防「整组忘光」,组内子项被静默丢失无法计数,建议场景还原带登记表。`);
|
|
797
|
+
return 0;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
let registry;
|
|
801
|
+
try {
|
|
802
|
+
registry = JSON.parse(readFileSync(registryPath, "utf-8"));
|
|
803
|
+
}
|
|
804
|
+
catch (e) {
|
|
805
|
+
console.error(`\n✖ 登记表读取/解析失败:${registryPath} —— ${e.message}`);
|
|
806
|
+
return 1;
|
|
807
|
+
}
|
|
808
|
+
const items = Array.isArray(registry.items) ? registry.items : [];
|
|
809
|
+
if (items.length === 0) {
|
|
810
|
+
console.error(`\n✖ 登记表不含 items:${registryPath}`);
|
|
811
|
+
return 1;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// 兑现集合 = changes[] ∪ visible:false ∪ pending[].proposed_id ∪ 顶层 global 节点
|
|
815
|
+
const declared = new Set(plan.map(p => p.id));
|
|
816
|
+
for (const p of plan) {
|
|
817
|
+
if (p.change && p.change.visible === false)
|
|
818
|
+
declared.add(p.id);
|
|
819
|
+
}
|
|
820
|
+
for (const p of Array.isArray(changeset.pending) ? changeset.pending : []) {
|
|
821
|
+
if (p && p.proposed_id)
|
|
822
|
+
declared.add(p.proposed_id);
|
|
823
|
+
}
|
|
824
|
+
if (changeset.global && Object.keys(changeset.global).length > 0)
|
|
825
|
+
declared.add(GLOBAL_PSEUDO_ID);
|
|
826
|
+
const mockupSids = new Set(Array.isArray(changeset.oracle?.sids) ? changeset.oracle.sids : []);
|
|
827
|
+
|
|
828
|
+
const applicable = items.filter(it => it.always === true || registrySids(it).some(s => mockupSids.has(s)));
|
|
829
|
+
const gaps = applicable.filter(it => !registrySids(it).every(s => declared.has(s)));
|
|
830
|
+
|
|
831
|
+
console.log(`\n=== 登记表覆盖率(子项级细闸 · 当场重算)===`);
|
|
832
|
+
console.log(` 登记表:${registryPath}(登记项 R=${items.length})`);
|
|
833
|
+
console.log(` 适用 A=${applicable.length} / 已兑现 C=${applicable.length - gaps.length} / 缺口 ${gaps.length}`);
|
|
834
|
+
|
|
835
|
+
// 效果图标注完整性:恒存在的子项必须能在效果图上找到同名 data-sid,否则基线根本抽不出来
|
|
836
|
+
const unannotated = items.filter(it => it.always === true && !registrySids(it).some(s => mockupSids.has(s)));
|
|
837
|
+
if (unannotated.length > 0) {
|
|
838
|
+
const bySid = new Map();
|
|
839
|
+
for (const it of unannotated) {
|
|
840
|
+
for (const s of registrySids(it))
|
|
841
|
+
bySid.set(s, (bySid.get(s) || 0) + 1);
|
|
842
|
+
}
|
|
843
|
+
console.log(` ⚠ 效果图标注缺口 ${unannotated.length} 条:这些子项恒存在,却无同名 data-sid → 基线抽取不全`);
|
|
844
|
+
console.log(` 涉及点位:${[...bySid.entries()].map(([s, n]) => `${s}(${n})`).join("、")}`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (gaps.length === 0) {
|
|
848
|
+
console.log(` ✔ 覆盖率通过:C = A,无子项被静默丢失`);
|
|
849
|
+
return 0;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const byGroup = new Map();
|
|
853
|
+
for (const it of gaps) {
|
|
854
|
+
const g = it.group || "(未分组)";
|
|
855
|
+
if (!byGroup.has(g))
|
|
856
|
+
byGroup.set(g, []);
|
|
857
|
+
byGroup.get(g).push(it);
|
|
858
|
+
}
|
|
859
|
+
console.log(`\n✘ 登记表覆盖率缺口(子项级欠声明,共 ${gaps.length} 条 → exit 1):`);
|
|
860
|
+
for (const [group, list] of byGroup) {
|
|
861
|
+
console.log(` · ${group}(${list.length})`);
|
|
862
|
+
const shown = opts.verbose ? list : list.slice(0, 6);
|
|
863
|
+
for (const it of shown)
|
|
864
|
+
console.log(` ${it.rule} ${registrySids(it).join(" + ")} [${(it.dims || []).join(",")}]`);
|
|
865
|
+
if (!opts.verbose && list.length > shown.length)
|
|
866
|
+
console.log(` …(同组其余 ${list.length - shown.length} 条,加 --verbose 展开)`);
|
|
867
|
+
}
|
|
868
|
+
return gaps.length;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* 登记表通道核验()
|
|
873
|
+
* 与 match 的覆盖率校验互补:不需要变更集,直接拿登记表对词典,回答一件事——
|
|
874
|
+
* 「这张登记表里的必登项,有多少在项目侧真有承载字段」。这是 1:1 对接的**通道缺口清单**:
|
|
875
|
+
* · sid 词典无此 ID 且未标 pending → 硬缺口(登记表引用了不存在的点位,exit 1)
|
|
876
|
+
* · dims 是设计侧通用维度(adjustable-elements 口径),词典无对应字段 → 通道缺口(列出,供补词典)
|
|
877
|
+
*/
|
|
878
|
+
function cmdRegistry(args, dataset) {
|
|
879
|
+
const verbose = args.includes("--verbose");
|
|
880
|
+
const file = args.find(a => !a.startsWith("--"));
|
|
881
|
+
if (!file) {
|
|
882
|
+
console.error("✖ 缺少登记表文件参数:node parser/index.js registry <restore-registry.json> [--verbose]");
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
const p = path.resolve(file);
|
|
886
|
+
let registry;
|
|
887
|
+
try {
|
|
888
|
+
registry = JSON.parse(readFileSync(p, "utf-8"));
|
|
889
|
+
}
|
|
890
|
+
catch (e) {
|
|
891
|
+
console.error(`✖ 登记表读取/解析失败:${e.message}`);
|
|
892
|
+
process.exit(1);
|
|
893
|
+
}
|
|
894
|
+
const items = Array.isArray(registry.items) ? registry.items : [];
|
|
895
|
+
if (items.length === 0) {
|
|
896
|
+
console.error(`✖ 登记表不含 items:${file}`);
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const index = buildMergedIndex(dataset);
|
|
901
|
+
const SOURCES = new Set(["spec", "mockup", "both", "none"]);
|
|
902
|
+
const CHANNELS = new Set(["form", "style", "layer", "structure_ops", "apply_via"]);
|
|
903
|
+
|
|
904
|
+
const hard = [];
|
|
905
|
+
const chanMissing = [];
|
|
906
|
+
const seenRule = new Set();
|
|
907
|
+
const pendingIds = new Set();
|
|
908
|
+
let sidOk = 0;
|
|
909
|
+
let dimTotal = 0;
|
|
910
|
+
let dimOk = 0;
|
|
911
|
+
|
|
912
|
+
for (const it of items) {
|
|
913
|
+
const rule = it.rule || "(无 rule)";
|
|
914
|
+
if (seenRule.has(rule))
|
|
915
|
+
hard.push(`rule 重复:${rule}`);
|
|
916
|
+
seenRule.add(rule);
|
|
917
|
+
if (it.source && !SOURCES.has(it.source))
|
|
918
|
+
hard.push(`${rule}: source "${it.source}" 非法(应为 ${[...SOURCES].join("/")})`);
|
|
919
|
+
for (const c of Array.isArray(it.channel) ? it.channel : []) {
|
|
920
|
+
if (!CHANNELS.has(c))
|
|
921
|
+
hard.push(`${rule}: channel "${c}" 非法(应为 ${[...CHANNELS].join("/")})`);
|
|
922
|
+
}
|
|
923
|
+
const sids = registrySids(it);
|
|
924
|
+
if (sids.length === 0)
|
|
925
|
+
hard.push(`${rule}: 未给 sid / sids`);
|
|
926
|
+
for (const sid of sids) {
|
|
927
|
+
if (sid === GLOBAL_PSEUDO_ID) {
|
|
928
|
+
sidOk++;
|
|
929
|
+
continue; // 全局令牌通道:对应变更集顶层 global 节点,不入词典
|
|
930
|
+
}
|
|
931
|
+
if (it.pending) {
|
|
932
|
+
pendingIds.add(sid);
|
|
933
|
+
if (index[sid])
|
|
934
|
+
hard.push(`${rule}: ${sid} 已标 pending,但词典已有该 ID → 应撤掉 pending 标注`);
|
|
935
|
+
}
|
|
936
|
+
const entry = index[sid];
|
|
937
|
+
if (!entry) {
|
|
938
|
+
if (!it.pending)
|
|
939
|
+
hard.push(`${rule}: ${sid} 词典无此 ID 且未标 pending → 须补词典或改标 pending`);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
sidOk++;
|
|
943
|
+
const fields = entryFieldSet(entry);
|
|
944
|
+
for (const d of Array.isArray(it.dims) ? it.dims : []) {
|
|
945
|
+
dimTotal++;
|
|
946
|
+
if (fields.has(d))
|
|
947
|
+
dimOk++;
|
|
948
|
+
else
|
|
949
|
+
chanMissing.push(`${rule} ${sid}.${d}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
console.log(`=== 登记表通道核验(${path.basename(p)})===`);
|
|
955
|
+
console.log(` 登记项 ${items.length} 条 / 引用点位 ${sidOk} 个命中词典${pendingIds.size ? ` / ${pendingIds.size} 个标 pending(待补词典)` : ""}`);
|
|
956
|
+
console.log(` 维度落点:${dimOk}/${dimTotal} 在词典有对应字段`);
|
|
957
|
+
|
|
958
|
+
if (pendingIds.size > 0) {
|
|
959
|
+
console.log(`\n 待补词典的新点位(设计侧须 changes[] + pending 双写):`);
|
|
960
|
+
for (const id of pendingIds)
|
|
961
|
+
console.log(` - ${id}`);
|
|
962
|
+
}
|
|
963
|
+
if (chanMissing.length > 0) {
|
|
964
|
+
const bySid = new Map();
|
|
965
|
+
for (const line of chanMissing) {
|
|
966
|
+
const sid = line.split(/\s+/)[1].split(".").slice(0, -1).join(".") || "?";
|
|
967
|
+
if (!bySid.has(sid))
|
|
968
|
+
bySid.set(sid, []);
|
|
969
|
+
bySid.get(sid).push(line.split(".").pop());
|
|
970
|
+
}
|
|
971
|
+
console.log(`\n 通道缺口 ${chanMissing.length} 处(设计侧通用维度在词典无字段 → 须补字段或换用已有字段):`);
|
|
972
|
+
for (const [sid, dims] of bySid) {
|
|
973
|
+
const uniq = [...new Set(dims)];
|
|
974
|
+
console.log(` - ${sid}: ${uniq.slice(0, verbose ? uniq.length : 8).join("、")}${!verbose && uniq.length > 8 ? ` …+${uniq.length - 8}` : ""}`);
|
|
975
|
+
}
|
|
976
|
+
if (verbose) {
|
|
977
|
+
console.log(`\n 明细:`);
|
|
978
|
+
for (const line of chanMissing)
|
|
979
|
+
console.log(` ${line}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (hard.length === 0 && chanMissing.length === 0) {
|
|
984
|
+
console.log(`\n✔ 登记表通道核验通过:点位全部命中、维度全部有落点`);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (hard.length > 0) {
|
|
988
|
+
console.error(`\n✖ 硬缺口 ${hard.length} 处(登记表引用了不存在的点位 / 元数据非法)→ exit 1:`);
|
|
989
|
+
for (const h of hard)
|
|
990
|
+
console.error(` ✘ ${h}`);
|
|
991
|
+
process.exit(1);
|
|
608
992
|
}
|
|
993
|
+
console.log(`\n⚠ 存在通道缺口(属「项目侧补词典」事项,不阻断匹配;补齐后还原度才可达 100%)。`);
|
|
609
994
|
}
|
|
610
995
|
|
|
611
996
|
/** 输出结构施工声明(structure_ops):数据 / 结构驱动点位,只写 CSS 是无效声明 */
|
|
@@ -677,7 +1062,7 @@ function checkApplyFields(change, label, violations) {
|
|
|
677
1062
|
}
|
|
678
1063
|
}
|
|
679
1064
|
|
|
680
|
-
// ---- audit:挂载点存在性 +
|
|
1065
|
+
// ---- audit:挂载点存在性 + 死通道(,静态审计,需目标项目源码) ----
|
|
681
1066
|
// 本节的模块级常量(AUDIT_SKIP_DIRS / STRUCT_EXT / STYLE_EXT / DESIGN_LAYER_RE /
|
|
682
1067
|
// BUILTIN_BARE_CLASSES)已在文件顶部声明——模块级常量必须在 main() 调用前初始化,勿下移。
|
|
683
1068
|
|
|
@@ -882,7 +1267,7 @@ function cmdAudit(args, dataset) {
|
|
|
882
1267
|
}
|
|
883
1268
|
}
|
|
884
1269
|
|
|
885
|
-
// ---- verify:渲染态逐点验收(
|
|
1270
|
+
// ---- verify:渲染态逐点验收( 补丁 3 · 闭环验收回路) ----
|
|
886
1271
|
/**
|
|
887
1272
|
* 比对两份 computed style 转储:oracle(效果图期望)vs actual(项目实测)。
|
|
888
1273
|
* 转储格式:{ "<语义ID>": { "<css属性>": "<值>", ... }, ... }
|
|
@@ -1110,7 +1495,7 @@ function cmdList(args, dataset) {
|
|
|
1110
1495
|
console.log(` style 字段:${fields.join(" ")}`);
|
|
1111
1496
|
if (forms.length > 0)
|
|
1112
1497
|
console.log(` form 形态:${forms.join(" / ")}`);
|
|
1113
|
-
//
|
|
1498
|
+
// :落点层级与覆写优先级(设计侧声明 layer / 项目侧选提权策略时可见)
|
|
1114
1499
|
const anchors = Array.isArray(e.anchors) ? e.anchors : [];
|
|
1115
1500
|
const layers = [...new Set(anchors.filter(a => a.kind === "class" && a.layer).map(a => a.layer))];
|
|
1116
1501
|
if (layers.length > 0 || (e.priority && e.priority.strategy))
|
package/parser/regression.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A-
|
|
4
|
+
* A-O 端到端一键回归(作者侧维护工具,不随技能分发)
|
|
5
5
|
*
|
|
6
6
|
* 用法(cwd 任意,脚本内部已固定为 skill 根目录执行):
|
|
7
7
|
* node parser/regression.js
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
* G match 旧协议兼容 → exit 0 且输出含 ⚠ 告警
|
|
18
18
|
* H match 整页场景还原样例 → exit 0 且输出含 #245BFF(壳层+内容区全模块 L1)
|
|
19
19
|
* I build --dry 产物链自检 → exit 0 且输出含 dry-run
|
|
20
|
-
* J match
|
|
20
|
+
* J match 三声明维度 → exit 0 且输出含「结构施工 structure_ops」
|
|
21
21
|
* K audit 挂载点 / 死通道审计 → exit 1(夹具项目故意存在挂载点缺失 / 死通道)
|
|
22
22
|
* L verify 渲染态逐点验收 → exit 1 且三类归因各计其数(夹具含故意缺口)
|
|
23
|
+
* M match 登记表覆盖率细闸 → exit 1(夹具故意欠声明两条恒存在子项)
|
|
24
|
+
* N registry 登记表通道核验 → exit 0 且输出含「登记表通道核验」
|
|
25
|
+
* O registry 硬缺口负例 → exit 1(引用了不存在且未标 pending 的点位)
|
|
23
26
|
*
|
|
24
27
|
* 任一步骤失败 → 本脚本 exit 1,并回显该步骤完整输出便于定位。
|
|
25
28
|
*/
|
|
@@ -109,10 +112,10 @@ const steps = [
|
|
|
109
112
|
},
|
|
110
113
|
{
|
|
111
114
|
key: "J",
|
|
112
|
-
name: "match
|
|
115
|
+
name: "match 三声明维度 + coverage 覆盖率(apply_via / layer / structure_ops / coverage)",
|
|
113
116
|
script: INDEX,
|
|
114
117
|
args: ["match", path.join(FIXTURES, "changeset-v32-fields.json")],
|
|
115
|
-
expect: { code: 0, includes: ["结构施工 structure_ops", "验收基线 oracle"] },
|
|
118
|
+
expect: { code: 0, includes: ["结构施工 structure_ops", "验收基线 oracle", "覆盖率 coverage"] },
|
|
116
119
|
},
|
|
117
120
|
{
|
|
118
121
|
key: "K",
|
|
@@ -128,12 +131,33 @@ const steps = [
|
|
|
128
131
|
args: ["verify", "--oracle", path.join(FIXTURES, "verify", "oracle.json"), "--actual", path.join(FIXTURES, "verify", "actual.json")],
|
|
129
132
|
expect: { code: 1, includes: "选择器落空 1 / 权重被压 1 / 语境错位 2" },
|
|
130
133
|
},
|
|
134
|
+
{
|
|
135
|
+
key: "M",
|
|
136
|
+
name: "match 登记表覆盖率细闸(夹具故意欠声明两条恒存在子项 → exit 1)",
|
|
137
|
+
script: INDEX,
|
|
138
|
+
args: ["match", "--registry", path.join(FIXTURES, "registry-sample.json"), path.join(FIXTURES, "changeset-registry-gap.json")],
|
|
139
|
+
expect: { code: 1, includes: ["登记表覆盖率", "缺口 2"] },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: "N",
|
|
143
|
+
name: "registry 登记表通道核验(点位全部命中 → exit 0)",
|
|
144
|
+
script: INDEX,
|
|
145
|
+
args: ["registry", path.join(FIXTURES, "registry-sample.json")],
|
|
146
|
+
expect: { code: 0, includes: ["登记表通道核验", "登记项 6 条"] },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
key: "O",
|
|
150
|
+
name: "registry 硬缺口负例(引用不存在且未标 pending 的点位 → exit 1)",
|
|
151
|
+
script: INDEX,
|
|
152
|
+
args: ["registry", path.join(FIXTURES, "registry-bad.json")],
|
|
153
|
+
expect: { code: 1, includes: "硬缺口" },
|
|
154
|
+
},
|
|
131
155
|
];
|
|
132
156
|
|
|
133
157
|
const results = [];
|
|
134
158
|
let failed = 0;
|
|
135
159
|
|
|
136
|
-
console.log("=== A-
|
|
160
|
+
console.log("=== A-O 端到端一键回归 ===\n");
|
|
137
161
|
|
|
138
162
|
for (const step of steps) {
|
|
139
163
|
const { status, stdout, stderr } = spawnSync(
|
|
@@ -170,4 +194,4 @@ if (failed > 0) {
|
|
|
170
194
|
console.error(`\n✖ ${failed} 步未通过,见上方详情`);
|
|
171
195
|
process.exit(1);
|
|
172
196
|
}
|
|
173
|
-
console.log("\n✔ A-
|
|
197
|
+
console.log("\n✔ A-O 全部通过,mapping/parser 可提交或发版");
|