@qilitt-mickey/vue3-temp-skill 1.1.48 → 1.1.50
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/mapping/scenes/admin/content.json +50 -10
- package/mapping/scenes/admin/layout.json +53 -7
- package/package.json +1 -1
- package/parser/fixtures/changeset-fullpage-restore.json +56 -2
- package/parser/fixtures/changeset-neg4-g8.json +15 -0
- package/parser/fixtures/changeset-neg5-g8-stub.json +26 -0
- package/parser/fixtures/registry-sample.json +2 -2
- package/parser/fixtures/verify//350/277/220/345/215/225/347/256/241/347/220/206-/351/222/264/350/223/235.html +44 -0
- package/parser/index.js +440 -31
- package/parser/regression.js +21 -5
- package/references/design-apply.md +31 -4
package/parser/index.js
CHANGED
|
@@ -45,6 +45,13 @@ const COMPOSITE_VALUE_RE = /\s/;
|
|
|
45
45
|
const LAYER_VALUES = ["host", "inner"];
|
|
46
46
|
/** 语义 ID 严格格式(apply_via 锚点名须符合) */
|
|
47
47
|
const ID_RE_STRICT = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$/;
|
|
48
|
+
/** 内层盒属性族(changeset-schema §layer):这些属性落在 inner 才生效(G5 几何语境闸使用) */
|
|
49
|
+
const INNER_ONLY_PROPS = new Set(["white_space", "text_overflow", "overflow", "word_break", "text_align", "line_height", "vertical_align", "nowrap", "text_align_last"]);
|
|
50
|
+
/** 几何字段(宿主内边距/外边距族):落 host 时与出厂内层盒自带 padding 叠加,必须同步清零内层(G5) */
|
|
51
|
+
const GEOMETRY_PAD_RE = /^(padding|padding_x|padding_y|padding_inline|padding_block|margin|margin_x|margin_y|margin_inline|margin_block)$/;
|
|
52
|
+
/** 登记表里唯一允许的「非词典点位」:全局令牌通道(对应变更集顶层 global 节点,无 css selector 故不入词典)。
|
|
53
|
+
* ⚠ 必须在 main() 调用前初始化(registry/match 命令运行时即访问;下移会触发 TDZ ReferenceError) */
|
|
54
|
+
const GLOBAL_PSEUDO_ID = "global";
|
|
48
55
|
|
|
49
56
|
/** audit:审计时跳过的目录(依赖 / 产物 / 缓存) */
|
|
50
57
|
const AUDIT_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".output", ".vite", ".nuxt", "coverage", "public"]);
|
|
@@ -97,7 +104,7 @@ function printHelp() {
|
|
|
97
104
|
|
|
98
105
|
用法:
|
|
99
106
|
node parser/index.js build [--dry] 校验映射表并生成 dist/ 产物
|
|
100
|
-
node parser/index.js match <changeset.json...>
|
|
107
|
+
node parser/index.js match <changeset.json...> 对设计变更集执行三级匹配(可多文件;顶层 mode=restore 时 G8 模式闸强制 oracle.mockup + coverage.registry 齐备)
|
|
101
108
|
node parser/index.js match <cs.json> --registry <reg.json>
|
|
102
109
|
匹配 + 登记表覆盖率当场重算(子项级细闸,有缺口 exit 1)
|
|
103
110
|
node parser/index.js match --batch <dir> 项目级批量匹配:目录下全部变更集 + 跨文件汇总
|
|
@@ -234,6 +241,55 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
234
241
|
console.warn(` ⚠ schema "${changeset.schema}" 为历史值,建议设计侧升级为 "design.changeset/1.0"`);
|
|
235
242
|
}
|
|
236
243
|
|
|
244
|
+
// G8 模式闸(2026-09-20 事故机制化):变更集顶层 mode 声明交付模式。
|
|
245
|
+
// mode=restore(场景还原)→ oracle + coverage 强制齐备,缺失 = exit 1。
|
|
246
|
+
// 事故背景:还原项目后重跑技能,产出「只换主题色」的轻量变更集却以还原之名交付——
|
|
247
|
+
// 186 条登记表、oracle 基线、coverage 记账全部缺席,parser 只打提示不阻断,一路绿灯
|
|
248
|
+
// 落地后用户看到「只变了主题、布局细节零变化」。教训文档化 ≠ 机器闸门:
|
|
249
|
+
// 「场景还原应给」的软提示升级为「mode=restore 必须给」的硬闸。
|
|
250
|
+
// 二轮强化:oracle/coverage 须有「芯」——oracle.mockup 与 coverage.registry 非空,
|
|
251
|
+
// 堵住空壳伪基线(有对象无内容)继续蒙混。
|
|
252
|
+
// mode 缺省 / mode=design → 维持软提示口径(轻量主题调整等规范设计场景不受影响)。
|
|
253
|
+
const MODES = ["design", "restore"];
|
|
254
|
+
const mode = changeset.mode === undefined || changeset.mode === null ? "design" : changeset.mode;
|
|
255
|
+
if (!MODES.includes(mode)) {
|
|
256
|
+
console.error(`✖ mode 应为 "design" / "restore",实际 "${mode}"`);
|
|
257
|
+
batchSummary?.invalid.push(path.basename(file));
|
|
258
|
+
if (!batchSummary)
|
|
259
|
+
process.exit(1);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (mode === "restore") {
|
|
263
|
+
// G8 强化(2026-09-20 二轮):不只看「有没有 oracle/coverage 这两个对象」,
|
|
264
|
+
// 还要看**芯**在不在——oracle.mockup 与 coverage.registry 是基线的两根承重梁:
|
|
265
|
+
// · oracle.mockup 空 → 效果图无从抽取,还原度不可证明;
|
|
266
|
+
// · coverage.registry 空 → A/C 无法以登记表为权威当场重算,自报数字即可蒙混,「完备性可数」形同虚设。
|
|
267
|
+
// 空壳写法({ oracle:{sids:[]}, coverage:{applicable:0,covered:0} })过去能一路绿灯,现予拦截。
|
|
268
|
+
const hasStr = v => typeof v === "string" && v.trim().length > 0;
|
|
269
|
+
const missing = [];
|
|
270
|
+
if (!hasStr(changeset.scheme))
|
|
271
|
+
missing.push("scheme(设计方案标识——缺它无法定位「方案 × 场景」的清单 / 登记表,还原范围失去锚点)");
|
|
272
|
+
if (!changeset.oracle || typeof changeset.oracle !== "object")
|
|
273
|
+
missing.push("oracle(验收基线:mockup + sids,效果图即 1:1 的验收凭证)");
|
|
274
|
+
else if (!hasStr(changeset.oracle.mockup))
|
|
275
|
+
missing.push("oracle.mockup(效果图路径——空则基线抽不出来,还原度无从证明)");
|
|
276
|
+
if (!changeset.coverage || typeof changeset.coverage !== "object")
|
|
277
|
+
missing.push("coverage(登记表覆盖率记账:registry + registrable/applicable/covered + gaps)");
|
|
278
|
+
else if (!hasStr(changeset.coverage.registry))
|
|
279
|
+
missing.push("coverage.registry(登记表路径——空则 parser 无法以登记表为权威重算 A/C,自报数字形同虚设)");
|
|
280
|
+
if (missing.length > 0) {
|
|
281
|
+
console.error(`\n✖ G8 模式闸:mode=restore(场景还原)但缺少 ${missing.join(" 与 ")} → 拒绝执行。`);
|
|
282
|
+
console.error(` 场景还原的完备性以登记表覆盖率为分母、以 oracle 为验收基线——缺任一项即「伪还原变更集」:`);
|
|
283
|
+
console.error(` 只换主题色也能一路绿灯落地,但布局细节点位从未被声明,还原度不可证明(2026-09-20 实证事故)。`);
|
|
284
|
+
console.error(` 处置:补齐后重跑;若本次确属轻量主题调整而非场景还原,改 mode="design" 并如实声明。`);
|
|
285
|
+
batchSummary?.invalid.push(path.basename(file));
|
|
286
|
+
if (!batchSummary)
|
|
287
|
+
process.exit(1);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
console.log(` ✔ G8 模式闸:mode=restore,oracle + coverage 齐备(基线三件套完整)`);
|
|
291
|
+
}
|
|
292
|
+
|
|
237
293
|
// 版本校验(mapping_version 缺失或不匹配 → 拒绝。版本恒定 1.0.0,此校验为破坏性升级保险丝:仅双侧同步升版时才会拦截)
|
|
238
294
|
const versions = new Set(
|
|
239
295
|
Object.values(dataset.files).map(d => d.meta?.version).filter(Boolean),
|
|
@@ -271,6 +327,7 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
271
327
|
const plan = [];
|
|
272
328
|
const pending = [];
|
|
273
329
|
const fieldPending = []; // 字段级待补充:ID 命中但 style 字段两层映射均未命中
|
|
330
|
+
const gateViolations = []; // 词典驱动硬闸:suppress 收口(G1)+ 几何语境(G5),违规 = 协议违规同级阻断
|
|
274
331
|
|
|
275
332
|
// global 令牌翻译(design-apply 第 3 步:走 basic/ 词典,主色另需 setEpThemeColor 链路)
|
|
276
333
|
if (changeset.global && Object.keys(changeset.global).length > 0) {
|
|
@@ -336,8 +393,10 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
336
393
|
}
|
|
337
394
|
|
|
338
395
|
// oracle 验收基线(:设计侧给的效果图 + 待比对点位;项目侧落地后跑 verify 逐点 diff)
|
|
339
|
-
|
|
340
|
-
|
|
396
|
+
// mockup 相对路径解析以「变更集同目录」为第一基准(协议约定 oracle 产物与变更集同目录),
|
|
397
|
+
// 其次 cwd / 技能根(历史行为兼容)。
|
|
398
|
+
checkOracle(changeset, plan, valueDict, { strict: mode === "restore", filePath: file });
|
|
399
|
+
checkCoverage(changeset, { strict: mode === "restore" });
|
|
341
400
|
// 登记表覆盖率(子项级细闸):以登记表为权威、当场重算 A/C,设计侧自报的 coverage 不参与
|
|
342
401
|
const regGaps = checkRegistryCoverage(changeset, plan, opts);
|
|
343
402
|
if (batchSummary)
|
|
@@ -349,12 +408,18 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
349
408
|
if (p.level === 1) {
|
|
350
409
|
console.log(`\n[L1 完整命中] ${p.id}`);
|
|
351
410
|
checkFormEnum(p.entry, p.change);
|
|
411
|
+
checkSuppress(p.change, p.entry, p.id, gateViolations);
|
|
412
|
+
checkGeometryContext(p.change, p.entry, p.id, gateViolations);
|
|
413
|
+
checkFieldForms(p.change, p.entry, p.id, gateViolations);
|
|
352
414
|
printApplyPlan(p.entry, p.change, p.id, index);
|
|
353
415
|
printStyleTranslation(p.entry, p.change, p.id, fieldPending, valueDict);
|
|
354
416
|
}
|
|
355
417
|
else if (p.level === 2) {
|
|
356
418
|
console.log(`\n[L2 父模块命中] ${p.id} → 继承 ${p.parent}(${p.entry.selector ?? p.entry.component ?? "?"})`);
|
|
357
419
|
console.log(` 结构:定位父容器按相对位置增量插入;样式:复用父模块映射 + 全局组件映射`);
|
|
420
|
+
checkSuppress(p.change, p.entry, p.id, gateViolations);
|
|
421
|
+
checkGeometryContext(p.change, p.entry, p.id, gateViolations);
|
|
422
|
+
checkFieldForms(p.change, p.entry, p.id, gateViolations);
|
|
358
423
|
printApplyPlan(p.entry, p.change, `${p.id}(继承 ${p.parent})`, index);
|
|
359
424
|
printStyleTranslation(p.entry, p.change, `${p.id}(继承 ${p.parent})`, fieldPending, valueDict);
|
|
360
425
|
}
|
|
@@ -383,6 +448,18 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
383
448
|
const l3 = plan.filter(p => p.level === 3).length;
|
|
384
449
|
console.log(`\n汇总:L1=${l1}(还原度 100%) L2=${l2}(85-90%) L3=${l3}(70-80%,补映射后可达 100%)`);
|
|
385
450
|
|
|
451
|
+
// 词典驱动硬闸(suppress 收口 / 几何语境):违规 = 交付不完整,与禁词同级阻断
|
|
452
|
+
if (gateViolations.length > 0) {
|
|
453
|
+
console.error(`\n✖ 词典硬闸违规(共 ${gateViolations.length} 处 → 拒绝执行):`);
|
|
454
|
+
for (const v of gateViolations)
|
|
455
|
+
console.error(` ✘ ${v}`);
|
|
456
|
+
console.error(` 处置:回变更集补齐 suppress / suppress_inner / layer 声明后重跑;判据均来自词典条目自身,不接受「我观感上对了」。`);
|
|
457
|
+
batchSummary?.invalid.push(path.basename(file));
|
|
458
|
+
if (!batchSummary)
|
|
459
|
+
process.exit(1);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
386
463
|
// 汇入跨文件统计(项目级批量模式)
|
|
387
464
|
if (batchSummary) {
|
|
388
465
|
batchSummary.l1 += l1;
|
|
@@ -534,6 +611,10 @@ function printStyleTranslation(entry, change, idLabel, fieldPending, valueDict)
|
|
|
534
611
|
const layerLabel = spot.layer ? ` · ${spot.layer} 层` : "";
|
|
535
612
|
lines.push(` . ${field}: ${valueLabel} → 自定义 ${entry.custom_style_map[field]}(@ ${spot.target}${layerLabel})`);
|
|
536
613
|
}
|
|
614
|
+
else if (entry.data_channels && entry.data_channels[field]) {
|
|
615
|
+
// 数据/结构通道:值走业务页 prop / 数据 / 模板(禁 CSS 仿冒),非缺口
|
|
616
|
+
lines.push(` . ${field}: ${valueLabel} → 数据/结构通道(${entry.data_channels[field].slice(0, 60)}…)`);
|
|
617
|
+
}
|
|
537
618
|
else {
|
|
538
619
|
unmatched.push(field);
|
|
539
620
|
continue;
|
|
@@ -651,10 +732,13 @@ function priorityRequirement(strategy) {
|
|
|
651
732
|
* 反之声明了却不在 sids 里的点位 → 落地后无法机器验收。
|
|
652
733
|
* 两者都属「交付不完整」,与颜色红线同级,必须回流设计侧补齐。
|
|
653
734
|
*/
|
|
654
|
-
function checkOracle(changeset, plan, valueDict) {
|
|
735
|
+
function checkOracle(changeset, plan, valueDict, opts = {}) {
|
|
655
736
|
const oracle = changeset.oracle;
|
|
656
737
|
if (!oracle || typeof oracle !== "object") {
|
|
657
|
-
|
|
738
|
+
if (opts.strict)
|
|
739
|
+
console.error(`\n✖ mode=restore 下 oracle 缺失(G8 模式闸要求,见变更集校验段)——基线三件套不完整,先回设计侧补齐。`);
|
|
740
|
+
else
|
|
741
|
+
console.log(`\n提示:变更集未提供 oracle 验收基线(场景还原应给)—— 落地后无法做 computed style 逐点比对,还原度不可证明,建议回流设计侧补齐。`);
|
|
658
742
|
return;
|
|
659
743
|
}
|
|
660
744
|
|
|
@@ -705,6 +789,25 @@ function checkOracle(changeset, plan, valueDict) {
|
|
|
705
789
|
const hiddenOnly = new Set(
|
|
706
790
|
plan.filter(p => p.change && p.change.visible === false).map(p => p.id),
|
|
707
791
|
);
|
|
792
|
+
// 实物闭合兜底(G3):带登记表时由 checkRegistryCoverage 做权威实物对账;
|
|
793
|
+
// 未带登记表时此处直接打开 mockup 数 data-sid,防止「sids 自报全绿、实物基线为空」。
|
|
794
|
+
// mockup 路径解析复用 resolveFirstExisting(变更集同目录 → cwd → 技能根)。
|
|
795
|
+
if (!changeset.coverage?.registry && mockup) {
|
|
796
|
+
const mockupPath = resolveFirstExisting(mockup, opts.filePath);
|
|
797
|
+
let physical = null;
|
|
798
|
+
if (mockupPath) {
|
|
799
|
+
try {
|
|
800
|
+
const stripped = readFileSync(mockupPath, "utf-8").replace(/<!--[\s\S]*?-->/g, "");
|
|
801
|
+
physical = new Set([...stripped.matchAll(/data-sid\s*=\s*["']([^"']+)["']/g)].map(m => m[1]));
|
|
802
|
+
}
|
|
803
|
+
catch { /* mockup 不可读,physical 保持 null */ }
|
|
804
|
+
}
|
|
805
|
+
if (physical) {
|
|
806
|
+
const sidNoPhysical = sids.filter(s => !physical.has(s) && !hiddenOnly.has(s));
|
|
807
|
+
if (sidNoPhysical.length > 0)
|
|
808
|
+
issues.push(`实物缺口 ${sidNoPhysical.length} 处:oracle.sids 声明了、效果图 HTML 实物里无对应 data-sid(剥注释后数点)→ 基线抽不出来。\n ${sidNoPhysical.join("、")}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
708
811
|
const sidNotDeclared = sids.filter(s => !declaredIds.includes(s));
|
|
709
812
|
const declaredWithoutSid = declaredIds.filter(id => !sidSet.has(id) && !hiddenOnly.has(id));
|
|
710
813
|
if (hiddenOnly.size > 0)
|
|
@@ -729,10 +832,13 @@ function checkOracle(changeset, plan, valueDict) {
|
|
|
729
832
|
* 这里只做结构一致性校验:covered 必须等于 applicable、gaps 必须为空。
|
|
730
833
|
* 缺 coverage 时无法判断「是否有组内子项被静默丢失」——属交付完整性的信号缺口。
|
|
731
834
|
*/
|
|
732
|
-
function checkCoverage(changeset) {
|
|
835
|
+
function checkCoverage(changeset, opts = {}) {
|
|
733
836
|
const cov = changeset.coverage;
|
|
734
837
|
if (!cov || typeof cov !== "object") {
|
|
735
|
-
|
|
838
|
+
if (opts.strict)
|
|
839
|
+
console.error(`\n✖ mode=restore 下 coverage 缺失(G8 模式闸要求,见变更集校验段)——186 条登记表口径的完备性无从计数,先回设计侧补齐。`);
|
|
840
|
+
else
|
|
841
|
+
console.log(`\n提示:变更集未提供 coverage 覆盖率记账(场景还原应给)—— 无法判断是否有规范子项被静默丢失,完备性不可数,建议回流设计侧补齐。`);
|
|
736
842
|
return;
|
|
737
843
|
}
|
|
738
844
|
const num = v => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
@@ -796,9 +902,6 @@ function registrySids(item) {
|
|
|
796
902
|
return item?.sid ? [item.sid] : [];
|
|
797
903
|
}
|
|
798
904
|
|
|
799
|
-
/** 登记表里唯一允许的「非词典点位」:全局令牌通道(对应变更集顶层 global 节点,无 css selector 故不入词典) */
|
|
800
|
-
const GLOBAL_PSEUDO_ID = "global";
|
|
801
|
-
|
|
802
905
|
/**
|
|
803
906
|
* 登记表覆盖率(子项级细闸,)
|
|
804
907
|
* 组级台账(restore-checklist)只能防「整组忘光」;组内子项是否被静默丢失,必须由登记表
|
|
@@ -849,23 +952,21 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
|
|
|
849
952
|
const declaredSids = new Set(Array.isArray(changeset.oracle?.sids) ? changeset.oracle.sids : []);
|
|
850
953
|
|
|
851
954
|
// 实物闭合:打开 oracle.mockup 这个 HTML,数真实存在的 data-sid(不只看设计侧自报的 oracle.sids)
|
|
955
|
+
// mockup 路径解析复用 resolveFirstExisting(变更集同目录 → cwd → 技能根)。
|
|
852
956
|
const mockupRef = changeset.oracle?.mockup;
|
|
853
957
|
let physicalSids = null;
|
|
854
958
|
let mockupFile = null;
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
mockupFile = c;
|
|
865
|
-
break;
|
|
866
|
-
}
|
|
867
|
-
catch { /* 该候选路径不可读,试下一个 */ }
|
|
959
|
+
const mockupResolved = resolveFirstExisting(typeof mockupRef === "string" ? mockupRef : null, opts.filePath);
|
|
960
|
+
if (mockupResolved) {
|
|
961
|
+
try {
|
|
962
|
+
const html = readFileSync(mockupResolved, "utf-8");
|
|
963
|
+
// 先剥离 HTML 注释再数 data-sid:注释内的同形串是说明文字、不是点位。
|
|
964
|
+
// 不剥离 → 注释可「兜住」一个实际缺失的标注,闸门全绿而基线为空(详见 mockup-extraction-spec.md §7.1)。
|
|
965
|
+
const stripped = html.replace(/<!--[\s\S]*?-->/g, "");
|
|
966
|
+
physicalSids = new Set([...stripped.matchAll(/data-sid\s*=\s*["']([^"']+)["']/g)].map(m => m[1]));
|
|
967
|
+
mockupFile = mockupResolved;
|
|
868
968
|
}
|
|
969
|
+
catch { /* mockup 不可读,physicalSids 保持 null */ }
|
|
869
970
|
}
|
|
870
971
|
// 比对基准:优先效果图实物;实物读不到时退化为自报清单(并在下方告警)
|
|
871
972
|
const mockupSids = physicalSids || declaredSids;
|
|
@@ -1131,7 +1232,125 @@ function checkApplyFields(change, label, violations) {
|
|
|
1131
1232
|
}
|
|
1132
1233
|
}
|
|
1133
1234
|
|
|
1134
|
-
// ----
|
|
1235
|
+
// ---- 词典驱动硬闸:suppress 收口(G1)+ 几何语境(G5) ----
|
|
1236
|
+
// 与 checkApplyFields(协议自洽性校验)不同,这两个闸的判据来自**词典条目自身**:
|
|
1237
|
+
// · suppress_rules:条目声明某 form 时必须收口的出厂互斥通道(声明 form = 换掉旧形态,不是叠加)
|
|
1238
|
+
// · preconditions 含「几何语境随值迁移」:几何值落宿主时必须同步清零内层盒同向属性
|
|
1239
|
+
// 缺失即「新旧形态叠加 / 两层 padding 相加」——值都对、观感错,是「还原度连乘公式」里
|
|
1240
|
+
// 通道正确率的天花板级杀手,因此与禁词同级(阻断,不是提示)。
|
|
1241
|
+
|
|
1242
|
+
/** 提取条目 suppress_rules:优先结构化字段;回落解析 forms 描述与 $note 中的「suppress 必含 xxx」内联标记 */
|
|
1243
|
+
function extractSuppressRules(entry) {
|
|
1244
|
+
const rules = {};
|
|
1245
|
+
if (entry && typeof entry.suppress_rules === "object" && entry.suppress_rules !== null) {
|
|
1246
|
+
for (const [form, channels] of Object.entries(entry.suppress_rules)) {
|
|
1247
|
+
rules[form] = Array.isArray(channels) ? channels.filter(c => typeof c === "string") : [];
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
// 兜底:从 forms 描述文本提取「suppress 必含 名1/名2」标记(历史条目未结构化时仍能机检)
|
|
1251
|
+
const forms = entry?.forms;
|
|
1252
|
+
if (forms && typeof forms === "object" && !Array.isArray(forms)) {
|
|
1253
|
+
for (const [form, desc] of Object.entries(forms)) {
|
|
1254
|
+
if (typeof desc !== "string")
|
|
1255
|
+
continue;
|
|
1256
|
+
// 通道名 = 小写下划线标识符(可多项、以 、/, 分隔);遇非标识符字符(——、。等)即止
|
|
1257
|
+
for (const m of desc.matchAll(/suppress\s*必含\s*([a-z][a-z0-9_]*(?:[、,/][a-z][a-z0-9_]*)*)/gi)) {
|
|
1258
|
+
const channels = m[1].split(/[、,/]/).map(s => s.trim()).filter(Boolean);
|
|
1259
|
+
rules[form] = [...new Set([...(rules[form] || []), ...channels])];
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return rules;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** G1:声明 form 时,词典 suppress_rules 要求的出厂互斥通道必须逐项声明在条目 suppress 数组 */
|
|
1267
|
+
function checkSuppress(change, entry, idLabel, violations) {
|
|
1268
|
+
if (typeof change.form !== "string" || !change.form || !entry)
|
|
1269
|
+
return;
|
|
1270
|
+
const rules = extractSuppressRules(entry);
|
|
1271
|
+
const required = rules[change.form];
|
|
1272
|
+
if (!required || required.length === 0)
|
|
1273
|
+
return;
|
|
1274
|
+
const declared = Array.isArray(change.suppress) ? change.suppress : [];
|
|
1275
|
+
const missing = required.filter(ch => !declared.includes(ch));
|
|
1276
|
+
if (missing.length > 0) {
|
|
1277
|
+
violations.push(
|
|
1278
|
+
`${idLabel}.suppress 缺口:词典要求 form="${change.form}" 必须收口出厂互斥通道 [${required.join("、")}],`
|
|
1279
|
+
+ `实际声明 [${declared.join("、") || "无"}],缺 [${missing.join("、")}]`
|
|
1280
|
+
+ ` —— 声明 form = 换掉旧形态而不是叠加;不收口 = 出厂旧装饰残留(历史事故:侧栏 pill 未收口右竖条 → 双竖条并存)。`
|
|
1281
|
+
+ `通道名取该条目 forms/css_vars 的键名。`,
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* G5:几何语境自检 —— 宿主 + 内层之和必须等于设计值。
|
|
1288
|
+
* 词典 preconditions 含「几何语境随值迁移」标记的条目:style 声明几何字段且 layer 未显式
|
|
1289
|
+
* 指到 inner(即默认落 host)时,必须同步声明 suppress_inner(内层清零口径,纯语义描述),
|
|
1290
|
+
* 否则实际内边距 = 设计值 + 出厂内层自带值(历史事故:th 16px + EP .cell 0 12px = 28px/侧 → 列挤坏)。
|
|
1291
|
+
*/
|
|
1292
|
+
function checkGeometryContext(change, entry, idLabel, violations) {
|
|
1293
|
+
if (!entry || !Array.isArray(entry.preconditions))
|
|
1294
|
+
return;
|
|
1295
|
+
if (!entry.preconditions.some(p => typeof p === "string" && p.includes("几何语境随值迁移")))
|
|
1296
|
+
return;
|
|
1297
|
+
const style = change.style && typeof change.style === "object" ? change.style : null;
|
|
1298
|
+
if (!style)
|
|
1299
|
+
return;
|
|
1300
|
+
const layer = change.layer && typeof change.layer === "object" && !Array.isArray(change.layer) ? change.layer : {};
|
|
1301
|
+
const geometryFields = Object.keys(style).filter(k => GEOMETRY_PAD_RE.test(k));
|
|
1302
|
+
if (geometryFields.length === 0)
|
|
1303
|
+
return;
|
|
1304
|
+
// 内层属性族字段(white-space/text-align 等)已逐字段声明 inner → 施工方已知内层结构,不重复拦截
|
|
1305
|
+
const hasInnerAware = Object.keys(layer).some(f => layer[f] === "inner") || geometryFields.every(f => layer[f] === "inner");
|
|
1306
|
+
if (hasInnerAware)
|
|
1307
|
+
return;
|
|
1308
|
+
const hostGeometry = geometryFields.filter(f => layer[f] !== "inner");
|
|
1309
|
+
if (hostGeometry.length === 0)
|
|
1310
|
+
return;
|
|
1311
|
+
const declaredInner = typeof change.suppress_inner === "string" ? change.suppress_inner.trim() : "";
|
|
1312
|
+
if (!declaredInner) {
|
|
1313
|
+
violations.push(
|
|
1314
|
+
`${idLabel}.suppress_inner 缺口:条目 preconditions 标记「几何语境随值迁移」(出厂内层文本盒自带内边距),`
|
|
1315
|
+
+ `style 几何字段 [${hostGeometry.join("、")}] 落宿主时必须同步声明 suppress_inner(内层清零口径)`
|
|
1316
|
+
+ ` —— 否则实际间距 = 设计值 + 出厂内层值,两层之和 ≠ 设计值(历史事故:表头 16px + 出厂内层 12px/侧 = 28px/侧 → 列宽被挤、标题截断)。`,
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* G7:field_forms 声明-消费闭合 —— style 字段必须被当前 form 消费。
|
|
1323
|
+
* 词典条目声明 field_forms(字段 → 合法 form 枚举)后:变更集 style 里的字段若不在
|
|
1324
|
+
* 当前 form 的合法集内 = 字段无消费者、将被静默丢弃(历史事故:变更集声明 pill +
|
|
1325
|
+
* active_indicator_* 三字段,设计层按 pill 施工把竖条令牌置透明,效果图要求的左竖条消失——
|
|
1326
|
+
* 「声明了却不消费」此前无任何闸门拦截)。修复路径:换 form(含组合形态)或删字段。
|
|
1327
|
+
*/
|
|
1328
|
+
function checkFieldForms(change, entry, idLabel, violations) {
|
|
1329
|
+
if (!entry || typeof entry.field_forms !== "object" || entry.field_forms === null)
|
|
1330
|
+
return;
|
|
1331
|
+
const style = change.style && typeof change.style === "object" ? change.style : null;
|
|
1332
|
+
if (!style)
|
|
1333
|
+
return;
|
|
1334
|
+
const form = typeof change.form === "string" && change.form ? change.form : null;
|
|
1335
|
+
// 无 form 声明时跳过(纯样式条目,字段消费不依赖形态);无该字段映射时跳过
|
|
1336
|
+
if (!form)
|
|
1337
|
+
return;
|
|
1338
|
+
for (const [field, legalForms] of Object.entries(entry.field_forms)) {
|
|
1339
|
+
if (!(field in style))
|
|
1340
|
+
continue;
|
|
1341
|
+
if (!Array.isArray(legalForms) || legalForms.length === 0)
|
|
1342
|
+
continue;
|
|
1343
|
+
if (!legalForms.includes(form)) {
|
|
1344
|
+
violations.push(
|
|
1345
|
+
`${idLabel}.field_forms 闭合违规:style 字段「${field}」仅在 form [${legalForms.join("、")}] 下有消费者,`
|
|
1346
|
+
+ `当前 form="${form}" 不消费该字段 → 声明将被静默丢弃`
|
|
1347
|
+
+ ` —— 修复:换用消费该字段的 form(含组合形态),或删除该字段。禁止「声明字段 + 互斥 form」的组合(历史事故:pill + active_indicator_* → 设计层置透明,左竖条消失)。`,
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
|
|
1135
1354
|
// 本节的模块级常量(AUDIT_SKIP_DIRS / STRUCT_EXT / STYLE_EXT / DESIGN_LAYER_RE /
|
|
1136
1355
|
// BUILTIN_BARE_CLASSES)已在文件顶部声明——模块级常量必须在 main() 调用前初始化,勿下移。
|
|
1137
1356
|
|
|
@@ -1219,8 +1438,9 @@ function findVariableConsumer(varName, sources) {
|
|
|
1219
1438
|
function judgeMount(token, where, hit) {
|
|
1220
1439
|
if (hit.struct.length > 0)
|
|
1221
1440
|
return [];
|
|
1441
|
+
const fmt = arr => arr.map(h => (typeof h === "string" ? h : `${h.file}(${(h.reasons || []).join(";")})`)).slice(0, 2).join("、");
|
|
1222
1442
|
if (hit.style.length > 0)
|
|
1223
|
-
return [`挂载点可疑:${where} 的「${token}」只出现在样式文件(${hit.style
|
|
1443
|
+
return [`挂载点可疑:${where} 的「${token}」只出现在样式文件(${fmt(hit.style)}),未在模板 / 脚本里挂载 —— 规则可能永不命中(历史缺口:列未绑 class-name,金额列右对齐 / 缩略图尺寸失效)`];
|
|
1224
1444
|
if (hit.designLayer.length > 0)
|
|
1225
1445
|
return [`挂载点缺失:${where} 的「${token}」只出现在设计层样式(${hit.designLayer.slice(0, 2).join("、")}),项目里既无结构挂载也无出厂对应 —— 规则不会生效`];
|
|
1226
1446
|
return [`挂载点缺失:${where} 的「${token}」在项目中未找到任何出现位置(选择器永不命中)`];
|
|
@@ -1229,8 +1449,16 @@ function judgeMount(token, where, hit) {
|
|
|
1229
1449
|
/** 单条目审计:selector / anchors 挂载点 + css_var 消费者 */
|
|
1230
1450
|
function auditEntry(entry, sources) {
|
|
1231
1451
|
const issues = [];
|
|
1232
|
-
|
|
1233
|
-
|
|
1452
|
+
const miss = (token) => {
|
|
1453
|
+
// 设计层已承载同类名 → 挂载点可疑降级为提示(不阻断):C 通道由 design-shell.scss 兜底
|
|
1454
|
+
if (hasDesignLayerTakeover(token, sources))
|
|
1455
|
+
return null;
|
|
1456
|
+
return true;
|
|
1457
|
+
};
|
|
1458
|
+
for (const t of extractMountTokens(entry.selector)) {
|
|
1459
|
+
if (miss(t))
|
|
1460
|
+
issues.push(...judgeMount(t, "selector", findMount(t, sources)));
|
|
1461
|
+
}
|
|
1234
1462
|
|
|
1235
1463
|
const anchors = Array.isArray(entry.anchors) ? entry.anchors : [];
|
|
1236
1464
|
for (const a of anchors) {
|
|
@@ -1238,17 +1466,47 @@ function auditEntry(entry, sources) {
|
|
|
1238
1466
|
continue;
|
|
1239
1467
|
if (a.kind === "css_var") {
|
|
1240
1468
|
const consumers = findVariableConsumer(a.target, sources);
|
|
1241
|
-
if (consumers.length === 0)
|
|
1242
|
-
|
|
1469
|
+
if (consumers.length === 0) {
|
|
1470
|
+
// 两类合法豁免(防误报):
|
|
1471
|
+
// ① --el-* 组件库变量:消费者在 element-plus 主题库(node_modules 不在扫描范围),
|
|
1472
|
+
// 项目源码无 var() 属正常 —— EP runtime 必然消费。
|
|
1473
|
+
// ② 设计层已接管变量定义(design-shell 显式赋值):suppress / 接管属故意行为
|
|
1474
|
+
// (词典 preconditions 有明示的条目,如 --vts-theme-menu-active-before)。
|
|
1475
|
+
if (a.target.startsWith("--el-")) {
|
|
1476
|
+
console.log(` ℹ ${a.target} 项目源码无 var() 消费者 —— 由组件库运行时消费(node_modules 不在扫描范围),不按死通道处理`);
|
|
1477
|
+
}
|
|
1478
|
+
else if (designLayerDefines(a.target, sources)) {
|
|
1479
|
+
console.log(` ℹ ${a.target} 项目源码无 var() 消费者 —— 设计层已接管该变量定义(suppress / 接管属故意行为),不按死通道处理`);
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
issues.push(`死通道:CSS 变量 ${a.target} 在项目中无 var() 消费者(只定义不消费 = 通道死路,翻译再准也不生效)`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1243
1485
|
}
|
|
1244
1486
|
else {
|
|
1245
|
-
for (const t of extractMountTokens(a.target))
|
|
1246
|
-
|
|
1487
|
+
for (const t of extractMountTokens(a.target)) {
|
|
1488
|
+
if (miss(t))
|
|
1489
|
+
issues.push(...judgeMount(t, `anchors(${a.layer || "未标层级"})`, findMount(t, sources)));
|
|
1490
|
+
}
|
|
1247
1491
|
}
|
|
1248
1492
|
}
|
|
1249
1493
|
return [...new Set(issues)];
|
|
1250
1494
|
}
|
|
1251
1495
|
|
|
1496
|
+
/** 判定「设计层已承载」的豁免:条目 anchors/selector 的类词元在设计层文件出现时,
|
|
1497
|
+
* 挂载点可疑可降级为提示 —— 设计层覆写本身就是合法落点(design-shell.scss 是 C 通道终点)。 */
|
|
1498
|
+
function hasDesignLayerTakeover(token, sources) {
|
|
1499
|
+
const needle = token.startsWith("[") ? token.slice(1, -1) : token;
|
|
1500
|
+
const re = new RegExp(`(^|[^\\w-])${escapeRe(needle)}([^\\w-]|$)`);
|
|
1501
|
+
return sources.some(s => DESIGN_LAYER_RE.test(s.file) && re.test(s.text));
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/** 设计层是否显式定义了该 CSS 变量(suppress / 接管动作),定义在非设计层的令牌宿主不算 */
|
|
1505
|
+
function designLayerDefines(varName, sources) {
|
|
1506
|
+
const re = new RegExp(`${escapeRe(varName)}\\s*:`);
|
|
1507
|
+
return sources.some(s => DESIGN_LAYER_RE.test(s.file) && re.test(s.text));
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1252
1510
|
function cmdAudit(args, dataset) {
|
|
1253
1511
|
const projIdx = args.indexOf("--project");
|
|
1254
1512
|
const projectRoot = projIdx > -1 ? args[projIdx + 1] : null;
|
|
@@ -1276,6 +1534,7 @@ function cmdAudit(args, dataset) {
|
|
|
1276
1534
|
|
|
1277
1535
|
// 给了变更集 → 只审被引用的点位(含 apply_via 指向的锚点来源);否则审词典全量
|
|
1278
1536
|
let targetIds;
|
|
1537
|
+
const loadedChangesets = []; // G2/G4:保留完整变更集供施工证据与 T 通道对账
|
|
1279
1538
|
if (files.length > 0) {
|
|
1280
1539
|
const wanted = new Set();
|
|
1281
1540
|
for (const f of files) {
|
|
@@ -1287,6 +1546,7 @@ function cmdAudit(args, dataset) {
|
|
|
1287
1546
|
console.error(`✖ 变更集读取失败 ${f}:${e.message}`);
|
|
1288
1547
|
process.exit(1);
|
|
1289
1548
|
}
|
|
1549
|
+
loadedChangesets.push(cs);
|
|
1290
1550
|
for (const c of cs.changes || []) {
|
|
1291
1551
|
if (c.id)
|
|
1292
1552
|
wanted.add(c.id);
|
|
@@ -1304,6 +1564,113 @@ function cmdAudit(args, dataset) {
|
|
|
1304
1564
|
console.log(`源码文件:${sources.length}(已剔除 node_modules / dist 等产物目录)`);
|
|
1305
1565
|
console.log(`检查条目:${targetIds.length}${files.length > 0 ? "(仅变更集引用点位)" : "(词典全量)"}`);
|
|
1306
1566
|
|
|
1567
|
+
// ---- G2:structure_ops 施工证据(台账落地断言)----
|
|
1568
|
+
// 数据 / 结构驱动点位必须在项目源码里找到施工痕迹,纯 CSS 落点声明 structure_ops = 冒充施工:
|
|
1569
|
+
// add_node/remove_node/move_node/render_branch/bind_source → 按条目 selector/anchors 的语义类名
|
|
1570
|
+
// 在**非设计层**源码(含模板与数据脚本)里查证该节点/绑定的存在性(结构文件命中才算兑现)。
|
|
1571
|
+
// 历史事故:操作列分割线 add_node 声明了多轮从未施工,CSS 覆写冒充结构改动 → 按钮间距 / 列宽永不达标。
|
|
1572
|
+
let structIssues = 0;
|
|
1573
|
+
if (loadedChangesets.length > 0) {
|
|
1574
|
+
console.log(`\n=== structure_ops 施工证据(G2 · 模板 diff 取证)===`);
|
|
1575
|
+
for (const cs of loadedChangesets) {
|
|
1576
|
+
for (const [i, c] of (cs.changes || []).entries()) {
|
|
1577
|
+
const ops = Array.isArray(c.structure_ops) ? c.structure_ops : [];
|
|
1578
|
+
if (ops.length === 0 || !c.id)
|
|
1579
|
+
continue;
|
|
1580
|
+
const entry = index[c.id];
|
|
1581
|
+
const probes = collectStructProbes(entry, c);
|
|
1582
|
+
const evidence = probes
|
|
1583
|
+
.map(p => ({ probe: p, hits: findStructEvidence(p, sources) }))
|
|
1584
|
+
.filter(x => x.hits.length > 0);
|
|
1585
|
+
if (probes.length === 0) {
|
|
1586
|
+
console.log(` ⚠ changes[${i}] ${c.id}:structure_ops ${ops.length} 项,但词典条目无 selector/anchors 可提取证据探针 → 人工核验施工台账(design-apply §施工台账)`);
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (evidence.length === 0) {
|
|
1590
|
+
console.log(` ✘ ${c.id}:structure_ops 声明 [${ops.map(o => o.op).join("/")}],但源码中找不到任何施工证据探针 [${probes.join("、")}](非设计层结构文件零命中)`);
|
|
1591
|
+
console.log(` → 纯 CSS 覆写不能兑现结构操作;须改模板/数据并在交付说明附施工台账,或从变更集移除该声明`);
|
|
1592
|
+
structIssues++;
|
|
1593
|
+
}
|
|
1594
|
+
else {
|
|
1595
|
+
const files1 = [...new Set(evidence.flatMap(x => x.hits.map(h => h.file)))];
|
|
1596
|
+
console.log(` ✔ ${c.id}:structure_ops 证据命中 ${evidence.length}/${probes.length} 探针(${files1.slice(0, 3).join("、")})`);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (structIssues === 0)
|
|
1601
|
+
console.log(` ✔ 全部 structure_ops 均有源码施工证据`);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// ---- G4:T 通道对账(运行时令牌 ↔ 变更集主题/形态声明)----
|
|
1605
|
+
// T 通道(platform-config.json / 运行时令牌)优先级最高、也最易被遗忘:
|
|
1606
|
+
// 主题色不一致 → 全局主色错;页签 ShowModel 与 form 叠加 → 新旧形态并存。
|
|
1607
|
+
let tIssues = 0;
|
|
1608
|
+
if (loadedChangesets.length > 0) {
|
|
1609
|
+
console.log(`\n=== T 通道对账(G4 · platform-config ↔ 变更集声明)===`);
|
|
1610
|
+
const pcPath = path.join(root, "public", "platform-config.json");
|
|
1611
|
+
let pc = null;
|
|
1612
|
+
try {
|
|
1613
|
+
pc = JSON.parse(readFileSync(pcPath, "utf-8"));
|
|
1614
|
+
console.log(` platform-config:${pcPath}`);
|
|
1615
|
+
}
|
|
1616
|
+
catch {
|
|
1617
|
+
console.log(` ⚠ 未能读取 ${pcPath}(无 public/platform-config.json)→ 跳过 T 通道对账`);
|
|
1618
|
+
}
|
|
1619
|
+
if (pc) {
|
|
1620
|
+
const tChecks = [];
|
|
1621
|
+
// ① 主题色:global.brand_primary(语义值经译表解析为 hex)↔ EpThemeColor
|
|
1622
|
+
const declaredBrand = loadedChangesets.map(cs => cs.global?.brand_primary).find(Boolean);
|
|
1623
|
+
if (declaredBrand && typeof declaredBrand === "string") {
|
|
1624
|
+
const allDict = buildSemanticValueIndex(dataset);
|
|
1625
|
+
const hex = resolveSemanticValue(declaredBrand, allDict) || (/^#/.test(declaredBrand) ? declaredBrand : null);
|
|
1626
|
+
if (hex && pc.EpThemeColor && String(pc.EpThemeColor).toLowerCase() !== String(hex).toLowerCase()) {
|
|
1627
|
+
tChecks.push(`✘ 主题色不一致:变更集 brand_primary=${declaredBrand}(解析 ${hex}),platform-config.EpThemeColor=${pc.EpThemeColor} → 改 platform-config 或运行时令牌持久化(T 通道先于 C 通道)`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
// ② 页签形态:tabs form ↔ ShowModel(出厂 flat/chrome 形态都运行在 ShowModel=card 结构上)
|
|
1631
|
+
const tabsForm = loadedChangesets
|
|
1632
|
+
.flatMap(cs => (cs.changes || []).filter(c => c.id === "admin.layout.tabs" && typeof c.form === "string").map(c => c.form))
|
|
1633
|
+
.find(Boolean);
|
|
1634
|
+
if (tabsForm && pc.ShowModel && !["flat", "chrome", "card"].includes(pc.ShowModel) === false) {
|
|
1635
|
+
// flat 与 card 共用 card 结构,chrome 形态要求 ShowModel=chrome
|
|
1636
|
+
if (tabsForm === "chrome" && pc.ShowModel !== "chrome") {
|
|
1637
|
+
tChecks.push(`✘ 页签形态不一致:变更集 form=chrome 需 ShowModel=chrome,platform-config.ShowModel=${pc.ShowModel}`);
|
|
1638
|
+
}
|
|
1639
|
+
else if (tabsForm !== "chrome" && pc.ShowModel === "chrome") {
|
|
1640
|
+
tChecks.push(`✘ 页签形态不一致:变更集 form=${tabsForm}(card/flat 结构),platform-config.ShowModel=chrome → 运行时仍是梯形页签,设计层覆写不改变结构形态`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
// ③ 显隐类 D 通道开关:变更集 visible:false 的点位若词典标了 visible_prop,必须核对开关
|
|
1644
|
+
const visibleProps = [];
|
|
1645
|
+
for (const cs of loadedChangesets) {
|
|
1646
|
+
for (const c of (cs.changes || [])) {
|
|
1647
|
+
if (c.visible !== false || !c.id)
|
|
1648
|
+
continue;
|
|
1649
|
+
const entry = index[c.id] || index[String(c.id).split(".").slice(0, -1).join(".")];
|
|
1650
|
+
if (entry?.visible_prop)
|
|
1651
|
+
visibleProps.push({ id: c.id, prop: entry.visible_prop });
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
for (const vp of visibleProps) {
|
|
1655
|
+
const m = /^(\w+)(platform-config\.json)$/.exec(vp.prop) || /^(\w+)$/.exec(vp.prop.split("(")[0].trim());
|
|
1656
|
+
const key = m ? m[1] : null;
|
|
1657
|
+
if (key && key in pc && pc[key] === true) {
|
|
1658
|
+
tChecks.push(`✘ 显隐不一致:${vp.id} 声明 visible:false,但词典 visible_prop=${vp.prop} 且 platform-config.${key}=${pc[key]} → 开关未关,点位仍渲染(D 通道先于 S/C 通道)`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (tChecks.length === 0) {
|
|
1662
|
+
console.log(` ✔ T/D 通道一致:主题色、页签形态、显隐开关均与变更集声明对齐`);
|
|
1663
|
+
}
|
|
1664
|
+
else {
|
|
1665
|
+
for (const t of tChecks) {
|
|
1666
|
+
console.log(` ${t}`);
|
|
1667
|
+
tIssues++;
|
|
1668
|
+
}
|
|
1669
|
+
console.log(` 处置:T 通道(platform-config / 运行时令牌持久化)优先级最高,先清 T 再谈 S/C;存量 localStorage 持久化值须一并断言清零。`);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1307
1674
|
let problems = 0;
|
|
1308
1675
|
const clean = [];
|
|
1309
1676
|
for (const id of targetIds) {
|
|
@@ -1334,6 +1701,48 @@ function cmdAudit(args, dataset) {
|
|
|
1334
1701
|
else {
|
|
1335
1702
|
console.log(`✔ 无挂载 / 通道问题(渲染态是否一致仍需 §验收比对确认:静态审计查不出「值对了、位置不对」)`);
|
|
1336
1703
|
}
|
|
1704
|
+
// G2/G4 违规同样置退出码(与挂载/通道问题同级 —— 施工没兑现、T 通道没清零 = 交付不完整)
|
|
1705
|
+
if (structIssues > 0) {
|
|
1706
|
+
console.log(`✖ structure_ops 施工证据缺失 ${structIssues} 处(G2)`);
|
|
1707
|
+
process.exitCode = 1;
|
|
1708
|
+
}
|
|
1709
|
+
if (tIssues > 0) {
|
|
1710
|
+
console.log(`✖ T 通道对账不一致 ${tIssues} 处(G4)`);
|
|
1711
|
+
process.exitCode = 1;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/** G2 辅助:从词典条目与 structure_ops 声明中提取「施工证据探针」(语义类名 / 节点名,不含技术词)。
|
|
1716
|
+
* 探针来源优先级:条目 anchors 的 class target 词元 → selector 词元 → op.node 语义名。 */
|
|
1717
|
+
function collectStructProbes(entry, change) {
|
|
1718
|
+
const probes = new Set();
|
|
1719
|
+
const pushTokens = (sel) => {
|
|
1720
|
+
if (typeof sel !== "string")
|
|
1721
|
+
return;
|
|
1722
|
+
for (const m of sel.matchAll(/\.([a-zA-Z][\w-]*__(?:divider|tag|icon|close|title|badge|arrow)[\w-]*|[a-zA-Z][\w-]*-divider[\w-]*|vts-[\w-]+|re-table-operate__[\w-]+)/g))
|
|
1723
|
+
probes.add(m[1]);
|
|
1724
|
+
};
|
|
1725
|
+
if (entry) {
|
|
1726
|
+
for (const a of (Array.isArray(entry.anchors) ? entry.anchors : [])) {
|
|
1727
|
+
if (a && a.kind === "class" && typeof a.target === "string")
|
|
1728
|
+
pushTokens(a.target);
|
|
1729
|
+
}
|
|
1730
|
+
pushTokens(entry.selector);
|
|
1731
|
+
}
|
|
1732
|
+
// op.node 语义名(如 tab_divider):既可能是设计层伪元素名(tab_divider → ::before 不可探测),
|
|
1733
|
+
// 也可能是真实类名(status_tag);转词元时保留下划线形式,匹配源码任意出现(类名/常量名/注释均可)
|
|
1734
|
+
for (const op of (Array.isArray(change.structure_ops) ? change.structure_ops : [])) {
|
|
1735
|
+
if (op && typeof op.node === "string" && op.node.trim())
|
|
1736
|
+
probes.add(op.node.trim());
|
|
1737
|
+
}
|
|
1738
|
+
return [...probes];
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/** G2 辅助:在非设计层源码中查找探针出现(结构文件命中 = 施工痕迹;仅样式文件命中不算) */
|
|
1742
|
+
function findStructEvidence(probe, sources) {
|
|
1743
|
+
const re = new RegExp(`(?:class|className|classList|node|name|id|label|key|prop)["']?\\s*[:=]\\s*["']${escapeRe(probe)}["']|${escapeRe(probe)}`);
|
|
1744
|
+
return sources.filter(h => h.kind === "struct" && !DESIGN_LAYER_RE.test(h.file) && re.test(h.text))
|
|
1745
|
+
.map(h => ({ file: h.file }));
|
|
1337
1746
|
}
|
|
1338
1747
|
|
|
1339
1748
|
// ---- verify:渲染态逐点验收( 补丁 3 · 闭环验收回路) ----
|