@qilitt-mickey/vue3-temp-skill 1.1.53 → 1.1.54

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 CHANGED
@@ -92,7 +92,7 @@ tags: [vue3, typescript, element-plus, pinia, vite, unocss, crud, demo, code-qua
92
92
 
93
93
  - 对照 `references/code-quality.md` 检查类型、组件复用、API、路由、权限、安全、异常反馈、响应式和可演进性。
94
94
  - 在目标项目执行 `pnpm lint:fix`、`pnpm type-check`;改动构建配置时再执行 `pnpm build`。
95
- - **设计文件落地时追加渲染态验收**:变更集含 `oracle`(或效果图带 `data-sid`)时,跑 `node parser/index.js verify`——抽取项目同名 `data-sid` 元素的 computed style,与效果图期望值逐点 diff;缺口按三类归因(选择器落空 / 权重被压 / 语境错位)修正,禁止用 `!important` 掩盖。
95
+ - **设计文件落地时追加渲染态验收**:变更集含 `oracle`(或效果图带 `data-sid`)时,跑 `node parser/index.js verify`——抽取项目同名 `data-sid` 元素的 computed style,与效果图期望值逐点 diff;缺口按四类归因(选择器落空 / 权重被压 / 语境错位 / **存在性缺口 G12**:`oracle.absent` 点位仍可见)修正,禁止用 `!important` 掩盖。多轮迭代场景每轮动工前先 `--snapshot` 导快照、交付前 `--baseline` 比对——上轮命中本轮退步 = 基线漂移,先回归上轮修法。申报过的固有残差(`oracle.residual_accepted`)不计缺口。
96
96
  - **设计文件落地时核对登记表覆盖率**:变更集带登记表(`coverage.registry` 或 `--registry`)时,跑 `node parser/index.js match <cs.json> --registry <restore-registry.json>`——`C = A` 且无缺口才可交付;parser **当场以登记表为权威重算**,设计侧自报的 `coverage` 数字不参与。单独核验登记表对词典的通道就绪度用 `node parser/index.js registry <restore-registry.json>`(硬缺口 exit 1、通道缺口列出待补字段)。
97
97
  - 最终说明:命中能力组、修改文件、功能验证、命令结果、未解决问题和下一步。
98
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qilitt-mickey/vue3-temp-skill",
3
- "version": "1.1.53",
3
+ "version": "1.1.54",
4
4
  "type": "module",
5
5
  "description": "Vue 3 企业级中后台项目开发规范技能包 — core-kernel 架构、按需功能模块",
6
6
  "bin": {
@@ -81,7 +81,7 @@
81
81
  { "id": "admin.content.pagination", "style": { "active_bg": "钴蓝-5", "active_color": "#FFFFFF" } }
82
82
  ],
83
83
  "oracle": {
84
- "mockup": "verify/运单管理-钴蓝.html",
84
+ "mockup": "verify/回归夹具-列表页-钴蓝.html",
85
85
  "sids": [
86
86
  "admin.layout.sidebar",
87
87
  "admin.layout.sidebar.logo_area",
@@ -17,7 +17,7 @@
17
17
  { "id": "admin.content.pagination", "style": { "active_bg": "橘橙-5", "active_color": "#FFFFFF" } }
18
18
  ],
19
19
  "oracle": {
20
- "mockup": "verify/运单管理-钴蓝.html",
20
+ "mockup": "verify/回归夹具-列表页-钴蓝.html",
21
21
  "theme_mapping": { "from": "钴蓝", "to": "橘橙" },
22
22
  "sids": [
23
23
  "admin.layout.sidebar.menu",
@@ -35,7 +35,7 @@
35
35
  { "id": "admin.content.pagination", "style": { "active_bg": "钴蓝-5", "active_color": "#FFFFFF" } }
36
36
  ],
37
37
  "oracle": {
38
- "mockup": "verify/运单管理-钴蓝.html",
38
+ "mockup": "verify/回归夹具-列表页-钴蓝.html",
39
39
  "sids": [
40
40
  "admin.content.table.header_cell",
41
41
  "admin.content.table.cell",
@@ -35,7 +35,7 @@
35
35
  { "id": "admin.content.pagination", "style": { "active_bg": "橘橙-5", "active_color": "#FFFFFF" } }
36
36
  ],
37
37
  "oracle": {
38
- "mockup": "verify/运单管理-钴蓝.html",
38
+ "mockup": "verify/回归夹具-列表页-钴蓝.html",
39
39
  "sids": [
40
40
  "admin.content.table.header_cell",
41
41
  "admin.content.table.cell",
@@ -17,7 +17,7 @@
17
17
  }
18
18
  ],
19
19
  "oracle": {
20
- "mockup": "运单管理-钴蓝.html",
20
+ "mockup": "verify/回归夹具-列表页-钴蓝.html",
21
21
  "sids": [
22
22
  "admin.content.table.header_cell",
23
23
  "admin.content.table.cell",
@@ -45,7 +45,7 @@
45
45
  }
46
46
  ],
47
47
  "oracle": {
48
- "mockup": "page-specs/styles/default/scenes/admin/mockups/list-page/运单管理-钴蓝.html",
48
+ "mockup": "verify/回归夹具-列表页-钴蓝.html",
49
49
  "sids": [
50
50
  "admin.content.table.header_cell",
51
51
  "admin.content.table.cell_status",
@@ -0,0 +1,29 @@
1
+ {
2
+ "$comment": "verify 夹具 · 项目实测(clean 全绿对照,与 oracle.json + actual.json 三件配套):全部属性命中、absent 点位已收口(display:none)、申报残差点位允许与效果图不同。供 U 步正例(exit 0)与 V 步快照基线段使用。",
3
+ "sids": {
4
+ "admin.content.table.header_cell": {
5
+ "padding": "16px",
6
+ "font-weight": "500",
7
+ "text-align": "left",
8
+ "background-color": "#fafafa",
9
+ "white-space": "nowrap"
10
+ },
11
+ "admin.content.table.cell_amount": {
12
+ "text-align": "right"
13
+ },
14
+ "admin.layout.tabs.item_active": {
15
+ "font-weight": "500",
16
+ "color": "#245bff"
17
+ },
18
+ "admin.layout.header.user_profile": {
19
+ "gap": "12px"
20
+ },
21
+ "admin.layout.header.breadcrumb": {
22
+ "display": "none"
23
+ },
24
+ "admin.layout.sidebar.logo_area": {
25
+ "background-color": "#10214d"
26
+ },
27
+ "$note": "logo_area 背景与效果图期望 #0B1A3C 不同 = 已申报固有残差(oracle.residual_accepted 命中后不计缺口,U 步正例演示)"
28
+ }
29
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "verify 夹具 · 项目实测(含 3 类故意缺口,用于回归):header_cell 字重被压 + 对齐语境错位;cell_amount 选择器落空;user_profile gap 语境错位。",
2
+ "$comment": "verify 夹具 · 项目实测(含 3 类故意缺口 + 1 类存在性缺口,用于回归):header_cell 字重被压 + 对齐语境错位 + white-space 未取到(期望值加密后暴露「属性所在规则未命中」新形态);cell_amount 选择器落空;user_profile gap 语境错位;breadcrumb 仍渲染可见(G12 存在性缺口故意展示)。logo_area 命中 = L 夹具不制造残差缺口、保持归因计数稳定;残差演示见 actual-clean.json 的 U 步。",
3
3
  "sids": {
4
4
  "admin.content.table.header_cell": {
5
5
  "padding": "16px",
@@ -13,6 +13,12 @@
13
13
  },
14
14
  "admin.layout.header.user_profile": {
15
15
  "gap": "8px"
16
+ },
17
+ "admin.layout.header.breadcrumb": {
18
+ "display": "flex"
19
+ },
20
+ "admin.layout.sidebar.logo_area": {
21
+ "background-color": "#0b1a3c"
16
22
  }
17
23
  }
18
24
  }
@@ -1,11 +1,15 @@
1
1
  {
2
- "$comment": "verify 夹具 · 效果图期望(computed style 转储外形)。真实抽取由浏览器链路完成,本文件仅用于回归。",
2
+ "$comment": "verify 夹具 · 效果图期望(computed style 转储外形)。真实抽取由浏览器链路完成,本文件仅用于回归。顶层扩展字段:absent=效果图上不存在的出厂点位(G12 存在性闸断言目标,2026-09-21 面包屑事故机制化);residual_accepted=申报过的固有残差(命中豁免不计缺口,reason 必填)。header_cell 增补 white-space 期望 = 列头截断防线的期望值加密(oracle 无期望值的属性验了等于没验)。",
3
3
  "sids": {
4
+ "admin.layout.sidebar.logo_area": {
5
+ "background-color": "#0B1A3C"
6
+ },
4
7
  "admin.content.table.header_cell": {
5
8
  "padding": "16px",
6
9
  "font-weight": "500",
7
10
  "text-align": "left",
8
- "background-color": "#FAFAFA"
11
+ "background-color": "#FAFAFA",
12
+ "white-space": "nowrap"
9
13
  },
10
14
  "admin.content.table.cell_amount": {
11
15
  "text-align": "right"
@@ -17,5 +21,11 @@
17
21
  "admin.layout.header.user_profile": {
18
22
  "gap": "12px"
19
23
  }
20
- }
24
+ },
25
+ "absent": [
26
+ "admin.layout.header.breadcrumb"
27
+ ],
28
+ "residual_accepted": [
29
+ { "sid": "admin.layout.sidebar.logo_area", "reason": "出厂 logo 图形属业务资产,效果图占位图形与实际 logo 不逐像素比对(restore-checklist §三:只还原载体模块)" }
30
+ ]
21
31
  }
@@ -0,0 +1,87 @@
1
+ {
2
+ "generated_at": "2026-09-22T01:04:15.248Z",
3
+ "oracle": "E:\\work\\Project\\Template\\skills\\vue3-temp-skill\\parser\\fixtures\\verify\\oracle.json",
4
+ "actual": "E:\\work\\Project\\Template\\skills\\vue3-temp-skill\\parser\\fixtures\\verify\\actual-clean.json",
5
+ "points": {
6
+ "admin.layout.sidebar.logo_area": {
7
+ "verdict": "miss",
8
+ "props": {
9
+ "background-color": {
10
+ "expected": "#0B1A3C",
11
+ "actual": "#10214d",
12
+ "verdict": "residual",
13
+ "kind": "权重被压"
14
+ }
15
+ }
16
+ },
17
+ "admin.content.table.header_cell": {
18
+ "verdict": "hit",
19
+ "props": {
20
+ "padding": {
21
+ "expected": "16px",
22
+ "actual": "16px",
23
+ "verdict": "hit"
24
+ },
25
+ "font-weight": {
26
+ "expected": "500",
27
+ "actual": "500",
28
+ "verdict": "hit"
29
+ },
30
+ "text-align": {
31
+ "expected": "left",
32
+ "actual": "left",
33
+ "verdict": "hit"
34
+ },
35
+ "background-color": {
36
+ "expected": "#FAFAFA",
37
+ "actual": "#fafafa",
38
+ "verdict": "hit"
39
+ },
40
+ "white-space": {
41
+ "expected": "nowrap",
42
+ "actual": "nowrap",
43
+ "verdict": "hit"
44
+ }
45
+ }
46
+ },
47
+ "admin.content.table.cell_amount": {
48
+ "verdict": "hit",
49
+ "props": {
50
+ "text-align": {
51
+ "expected": "right",
52
+ "actual": "right",
53
+ "verdict": "hit"
54
+ }
55
+ }
56
+ },
57
+ "admin.layout.tabs.item_active": {
58
+ "verdict": "hit",
59
+ "props": {
60
+ "font-weight": {
61
+ "expected": "500",
62
+ "actual": "500",
63
+ "verdict": "hit"
64
+ },
65
+ "color": {
66
+ "expected": "#245BFF",
67
+ "actual": "#245bff",
68
+ "verdict": "hit"
69
+ }
70
+ }
71
+ },
72
+ "admin.layout.header.user_profile": {
73
+ "verdict": "hit",
74
+ "props": {
75
+ "gap": {
76
+ "expected": "12px",
77
+ "actual": "12px",
78
+ "verdict": "hit"
79
+ }
80
+ }
81
+ },
82
+ "admin.layout.header.breadcrumb": {
83
+ "verdict": "absent-hit",
84
+ "evidence": "display=none visibility=-"
85
+ }
86
+ }
87
+ }
package/parser/index.js CHANGED
@@ -19,12 +19,12 @@
19
19
  * node parser/index.js list [--scene admin] [--verbose] # 列出全部语义 ID(--verbose 含字段与形态)
20
20
  * node parser/index.js query <semantic.id> # 查询单个 ID 映射详情
21
21
  * node parser/index.js audit --project <dir> [<cs…>] # 挂载点存在性 + 死通道静态审计()
22
- * node parser/index.js verify --oracle <o> --actual <a> # 渲染态逐点验收:computed style 期望 vs 实测 diff()
22
+ * node parser/index.js verify --oracle <o> --actual <a> # 渲染态逐点验收:computed style 期望 vs 实测 diff(G12 存在性闸 + 残差豁免 + 快照回归)
23
23
  * node parser/index.js registry <registry.json> # 登记表通道核验:登记项 sid / dims 是否在词典有落点()
24
24
  * node parser/watch.js # 开发态监听 mapping/ 自动重编译(§3.4.3)
25
25
  */
26
26
 
27
- import { readFileSync, statSync, readdirSync, existsSync } from "node:fs";
27
+ import { readFileSync, writeFileSync, statSync, readdirSync, existsSync } from "node:fs";
28
28
  import path from "node:path";
29
29
  import { fileURLToPath } from "node:url";
30
30
  import { buildMappingDataset } from "./loader.js";
@@ -110,7 +110,9 @@ function printHelp() {
110
110
  node parser/index.js match --batch <dir> 项目级批量匹配:目录下全部变更集 + 跨文件汇总
111
111
  node parser/index.js audit --project <dir> [<cs.json…>] 挂载点存在性 + 死通道静态审计(需目标项目源码)
112
112
  node parser/index.js verify --oracle <o.json> --actual <a.json> [--sids a,b]
113
- 渲染态逐点验收:computed style 期望 vs 实测 diff
113
+ 渲染态逐点验收:computed style 期望 vs 实测 diff(oracle.absent 存在性闸 + residual_accepted 残差豁免)
114
+ node parser/index.js verify --oracle <o.json> --actual <a.json> --baseline <snap.json> [--snapshot <out.json>]
115
+ 快照回归:与上轮快照比对基线漂移(上轮命中本轮退步 = 红);--snapshot 导出本轮快照
114
116
  node parser/index.js verify --oracle <o.json> --resolve [--sids a,b]
115
117
  实测端定位计划:sid → 词典 selector(项目页不打 data-sid 时按此抽取)
116
118
  node parser/index.js verify … --from-theme 钴蓝 --to-theme 橘橙
@@ -1852,7 +1854,16 @@ function findStructEvidence(probe, sources) {
1852
1854
  *
1853
1855
  * CLI:node parser/index.js verify --oracle <oracle.json> --actual <actual.json> [--sids a,b,c]
1854
1856
  * 实测端定位计划:node parser/index.js verify --oracle <oracle.json> --resolve [--sids a,b,c]
1855
- * 退出码:存在缺口 → 1;全部命中 → 0。
1857
+ * 快照回归:… --snapshot <path> 导出本次实测 + 判定结果为快照(json),供下轮 --baseline 比对
1858
+ * … --baseline <path> 与上一轮快照比对:上轮命中、本轮缺口的属性 = 基线漂移(红),治「上一轮改对的这一轮漂回」
1859
+ * 退出码:存在缺口(含存在性缺口 G12 / 基线漂移)→ 1;全部命中 → 0。
1860
+ *
1861
+ * G12 存在性闸(oracle.absent):效果图上**不存在的**出厂点位(面包屑 / 设置铃铛等收口项)
1862
+ * 列入 oracle.absent: ["sid", …],verify 逐个断言「实测转储中不存在,或 display:none / visibility:hidden」。
1863
+ * 治「该消失的没消失」——这类缺口在效果图上无可见元素 → 无 data-sid → 无期望值,逐点 diff 永远验不到。
1864
+ *
1865
+ * 残差豁免(oracle.residual_accepted):机制推算值 / 业务资产等**申报过的固有残差**点位不计缺口,
1866
+ * 每条 { "sid": …, "reason": … },reason 必填——防止把真缺口装进接受清单蒙混过关。
1856
1867
  */
1857
1868
  function cmdVerify(args, dataset) {
1858
1869
  // 几何 / 盒模型属性:值对不上 = 语境错位(宿主盒 / 内层盒模型 / 层叠上下文不同)
@@ -1879,6 +1890,8 @@ function cmdVerify(args, dataset) {
1879
1890
  const oraclePath = get("--oracle");
1880
1891
  const actualPath = get("--actual");
1881
1892
  const resolveOnly = args.includes("--resolve");
1893
+ const snapshotPath = get("--snapshot");
1894
+ const baselinePath = get("--baseline");
1882
1895
  if (!oraclePath || (!resolveOnly && !actualPath)) {
1883
1896
  console.error("✖ 缺少参数:node parser/index.js verify --oracle <oracle.json> --actual <actual.json> [--sids a,b,c]");
1884
1897
  console.error(" 或输出实测端定位计划:node parser/index.js verify --oracle <oracle.json> --resolve [--sids a,b,c]");
@@ -1914,6 +1927,14 @@ function cmdVerify(args, dataset) {
1914
1927
  };
1915
1928
 
1916
1929
  const oracle = readDump(oraclePath, "效果图期望");
1930
+ // oracle 顶层扩展字段(readDump 摊平后丢顶层,这里从原始文件补读)
1931
+ const oracleRaw = JSON.parse(readFileSync(path.resolve(oraclePath), "utf-8"));
1932
+ const absentSids = Array.isArray(oracleRaw.absent) ? oracleRaw.absent.filter(s => typeof s === "string" && s.trim()) : [];
1933
+ const residualAccepted = Array.isArray(oracleRaw.residual_accepted)
1934
+ ? oracleRaw.residual_accepted
1935
+ .map(r => (typeof r === "string" ? { sid: r, reason: "(未填 reason)" } : r))
1936
+ .filter(r => r && typeof r.sid === "string" && r.sid.trim())
1937
+ : [];
1917
1938
 
1918
1939
  let sids = Object.keys(oracle);
1919
1940
  if (sidFilter)
@@ -2009,11 +2030,21 @@ function cmdVerify(args, dataset) {
2009
2030
  console.log(`效果图基线:${path.resolve(oraclePath)}`);
2010
2031
  console.log(`项目实测:${path.resolve(actualPath)}`);
2011
2032
  console.log(`比对点位:${sids.length}${sidFilter ? `(--sids 过滤后)` : "(oracle 全量)"}`);
2033
+ if (absentSids.length > 0)
2034
+ console.log(`存在性闸(G12):${absentSids.length} 点位应已收口 —— ${absentSids.join("、")}`);
2035
+ if (residualAccepted.length > 0)
2036
+ console.log(`残差豁免:${residualAccepted.length} 点位已申报接受 —— ${residualAccepted.map(r => r.sid).join("、")}`);
2037
+
2038
+ // 残差豁免索引:sid → reason。命中豁免的属性缺口降级为「已申报残差」提示行,不进 missProps。
2039
+ const residualMap = new Map(residualAccepted.map(r => [r.sid.trim(), r.reason || "(未填 reason)"]));
2012
2040
 
2013
2041
  let hitProps = 0;
2014
2042
  let missProps = 0;
2043
+ let residualProps = 0;
2015
2044
  const attr = { selector_miss: 0, weight_override: 0, context_mismatch: 0 };
2016
2045
  const clean = [];
2046
+ // 快照收集:sid → { 属性: { expected, actual, verdict } };verdict: hit | miss | residual | absent-hit | absent-miss | selector-miss
2047
+ const snapshot = { generated_at: new Date().toISOString(), oracle: path.resolve(oraclePath), actual: path.resolve(actualPath), points: {} };
2017
2048
 
2018
2049
  for (const sid of sids) {
2019
2050
  const exp = oracle[sid];
@@ -2026,6 +2057,7 @@ function cmdVerify(args, dataset) {
2026
2057
  console.log(`\n✘ ${sid}\n - 项目页面未抽取到该点位 —— 选择器落空(挂载点缺失 / 元素未渲染 / data-sid 未标注)`);
2027
2058
  attr.selector_miss++;
2028
2059
  missProps++;
2060
+ snapshot.points[sid] = { verdict: "selector-miss" };
2029
2061
  continue;
2030
2062
  }
2031
2063
  const act = {};
@@ -2033,16 +2065,19 @@ function cmdVerify(args, dataset) {
2033
2065
  act[normProp(k)] = v;
2034
2066
 
2035
2067
  const lines = [];
2068
+ const snapProps = {};
2036
2069
  for (const [propRaw, expVal] of Object.entries(exp)) {
2037
2070
  const prop = normProp(propRaw);
2038
2071
  if (!(prop in act)) {
2039
2072
  lines.push(` - ${prop}: 期望 ${expVal} / 实测未取到 —— 选择器落空(该属性所在规则未命中)`);
2040
2073
  attr.selector_miss++;
2074
+ snapProps[prop] = { expected: expVal, actual: null, verdict: "selector-miss" };
2041
2075
  continue;
2042
2076
  }
2043
2077
  const expNorm = themeAlias ? themeAlias(expVal) : expVal;
2044
2078
  if (normValue(expNorm) === normValue(act[prop])) {
2045
2079
  hitProps++;
2080
+ snapProps[prop] = { expected: expVal, actual: act[prop], verdict: "hit" };
2046
2081
  continue;
2047
2082
  }
2048
2083
  const isGeom = GEOMETRY_PROPS.has(prop);
@@ -2051,6 +2086,14 @@ function cmdVerify(args, dataset) {
2051
2086
  attr.context_mismatch++;
2052
2087
  else
2053
2088
  attr.weight_override++;
2089
+ snapProps[prop] = { expected: expVal, actual: act[prop], verdict: "miss", kind };
2090
+ if (residualMap.has(sid)) {
2091
+ // 申报过的固有残差:降级为提示行,不进缺口计数
2092
+ residualProps++;
2093
+ snapProps[prop].verdict = "residual";
2094
+ lines.push(` ○ ${prop}: 期望 ${expVal} / 实得 ${act[prop]} —— 已申报固有残差(${residualMap.get(sid)}),不计缺口`);
2095
+ continue;
2096
+ }
2054
2097
  lines.push(` - ${prop}: 期望 ${expVal} / 实得 ${act[prop]} —— ${kind}(${
2055
2098
  isGeom ? "几何值对不上:宿主盒 / 内层盒模型或层叠上下文不同" : "同权重被组件 scoped / 行内样式反压,或令牌未生效"
2056
2099
  })`);
@@ -2059,21 +2102,107 @@ function cmdVerify(args, dataset) {
2059
2102
 
2060
2103
  if (lines.length === 0) {
2061
2104
  clean.push(sid);
2105
+ snapshot.points[sid] = { verdict: "hit", props: snapProps };
2062
2106
  }
2063
2107
  else {
2064
2108
  console.log(`\n✘ ${sid}`);
2065
2109
  for (const l of lines)
2066
2110
  console.log(l);
2111
+ snapshot.points[sid] = { verdict: "miss", props: snapProps };
2112
+ }
2113
+ }
2114
+
2115
+ // ---- G12 存在性闸:oracle.absent 点位必须「不存在或已隐藏」----
2116
+ let absentMiss = 0;
2117
+ if (absentSids.length > 0) {
2118
+ console.log(`\n── 存在性闸(G12):以下点位效果图上不存在,页面必须收口 ──`);
2119
+ for (const sid of absentSids) {
2120
+ if (sidFilter && !sidFilter.has(sid))
2121
+ continue;
2122
+ const actRaw = actual[sid];
2123
+ let gone = true;
2124
+ let evidence = "页面无此元素(未抽取到)";
2125
+ if (actRaw && typeof actRaw === "object") {
2126
+ const disp = normValue(actRaw["display"] ?? actRaw["display_style"] ?? "");
2127
+ const vis = normValue(actRaw["visibility"] ?? "");
2128
+ if (disp === "none" || vis === "hidden") {
2129
+ evidence = `display=${disp || "-"} visibility=${vis || "-"}`;
2130
+ }
2131
+ else {
2132
+ gone = false;
2133
+ evidence = `仍渲染可见(display=${disp || "?"})`;
2134
+ }
2135
+ }
2136
+ if (gone) {
2137
+ console.log(` ✔ ${sid} —— 已收口(${evidence})`);
2138
+ snapshot.points[sid] = { verdict: "absent-hit", evidence };
2139
+ }
2140
+ else {
2141
+ absentMiss++;
2142
+ console.log(` ✘ ${sid} —— ${evidence}:效果图无此元素,应 visible:false 收口(G12 存在性缺口)`);
2143
+ snapshot.points[sid] = { verdict: "absent-miss", evidence };
2144
+ }
2067
2145
  }
2068
2146
  }
2069
2147
 
2070
2148
  if (clean.length > 0)
2071
2149
  console.log(`\n✔ 逐点命中:${clean.join("、")}`);
2072
2150
 
2073
- console.log(`\n汇总:${sids.length} 点位 · ${hitProps} 属性命中 · ${missProps} 缺口`);
2074
- console.log(`归因:选择器落空 ${attr.selector_miss} / 权重被压 ${attr.weight_override} / 语境错位 ${attr.context_mismatch}`);
2075
- if (missProps > 0) {
2076
- console.log(`处置口径:按归因修正 —— 选择器落空补挂载点(属能力缺口,走 structure_ops / 补通道并申报);权重被压按 priority 声明提权(禁止 !important 掩盖);语境错位校准 layer(host / inner)。`);
2151
+ // ---- 快照回归:--snapshot 导出 / --baseline 漂移比对 ----
2152
+ let driftCount = 0;
2153
+ if (baselinePath) {
2154
+ let baseline;
2155
+ try {
2156
+ baseline = JSON.parse(readFileSync(path.resolve(baselinePath), "utf-8"));
2157
+ }
2158
+ catch (e) {
2159
+ console.error(`✖ 基线快照读取失败 ${baselinePath}:${e.message}`);
2160
+ process.exit(1);
2161
+ }
2162
+ const basePoints = baseline.points || {};
2163
+ const drifts = [];
2164
+ for (const [sid, cur] of Object.entries(snapshot.points)) {
2165
+ const prev = basePoints[sid];
2166
+ if (!prev)
2167
+ continue;
2168
+ // 点位级漂移:上轮命中/收口、本轮缺口
2169
+ const prevOk = prev.verdict === "hit" || prev.verdict === "absent-hit";
2170
+ const curOk = cur.verdict === "hit" || cur.verdict === "absent-hit";
2171
+ if (prevOk && !curOk) {
2172
+ drifts.push(`${sid}(${prev.verdict} → ${cur.verdict})`);
2173
+ }
2174
+ else if (prevOk && curOk && prev.props && cur.props) {
2175
+ for (const [prop, pv] of Object.entries(prev.props)) {
2176
+ if (pv.verdict === "hit" && cur.props[prop] && cur.props[prop].verdict !== "hit" && cur.props[prop].verdict !== "residual")
2177
+ drifts.push(`${sid}.${prop}(hit → ${cur.props[prop].verdict})`);
2178
+ }
2179
+ }
2180
+ }
2181
+ driftCount = drifts.length;
2182
+ if (driftCount > 0) {
2183
+ console.log(`\n✘ 基线漂移 ${driftCount} 处(上一轮已命中、本轮退步——每轮动工前先跑 verify 存快照,交付前比对):`);
2184
+ for (const d of drifts)
2185
+ console.log(` - ${d}`);
2186
+ }
2187
+ else {
2188
+ console.log(`\n✔ 基线快照比对:无漂移(上轮命中点位本轮全部保持)`);
2189
+ }
2190
+ }
2191
+ if (snapshotPath) {
2192
+ try {
2193
+ writeFileSync(path.resolve(snapshotPath), JSON.stringify(snapshot, null, 2), "utf-8");
2194
+ console.log(`\n快照已导出:${path.resolve(snapshotPath)}(下轮验证带 --baseline <本文件> 比对漂移)`);
2195
+ }
2196
+ catch (e) {
2197
+ console.error(`✖ 快照写入失败 ${snapshotPath}:${e.message}`);
2198
+ process.exitCode = 1;
2199
+ }
2200
+ }
2201
+
2202
+ console.log(`\n汇总:${sids.length} 点位 · ${hitProps} 属性命中 · ${missProps} 缺口${residualProps > 0 ? ` · ${residualProps} 已申报残差` : ""}${absentSids.length > 0 ? ` · 存在性 ${absentSids.length - absentMiss}/${absentSids.length} 收口` : ""}`);
2203
+ console.log(`归因:选择器落空 ${attr.selector_miss} / 权重被压 ${attr.weight_override} / 语境错位 ${attr.context_mismatch}${absentSids.length > 0 ? ` / 存在性缺口 ${absentMiss}` : ""}${driftCount > 0 ? ` / 基线漂移 ${driftCount}` : ""}`);
2204
+ if (missProps > 0 || absentMiss > 0 || driftCount > 0) {
2205
+ console.log(`处置口径:按归因修正 —— 选择器落空补挂载点(属能力缺口,走 structure_ops / 补通道并申报);权重被压按 priority 声明提权(禁止 !important 掩盖);语境错位校准 layer(host / inner);存在性缺口补 visible:false 收口并按词典 priority 提权;基线漂移逐条回归上轮修法。`);
2077
2206
  process.exitCode = 1;
2078
2207
  }
2079
2208
  else {
@@ -24,10 +24,12 @@
24
24
  * I build --dry 产物链自检 → exit 0 且输出含 dry-run
25
25
  * J match 三声明维度 → exit 0 且输出含「结构施工 structure_ops」
26
26
  * K audit 挂载点 / 死通道审计 → exit 1(夹具项目故意存在挂载点缺失 / 死通道)
27
- * L verify 渲染态逐点验收 → exit 1 且三类归因各计其数(夹具含故意缺口)
27
+ * L verify 渲染态逐点验收 → exit 1 且归因各计其数(夹具含故意缺口:三类渲染缺口 + G12 存在性缺口)
28
28
  * M match 登记表覆盖率细闸 → exit 1(夹具故意欠声明两条恒存在子项)
29
29
  * N registry 登记表通道核验 → exit 0 且输出含「登记表通道核验」
30
30
  * O registry 硬缺口负例 → exit 1(引用了不存在且未标 pending 的点位)
31
+ * U verify 残差豁免 + 收口正例(clean 夹具)→ exit 0 且输出含「已申报残差」「已收口」(申报点位不计缺口、收口点位绿灯)
32
+ * V verify 快照回归(snapshot → baseline)→ clean 快照为基线、缺口夹具为实测 → exit 1 且输出含「基线漂移」(上轮命中本轮退步必红)
31
33
  *
32
34
  * 任一步骤失败 → 本脚本 exit 1,并回显该步骤完整输出便于定位。
33
35
  */
@@ -131,14 +133,13 @@ const steps = [
131
133
  },
132
134
  {
133
135
  key: "L",
134
- name: "verify 渲染态逐点验收(夹具含三类故意缺口 → exit 1)",
136
+ name: "verify 渲染态逐点验收(夹具含三类渲染缺口 + G12 存在性缺口 → exit 1)",
135
137
  script: INDEX,
136
138
  args: ["verify", "--oracle", path.join(FIXTURES, "verify", "oracle.json"), "--actual", path.join(FIXTURES, "verify", "actual.json")],
137
- expect: { code: 1, includes: "选择器落空 1 / 权重被压 1 / 语境错位 2" },
139
+ expect: { code: 1, includes: "选择器落空 2 / 权重被压 1 / 语境错位 2 / 存在性缺口 1" },
138
140
  },
139
141
  {
140
142
  key: "M",
141
- name: "match 登记表覆盖率细闸(夹具故意欠声明两条恒存在子项 → exit 1)",
142
143
  script: INDEX,
143
144
  args: ["match", "--registry", path.join(FIXTURES, "registry-sample.json"), path.join(FIXTURES, "changeset-registry-gap.json")],
144
145
  expect: { code: 1, includes: ["登记表覆盖率", "缺口 2"] },
@@ -192,19 +193,60 @@ const steps = [
192
193
  args: ["match", path.join(FIXTURES, "changeset-neg8-g11-oracle.json")],
193
194
  expect: { code: 1, includes: ["G11", "主题错配"] },
194
195
  },
196
+ {
197
+ key: "U",
198
+ name: "verify 残差豁免 + 收口正例(clean 夹具:申报点位不计缺口、absent 已收口 → exit 0)",
199
+ script: INDEX,
200
+ args: ["verify", "--oracle", path.join(FIXTURES, "verify", "oracle.json"), "--actual", path.join(FIXTURES, "verify", "actual-clean.json")],
201
+ expect: { code: 0, includes: ["已申报固有残差", "已收口", "验收通过"] },
202
+ },
203
+ {
204
+ key: "V",
205
+ name: "verify 快照回归(clean 快照为基线 + L 缺口夹具为实测:上轮命中本轮退步 → exit 1 且含「基线漂移」)",
206
+ script: INDEX,
207
+ args: [], // 两段式:先 --snapshot 导出,再 --baseline 比对,见下方 custom 分支
208
+ expect: { code: 1, includes: "基线漂移" },
209
+ custom: "snapshot-drift",
210
+ },
195
211
  ];
196
212
 
197
213
  const results = [];
198
214
  let failed = 0;
199
215
 
200
- console.log("=== A-T 端到端一键回归 ===\n");
216
+ console.log("=== A-V 端到端一键回归 ===\n");
217
+
218
+ const SNAPSHOT_TMP = path.join(FIXTURES, "verify", "snapshot-latest.json");
201
219
 
202
220
  for (const step of steps) {
203
- const { status, stdout, stderr } = spawnSync(
204
- process.execPath,
205
- [step.script, ...step.args],
206
- { cwd: ROOT, encoding: "utf-8" },
207
- );
221
+ // V 步两段式:① 用 clean 夹具导出「上一轮全绿」快照;② 用 L 缺口夹具 + --baseline 比对 → 应报基线漂移
222
+ const spawnArgs = step.custom === "snapshot-drift"
223
+ ? []
224
+ : [step.script, ...step.args];
225
+ let status, stdout, stderr;
226
+ if (step.custom === "snapshot-drift") {
227
+ const s1 = spawnSync(process.execPath, [
228
+ INDEX, "verify",
229
+ "--oracle", path.join(FIXTURES, "verify", "oracle.json"),
230
+ "--actual", path.join(FIXTURES, "verify", "actual-clean.json"),
231
+ "--snapshot", SNAPSHOT_TMP,
232
+ ], { cwd: ROOT, encoding: "utf-8" });
233
+ if (s1.status !== 0 || !existsSync(SNAPSHOT_TMP)) {
234
+ status = s1.status;
235
+ stdout = s1.stdout ?? "";
236
+ stderr = `快照导出段失败(exit ${s1.status}):快照回归前置不成立\n${s1.stderr ?? ""}`;
237
+ }
238
+ else {
239
+ ({ status, stdout, stderr } = spawnSync(process.execPath, [
240
+ INDEX, "verify",
241
+ "--oracle", path.join(FIXTURES, "verify", "oracle.json"),
242
+ "--actual", path.join(FIXTURES, "verify", "actual.json"),
243
+ "--baseline", SNAPSHOT_TMP,
244
+ ], { cwd: ROOT, encoding: "utf-8" }));
245
+ }
246
+ }
247
+ else {
248
+ ({ status, stdout, stderr } = spawnSync(process.execPath, spawnArgs, { cwd: ROOT, encoding: "utf-8" }));
249
+ }
208
250
  const output = `${stdout ?? ""}${stderr ?? ""}`;
209
251
  const code = status ?? -1;
210
252
 
@@ -234,4 +276,4 @@ if (failed > 0) {
234
276
  console.error(`\n✖ ${failed} 步未通过,见上方详情`);
235
277
  process.exit(1);
236
278
  }
237
- console.log("\n✔ A-T 全部通过,mapping/parser 可提交或发版");
279
+ console.log("\n✔ A-W 全部通过,mapping/parser 可提交或发版");
@@ -470,15 +470,34 @@ html .navbar .navbar-breadcrumb-wrap {
470
470
  # 同构异色复用(oracle.theme_mapping 已声明时必带)——效果图色值按语义槽位归一为本主题色值再比:
471
471
  node parser/index.js verify --oracle oracle.json --actual actual.json --from-theme 钴蓝 --to-theme 橘橙
472
472
  ```
473
- 退出码:存在缺口 → `1`。
473
+ 退出码:存在缺口(含存在性缺口 / 基线漂移)→ `1`。
474
474
 
475
- **归因与处置(三类,禁止用 `!important` 掩盖)**:
475
+ **归因与处置(四类,禁止用 `!important` 掩盖)**:
476
476
 
477
477
  | 归因 | 触发 | 处置 |
478
478
  |---|---|---|
479
479
  | **选择器落空** | oracle 有点位、actual 无(或某属性取不到) | 补挂载点:按 `structure_ops` 挂类 / 补 `data-sid` / 确认元素已渲染(属能力缺口,交付说明申报) |
480
480
  | **权重被压** | 颜色 / 字重 / 透明度 / 阴影类属性值不符 | 按词典 `priority` 声明的策略提权(`scope_qualified` / `self_plus_important` / `deep_qualified`) |
481
481
  | **语境错位** | 几何 / 盒模型属性(padding / 尺寸 / 对齐 / 行高)值不符 | 校准 `layer`(`host` vs `inner`)——值对了落错层 |
482
+ | **存在性缺口(G12)** | `oracle.absent` 点位在页面仍渲染可见(`display` 非 `none`) | 补 `visible:false` 收口并按词典 `priority` 提权(历史缺口:面包屑 `display:none` 被同权重 scoped 规则反压——写了不生效也是它) |
483
+
484
+ **存在性闸(`oracle.absent`)**:效果图上不存在的出厂点位(面包屑 / 设置铃铛等)逐条列入变更集 `oracle.absent`,verify 逐个断言「actual 不存在或 `display:none` / `visibility:hidden`」。**这类缺口无期望值,逐点 diff 永远验不到——不进 absent 就不在验收范围内**(2026-09-21 面包屑事故机制化)。
485
+
486
+ **残差豁免(`oracle.residual_accepted`)**:`[{ "sid": …, "reason": … }]`(reason 必填)。命中豁免的属性差异降级为「已申报固有残差」提示行,不进缺口计数;验收报告单列豁免明细,逐条可审计。**机制推算值(等宽槽宽)、业务资产(logo 图形)必须申报**——不申报 = 每轮验收重新报缺口 = 「改了很多版始终不行」的体感来源。
487
+
488
+ **快照回归(`--snapshot` / `--baseline`,治「上一轮改对的这一轮漂回」)**:
489
+
490
+ ```bash
491
+ # 每轮动工前:先对当前页面跑一次 verify 并导出快照
492
+ node parser/index.js verify --oracle oracle.json --actual actual.json --snapshot snapshot-r1.json
493
+ # 本轮交付前:带 --baseline 与上轮快照比对
494
+ node parser/index.js verify --oracle oracle.json --actual actual.json --baseline snapshot-r1.json
495
+ ```
496
+
497
+ - 上轮 `hit` / `absent-hit`、本轮退步的点位 / 属性 = **基线漂移**,逐条列出且 `exit 1`——先回归上轮修法,再谈新缺口。
498
+ - 项目还原到出厂(design-shell 回空壳)后第一轮验证**不带** `--baseline`(基线已失真),改以 clean 夹具重新起拍。
499
+
500
+ **夹具与设计资产的边界(职责单侧沉淀红线)**:`「页面名-主题」`命名法(如 `运单管理-钴蓝.html`)是设计侧效果图库的专属资产命名——**项目侧(含 parser/fixtures 回归夹具)禁止出现与设计侧资产同名的文件**。回归需要 mockup 实物时,一律用「回归夹具-」前缀合成名(如 `回归夹具-列表页-钴蓝.html`),点集为回归机制定制、与真基线**不同且不可互替**;真实效果图基线唯一沉淀在设计侧 `page-specs/styles/<方案>/scenes/<场景>/mockups/`,项目侧 `oracle.mockup` 指向它靠 `resolveFirstExisting` 跨库解析(变更集同目录 → cwd → 技能根 → 设计侧兄弟目录),不做本地拷贝。
482
501
 
483
502
  - 验收结果写入最终回复(命中 N / 缺口 M / 归因分布 / 修正)与 `code-quality` 结果块;**不得只报「已完成」**。
484
503
  - 变更集无 `oracle` 时输出 `N/A`(无基线),并在交付说明注明「无验收基线」——这是设计侧应补的项,回流设计侧。
@@ -502,7 +521,7 @@ html .navbar .navbar-breadcrumb-wrap {
502
521
  ## 端到端回归(改 parser/mapping 后必跑)
503
522
 
504
523
  ```bash
505
- node parser/regression.js # 一键跑完 A-Q 十七步(自动断言退出码与关键输出,任一失败 exit 1 并回显详情)
524
+ node parser/regression.js # 一键跑完 A-V 二十二步(自动断言退出码与关键输出,任一失败 exit 1 并回显详情)
506
525
  npm run regression # 等价写法
507
526
  ```
508
527
 
@@ -529,9 +548,11 @@ node parser/index.js match parser/fixtures/changeset-neg5-g8-stub.json # Q. G8
529
548
  node parser/index.js match parser/fixtures/changeset-neg6-g9-identity.json # R. G9 身份闸负例:scheme=tangerine 却引 default 方案登记表(分母偷换)→ exit 1
530
549
  node parser/index.js match parser/fixtures/changeset-neg7-g10-dims.json # S. G10 dims 闸负例:header_cell 缺 text_align/nowrap(点位在细节丢)→ exit 1
531
550
  node parser/index.js match parser/fixtures/changeset-neg8-g11-oracle.json # T. G11 oracle 闸负例:橘橙主色配钴蓝效果图且无 theme_mapping(主题错配)→ exit 1
551
+ node parser/index.js verify --oracle parser/fixtures/verify/oracle.json --actual parser/fixtures/verify/actual-clean.json # U. 残差豁免 + G12 收口正例:申报点位不计缺口、absent 已收口 → exit 0
552
+ # V. 快照回归(两段式,regression.js 内部自动展开):clean 夹具导快照 → 缺口夹具比对 → 基线漂移 → exit 1
532
553
  ```
533
554
 
534
- 通过标准:regression.js 输出 `A✔ B✔ … T✔` 且 exit 0;分步跑时各步符合注释预期。夹具含义见各文件 `$comment`。
555
+ 通过标准:regression.js 输出 `A✔ B✔ … V✔` 且 exit 0;分步跑时各步符合注释预期。夹具含义见各文件 `$comment`。
535
556
 
536
557
  ## 配套工具(作者/维护侧)
537
558
 
@@ -1,44 +0,0 @@
1
- <!DOCTYPE html>
2
- <!-- G8 模式闸正例配套 · 效果图实物(changeset-fullpage-restore.json 的 oracle.mockup 夹具,非真实交付件)。
3
- 文件名带主题词「钴蓝」(oracle 主题一致性校验依据);每个 data-sid 对应 oracle.sids。
4
- 文末注释内含 1 个同形 data-sid(parser 剥注释后不计数,验证 §7.1 注释兜底防线)。 -->
5
- <html>
6
- <head><meta charset="utf-8"><title>运单管理-钴蓝</title></head>
7
- <body>
8
- <div class="sidebar-container" data-sid="admin.layout.sidebar">
9
- <div class="sidebar-logo" data-sid="admin.layout.sidebar.logo_area">LOGO</div>
10
- <ul class="el-menu" data-sid="admin.layout.sidebar.menu"><li>菜单项</li></ul>
11
- </div>
12
- <header class="navbar" data-sid="admin.layout.header">
13
- <div class="language-trigger" data-sid="admin.layout.header.lang_switch"><span class="language-icon">中</span><span class="language-text">简体中文</span></div>
14
- <div class="navbar-right el-dropdown-link" data-sid="admin.layout.header.user_profile"><img alt=""><p>张伟</p></div>
15
- </header>
16
- <div class="tags-view" data-sid="admin.layout.tabs"><span class="scroll-item">首页</span></div>
17
- <main class="app-main">
18
- <div class="vts-page-header" data-sid="admin.content.page_header"><h2>运单管理</h2></div>
19
- <div class="filter-area" data-sid="admin.content.filter_area"><input></div>
20
- <div class="table-area">
21
- <div class="table-bar" data-sid="admin.content.table.operation_bar"><button>新增运单</button></div>
22
- <table data-sid="admin.content.table.area">
23
- <thead>
24
- <tr>
25
- <th class="el-table__cell" data-sid="admin.content.table.header_cell">列名</th>
26
- </tr>
27
- </thead>
28
- <tbody>
29
- <tr>
30
- <td class="el-table__cell" data-sid="admin.content.table.cell">单元格文本</td>
31
- <td class="operate" data-sid="admin.content.table.operation_column"><a>查看</a><span class="divider"></span><a>更多</a></td>
32
- <td class="amount" data-sid="admin.content.table.cell_amount">¥1,280.00</td>
33
- <td><img class="thumb" data-sid="admin.content.table.cell_thumb"></td>
34
- <td><span class="tag" data-sid="admin.content.table.cell_status">已签收</span><span class="tag global" data-sid="component.tag.status">状态</span></td>
35
- </tr>
36
- </tbody>
37
- </table>
38
- <button class="danger" data-sid="component.button.danger">批量删除</button>
39
- <div class="pagination" data-sid="admin.content.pagination">1 2 3</div>
40
- </div>
41
- </main>
42
- <!-- 注释内同形串(剥注释后不计数):data-sid="admin.content.page_header" -->
43
- </body>
44
- </html>